From b4c988db3cde046d6169e8f52ebeba34dbe91b62 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sun, 22 Jun 2025 10:03:26 -0400 Subject: [PATCH 01/10] First pass at `macro_code_coverage` tool integration --- .github/workflows/ci.yml | 2 +- scripts/test.sh | 54 ++++++++++----- src/components/console/spec/compiler_spec.cr | 4 +- .../spec/compiler_passes/auto_wire_spec.cr | 6 +- .../compiler_passes/define_getters_spec.cr | 8 +-- .../merge_extension_config_spec.cr | 28 ++++---- .../normalize_definitions_spec.cr | 7 +- .../compiler_passes/process_aliases_spec.cr | 6 +- .../process_auto_configurations_spec.cr | 14 ++-- .../compiler_passes/register_services_spec.cr | 22 +++--- .../resolve_parameter_placeholders_spec.cr | 10 +-- .../compiler_passes/resolve_values_spec.cr | 6 +- .../validate_arguments_spec.cr | 22 +++--- .../compiler_passes/validate_generics_spec.cr | 8 +-- .../spec/extension_spec.cr | 8 +-- .../event_dispatcher/spec/compiler_spec.cr | 16 ++--- src/components/framework/spec/bundle_spec.cr | 8 +-- .../framework/spec/compiler_spec.cr | 68 +++++++++---------- .../ext/console/register_commands_spec.cr | 6 +- .../routing/spec/requirement/enum_spec.cr | 2 +- src/components/spec/CHANGELOG.md | 6 +- src/components/spec/spec/compiler_spec.cr | 28 +++++--- src/components/spec/spec/methods_spec.cr | 26 ++++--- src/components/spec/src/methods.cr | 68 +++++++++++++------ 24 files changed, 247 insertions(+), 186 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b626b7f8..27a9185c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true directory: coverage - files: '**/cov.xml' # There is no `unreachable.codecov.json` file when running _only_ compiled specs + files: '**/cov.xml,**/macro_coverage.*.codecov.json' # There is no `unreachable.codecov.json` file when running _only_ compiled specs flags: compiled verbose: true - uses: codecov/test-results-action@v1 diff --git a/scripts/test.sh b/scripts/test.sh index eba4e86e1..cb9eebd91 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -6,21 +6,41 @@ function runSpecs() ( $CRYSTAL spec "${DEFAULT_BUILD_OPTIONS[@]}" "${DEFAULT_OPTIONS[@]}" "src/components/$1/spec" ) -# Coverage generation logic based on https://hannes.kaeufler.net/posts/measuring-code-coverage-in-crystal-with-kcov +# Runtime coverage generation logic based on https://hannes.kaeufler.net/posts/measuring-code-coverage-in-crystal-with-kcov. +# Additionally generates a coverage report for unreachable code. +# +# Compiled time code generates a macro code coverage report for the entire component, and each compiled sub-process spec. # # $1 component name function runSpecsWithCoverage() ( set -e rm -rf "coverage/$1" mkdir -p coverage/bin "coverage/$1" + + # Build spec binary that covers entire `spec/` directory to run coverage against. echo "require \"../../src/components/$1/spec/**\"" > "./coverage/bin/$1.cr" && \ $CRYSTAL build "${DEFAULT_BUILD_OPTIONS[@]}" "./coverage/bin/$1.cr" -o "./coverage/bin/$1" && \ - kcov $(if $IS_CI != "true"; then echo "--cobertura-only"; fi) --clean --include-path="./src/components/$1" "./coverage/$1" "./coverage/bin/$1" --junit_output="./coverage/$1/junit.xml" "${DEFAULT_OPTIONS[@]}" + ATHENA_SPEC_COVERAGE_OUTPUT_DIR="$(realpath ./coverage/$1/)" \ + kcov $(if $IS_CI != "true"; then echo "--cobertura-only"; fi) \ + --clean \ + --include-path="./src/components/$1"\ + "./coverage/$1"\ + "./coverage/bin/$1"\ + --junit_output="./coverage/$1/junit.xml"\ + "${DEFAULT_OPTIONS[@]}" + + if [ "$TYPE" != "unit" ] + then + # Generate macro coverage report. + # The report itself is sent to STDOUT while other output is sent to STDERR. + # We can ignore STDERR since those failures would be captured as part of running the specs themselves. + $CRYSTAL tool macro_code_coverage --no-color "./coverage/bin/$1.cr" > "./coverage/$1/macro_coverage.root.codecov.json" + fi - # Scope this to only non-compiled tests as that's all it's relevant for. - if [ $TYPE != "compiled" ] + # Only runtime code can be unreachable. + if [ "$TYPE" != "compiled" ] then - $CRYSTAL tool unreachable --format=codecov "./coverage/bin/$1.cr" > "./coverage/$1/unreachable.codecov.json" + $CRYSTAL tool unreachable --no-color --format=codecov "./coverage/bin/$1.cr" > "./coverage/$1/unreachable.codecov.json" fi ) @@ -39,13 +59,13 @@ IS_CI=${CI:="false"} COMPONENT=${1-all} TYPE=${2-all} -if [ $TYPE == "unit" ] +if [ "$TYPE" == "unit" ] then DEFAULT_OPTIONS+=("--tag=~compiled") -elif [ $TYPE == "compiled" ] +elif [ "$TYPE" == "compiled" ] then DEFAULT_OPTIONS+=("--tag=compiled") -elif [ $TYPE != "all" ] +elif [ "$TYPE" != "all" ] then echo "Second argument must be 'unit', 'compiled', or 'all' got '${2}'." exit 1 @@ -53,25 +73,25 @@ fi EXIT_CODE=0 -if [ $COMPONENT != "all" ] +if [ "$COMPONENT" != "all" ] then - if [ $HAS_KCOV = "true" ] + if [ "$HAS_KCOV" = "true" ] then - runSpecsWithCoverage $COMPONENT + runSpecsWithCoverage "$COMPONENT" else - runSpecs $COMPONENT + runSpecs "$COMPONENT" fi exit $? fi - -for component in $(find src/components/ -maxdepth 2 -type f -name shard.yml | xargs -I{} dirname {} | xargs -I{} basename {} | sort); do +$HAS_KCOV +for component in $(find src/components/ -maxdepth 2 -type f -name shard.yml -print0 | xargs -0 -I{} dirname {} | xargs -0 -I{} basename {} | sort); do echo "::group::$component" - if [ $HAS_KCOV = "true" ] + if [ "$HAS_KCOV" = "true" ] then - runSpecsWithCoverage $component + runSpecsWithCoverage "$component" else - runSpecs $component + runSpecs "$component" fi if [ $? -eq 1 ]; then diff --git a/src/components/console/spec/compiler_spec.cr b/src/components/console/spec/compiler_spec.cr index 6ab2c5b67..7d253b193 100644 --- a/src/components/console/spec/compiler_spec.cr +++ b/src/components/console/spec/compiler_spec.cr @@ -4,7 +4,7 @@ describe Athena::Console do describe "compiler errors", tags: "compiled" do describe "when a command configured via annotation doesn't have a name" do it "non hidden no aliases" do - ASPEC::Methods.assert_error "Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR + ASPEC::Methods.assert_compile_time_error "Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR require "./spec_helper.cr" @[ACONA::AsCommand] @@ -19,7 +19,7 @@ describe Athena::Console do end it "hidden" do - ASPEC::Methods.assert_error "Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR + ASPEC::Methods.assert_compile_time_error "Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR require "./spec_helper.cr" @[ACONA::AsCommand(hidden: true)] diff --git a/src/components/dependency_injection/spec/compiler_passes/auto_wire_spec.cr b/src/components/dependency_injection/spec/compiler_passes/auto_wire_spec.cr index bdd24b694..4399c2dd9 100644 --- a/src/components/dependency_injection/spec/compiler_passes/auto_wire_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/auto_wire_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} CR @@ -36,7 +36,7 @@ record SameInstanceClient, a : SameInstancePrimary, b : SameInstanceAliasInterfa describe ADI::ServiceContainer do describe "compiler errors", tags: "compiled" do it "does not resolve an un-aliased interface when there is only 1 implementation" do - assert_error "Failed to resolve value for parameter 'a : SomeInterface' of service 'bar' (Bar).", <<-CR + assert_compile_time_error "Failed to resolve value for parameter 'a : SomeInterface' of service 'bar' (Bar).", <<-CR module SomeInterface; end @[ADI::Register] diff --git a/src/components/dependency_injection/spec/compiler_passes/define_getters_spec.cr b/src/components/dependency_injection/spec/compiler_passes/define_getters_spec.cr index 02f93e896..25696c1f8 100644 --- a/src/components/dependency_injection/spec/compiler_passes/define_getters_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/define_getters_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} CR @@ -53,7 +53,7 @@ describe ADI::ServiceContainer::DefineGetters, tags: "compiled" do describe "compiler errors" do describe "aliases" do it "does not expose named getter for non-public string aliases" do - assert_error "undefined method 'bar' for Athena::DependencyInjection::ServiceContainer", <<-'CR' + assert_compile_time_error "undefined method 'bar' for Athena::DependencyInjection::ServiceContainer", <<-'CR' module SomeInterface; end @[ADI::Register] @@ -67,7 +67,7 @@ describe ADI::ServiceContainer::DefineGetters, tags: "compiled" do end it "does not expose typed getter for non-public typed aliases" do - assert_error "undefined method 'get' for Athena::DependencyInjection::ServiceContainer", <<-'CR' + assert_compile_time_error "undefined method 'get' for Athena::DependencyInjection::ServiceContainer", <<-'CR' module SomeInterface; end @[ADI::Register] diff --git a/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr b/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr index 4573eabe8..6f52594b2 100644 --- a/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -12,7 +12,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do describe "compiler errors" do describe "root level" do it "errors if a required configuration value has not been provided" do - assert_error "Required configuration property 'test.id : Int32' must be provided.", <<-'CR' + assert_compile_time_error "Required configuration property 'test.id : Int32' must be provided.", <<-'CR' module Schema include ADI::Extension::Schema @@ -31,7 +31,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if there is a collection type mismatch" do - assert_error "Expected configuration value 'test.foo' to be a 'Array(Int32)', but got 'Array(String)'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.foo' to be a 'Array(Int32)', but got 'Array(String)'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -49,7 +49,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if there is a type mismatch within an array" do - assert_error "Expected configuration value 'test.foo[0]' to be a 'Int32', but got 'UInt64'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.foo[0]' to be a 'Int32', but got 'UInt64'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -67,7 +67,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if a configuration value not found in the schema is encountered" do - assert_error "Encountered unexpected property 'test.name' with value '\"Fred\"'.", <<-'CR' + assert_compile_time_error "Encountered unexpected property 'test.name' with value '\"Fred\"'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -88,7 +88,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do describe "nested level" do it "errors if a configuration value has the incorrect type" do - assert_error "Required configuration property 'test.sub_config.defaults.id : Int32' must be provided.", <<-'CR' + assert_compile_time_error "Required configuration property 'test.sub_config.defaults.id : Int32' must be provided.", <<-'CR' module Schema include ADI::Extension::Schema @@ -119,7 +119,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if there is a collection type mismatch" do - assert_error "Expected configuration value 'test.sub_config.defaults.foo' to be a 'Array(Int32)', but got 'Array(String)'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.sub_config.defaults.foo' to be a 'Array(Int32)', but got 'Array(String)'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -149,7 +149,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if there is a type mismatch within an array" do - assert_error "Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -179,7 +179,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if there is a type mismatch within an array without type hint" do - assert_error "Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -209,7 +209,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if there is a type mismatch within an array using NoReturn schema default" do - assert_error "Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -239,7 +239,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if a configuration value not found in the schema is encountered" do - assert_error "Encountered unexpected property 'test.sub_config.defaults.name' with value '\"Fred\"'.", <<-'CR' + assert_compile_time_error "Encountered unexpected property 'test.sub_config.defaults.name' with value '\"Fred\"'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -271,7 +271,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if a configuration value has the incorrect type" do - assert_error "Extension 'foo' is configured, but no extension with that name has been registered.", <<-'CR' + assert_compile_time_error "Extension 'foo' is configured, but no extension with that name has been registered.", <<-'CR' ADI.configure({ foo: { id: 1 @@ -281,7 +281,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "errors if nothing is configured, but a property is required" do - assert_error "Required configuration property 'test.id : Int32' must be provided.", <<-'CR' + assert_compile_time_error "Required configuration property 'test.id : Int32' must be provided.", <<-'CR' require "../spec_helper" module Schema diff --git a/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr b/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr index b957c3b9c..c593e58b7 100644 --- a/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" module MySchema include ADI::Extension::Schema @@ -32,7 +32,7 @@ end describe ADI::ServiceContainer::NormalizeDefinitions, tags: "compiled" do describe "compiler errors" do it "`class` is not provided" do - assert_error "Service 'some_service' is missing required 'class' property.", <<-'CR' + assert_compile_time_error "Service 'some_service' is missing required 'class' property.", <<-'CR' SERVICE_HASH["some_service"] = { public: false, } @@ -43,6 +43,7 @@ describe ADI::ServiceContainer::NormalizeDefinitions, tags: "compiled" do it "applies defaults to missing properties" do ASPEC::Methods.assert_success <<-'CR' require "../spec_helper.cr" + module MySchema include ADI::Extension::Schema diff --git a/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr b/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr index c0734e6b3..751f347a9 100644 --- a/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr @@ -8,8 +8,8 @@ private def assert_success(code : String, *, line : Int32 = __LINE__) : Nil CR end -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -18,7 +18,7 @@ end describe ADI::ServiceContainer::ProcessAliases, tags: "compiled" do it "errors if unable to determine the alias name" do - assert_error "Alias cannot be automatically determined for 'foo' (Foo). If the type includes multiple interfaces, provide the interface to alias as the first positional argument to `@[ADI::AsAlias]`.", <<-'CR' + assert_compile_time_error "Alias cannot be automatically determined for 'foo' (Foo). If the type includes multiple interfaces, provide the interface to alias as the first positional argument to `@[ADI::AsAlias]`.", <<-'CR' module SomeInterface; end module OtherInterface; end diff --git a/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr b/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr index df5177a14..15f04e0b6 100644 --- a/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -226,14 +226,14 @@ describe ADI::ServiceContainer::ProcessAutoconfigureAnnotations do describe "compiler errors", tags: "compiled" do describe "tags" do it "errors if the `tags` field is not of a valid type" do - assert_error "Tags for 'foo' must be an 'ArrayLiteral', got 'NumberLiteral'.", <<-CR + assert_compile_time_error "Tags for 'foo' must be an 'ArrayLiteral', got 'NumberLiteral'.", <<-CR @[ADI::Register(tags: 123)] record Foo CR end it "errors if the `tags` field on the auto configuration is not of a valid type" do - assert_error "Tags for auto configuration of 'Test' must be an 'ArrayLiteral', got 'NumberLiteral'.", <<-CR + assert_compile_time_error "Tags for auto configuration of 'Test' must be an 'ArrayLiteral', got 'NumberLiteral'.", <<-CR @[ADI::Autoconfigure(tags: 123)] module Test; end @@ -246,7 +246,7 @@ describe ADI::ServiceContainer::ProcessAutoconfigureAnnotations do describe ADI::TaggedIterator do it "errors if used with unsupported collection type" do - assert_error "Failed to register service 'fqn_tagged_iterator_named_client' (FQNTaggedIteratorNamedClient). Collection parameter '@[ADI::TaggedIterator] services : Set(String)' type must be one of `Indexable`, `Iterator`, or `Enumerable`. Got 'Set'.", <<-CR + assert_compile_time_error "Failed to register service 'fqn_tagged_iterator_named_client' (FQNTaggedIteratorNamedClient). Collection parameter '@[ADI::TaggedIterator] services : Set(String)' type must be one of `Indexable`, `Iterator`, or `Enumerable`. Got 'Set'.", <<-CR @[ADI::Register] class FQNTaggedIteratorNamedClient getter services @@ -260,7 +260,7 @@ describe ADI::ServiceContainer::ProcessAutoconfigureAnnotations do describe "calls" do it "errors if the method of a call is empty" do - assert_error "Method name cannot be empty.", <<-CR + assert_compile_time_error "Method name cannot be empty.", <<-CR @[ADI::Autoconfigure(calls: [{""}])] module Test; end @@ -272,7 +272,7 @@ describe ADI::ServiceContainer::ProcessAutoconfigureAnnotations do end it "errors if the method does not exist on the type" do - assert_error "Failed to auto register service for 'foo' (Foo). Call references non-existent method 'foo'.", <<-CR + assert_compile_time_error "Failed to auto register service for 'foo' (Foo). Call references non-existent method 'foo'.", <<-CR @[ADI::Autoconfigure(calls: [{"foo"}])] module Test; end diff --git a/src/components/dependency_injection/spec/compiler_passes/register_services_spec.cr b/src/components/dependency_injection/spec/compiler_passes/register_services_spec.cr index 0673a7720..3f11f9503 100644 --- a/src/components/dependency_injection/spec/compiler_passes/register_services_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/register_services_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -101,7 +101,7 @@ end describe ADI::ServiceContainer::RegisterServices do describe "compiler errors", tags: "compiled" do it "errors if a service has multiple ADI::Register annotations but not all of them have a name" do - assert_error "Failed to auto register services for 'Foo'. Each service must explicitly provide a name when auto registering more than one service based on the same type.", <<-CR + assert_compile_time_error "Failed to auto register services for 'Foo'. Each service must explicitly provide a name when auto registering more than one service based on the same type.", <<-CR @[ADI::Register(name: "one")] @[ADI::Register] record Foo @@ -109,14 +109,14 @@ describe ADI::ServiceContainer::RegisterServices do end it "errors if the generic service does not have a name." do - assert_error "Failed to auto register service for 'Foo(T)'. Generic services must explicitly provide a name.", <<-CR + assert_compile_time_error "Failed to auto register service for 'Foo(T)'. Generic services must explicitly provide a name.", <<-CR @[ADI::Register] record Foo(T) CR end it "errors if the service is already registered" do - assert_error "Failed to auto register service for 'my_service' (MyService). It is already registered.", <<-CR + assert_compile_time_error "Failed to auto register service for 'my_service' (MyService). It is already registered.", <<-CR @[ADI::Register] record MyService @@ -140,7 +140,7 @@ describe ADI::ServiceContainer::RegisterServices do describe "factory" do it "errors if method is an instance method" do - assert_error "Failed to auto register service 'foo'. Factory method 'foo' within 'Foo' is an instance method.", <<-CR + assert_compile_time_error "Failed to auto register service 'foo'. Factory method 'foo' within 'Foo' is an instance method.", <<-CR @[ADI::Register(factory: "foo")] record Foo do def foo; end @@ -149,7 +149,7 @@ describe ADI::ServiceContainer::RegisterServices do end it "errors if the method is missing" do - assert_error "Failed to auto register service 'foo'. Factory method 'foo' within 'Foo' does not exist.", <<-CR + assert_compile_time_error "Failed to auto register service 'foo'. Factory method 'foo' within 'Foo' does not exist.", <<-CR @[ADI::Register(factory: "foo")] record Foo CR @@ -158,14 +158,14 @@ describe ADI::ServiceContainer::RegisterServices do describe "tags" do it "errors if not all tags have a `name` field" do - assert_error "Failed to auto register service 'foo'. All tags must have a name.", <<-CR + assert_compile_time_error "Failed to auto register service 'foo'. All tags must have a name.", <<-CR @[ADI::Register(tags: [{priority: 100}])] record Foo CR end it "errors if not all tags are of the proper type" do - assert_error "Tag '100' must be a 'StringLiteral' or 'NamedTupleLiteral', got 'NumberLiteral'.", <<-CR + assert_compile_time_error "Tag '100' must be a 'StringLiteral' or 'NamedTupleLiteral', got 'NumberLiteral'.", <<-CR @[ADI::Register(tags: [100])] record Foo CR @@ -174,14 +174,14 @@ describe ADI::ServiceContainer::RegisterServices do describe "calls" do it "errors if the method of a call is empty" do - assert_error "Method name cannot be empty.", <<-CR + assert_compile_time_error "Method name cannot be empty.", <<-CR @[ADI::Register(calls: [{""}])] record Foo CR end it "errors if the method does not exist on the type" do - assert_error "Failed to auto register service for 'foo' (Foo). Call references non-existent method 'foo'.", <<-CR + assert_compile_time_error "Failed to auto register service for 'foo' (Foo). Call references non-existent method 'foo'.", <<-CR @[ADI::Register(calls: [{"foo"}])] record Foo CR diff --git a/src/components/dependency_injection/spec/compiler_passes/resolve_parameter_placeholders_spec.cr b/src/components/dependency_injection/spec/compiler_passes/resolve_parameter_placeholders_spec.cr index 92f4f28be..5b87398c9 100644 --- a/src/components/dependency_injection/spec/compiler_passes/resolve_parameter_placeholders_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/resolve_parameter_placeholders_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -11,7 +11,7 @@ end describe ADI::ServiceContainer::ResolveParameterPlaceholders do describe "compiler errors", tags: "compiled" do it "errors if a parameter references another undefined placeholder." do - assert_error "Parameter 'parameters[app.name]' referenced unknown parameter 'app.version'.", <<-CR + assert_compile_time_error "Parameter 'parameters[app.name]' referenced unknown parameter 'app.version'.", <<-CR ADI.configure({ parameters: { "app.name": "Testing v%app.version%" @@ -21,7 +21,7 @@ describe ADI::ServiceContainer::ResolveParameterPlaceholders do end it "errors if a parameter references another undefined placeholder within a hash." do - assert_error "Parameter 'parameters[app.settings][\"thing\"]' referenced unknown parameter 'app.name'.", <<-CR + assert_compile_time_error "Parameter 'parameters[app.settings][\"thing\"]' referenced unknown parameter 'app.name'.", <<-CR ADI.configure({ parameters: { "app.settings": { @@ -33,7 +33,7 @@ describe ADI::ServiceContainer::ResolveParameterPlaceholders do end it "errors if a parameter references another undefined placeholder within an array." do - assert_error "Parameter 'parameters[app.settings][0]' referenced unknown parameter 'app.name'.", <<-CR + assert_compile_time_error "Parameter 'parameters[app.settings][0]' referenced unknown parameter 'app.name'.", <<-CR ADI.configure({ parameters: { "app.settings": ["%app.name%"] diff --git a/src/components/dependency_injection/spec/compiler_passes/resolve_values_spec.cr b/src/components/dependency_injection/spec/compiler_passes/resolve_values_spec.cr index 1e90778bf..0b24c380b 100644 --- a/src/components/dependency_injection/spec/compiler_passes/resolve_values_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/resolve_values_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -84,7 +84,7 @@ ADI.bind alias_overridden_by_global_bind : ResolveValuePriorityInterface, "@serv describe ADI::ServiceContainer::ResolveValues do describe "compiler errors", tags: "compiled" do it "errors if a service string reference doesn't map to a known service" do - assert_error "Failed to register service 'foo'. Argument 'id : Int32' references undefined service 'bar'.", <<-CR + assert_compile_time_error "Failed to register service 'foo'. Argument 'id : Int32' references undefined service 'bar'.", <<-CR @[ADI::Register(_id: "@bar")] record Foo, id : Int32 CR diff --git a/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr b/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr index e7ea53cce..43abaee0d 100644 --- a/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -11,14 +11,14 @@ end describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do describe "compiler errors" do it "errors if a expects a string value parameter but it is not of that type" do - assert_error "Parameter 'value : String' of service 'foo' (Foo) expects a String but got '123'.", <<-'CR' + assert_compile_time_error "Parameter 'value : String' of service 'foo' (Foo) expects a String but got '123'.", <<-'CR' @[ADI::Register(_value: 123)] record Foo, value : String CR end it "still errors with explicit calls even if they are not of the proper type" do - assert_error "expected argument 'value' to 'Foo.new' to be String, not Int32", <<-'CR' + assert_compile_time_error "expected argument 'value' to 'Foo.new' to be String, not Int32", <<-'CR' @[ADI::Register(_value: "123".to_i, public: true)] record Foo, value : String @@ -27,7 +27,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "errors if a parameter resolves to a service of the incorrect type" do - assert_error "Parameter 'value : Int32' of service 'foo' (Foo) expects 'Int32' but the resolved service 'bar' is of type 'Bar'.", <<-'CR' + assert_compile_time_error "Parameter 'value : Int32' of service 'foo' (Foo) expects 'Int32' but the resolved service 'bar' is of type 'Bar'.", <<-'CR' @[ADI::Register] record Bar @@ -38,7 +38,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do describe NamedTuple do it "errors if configuration is missing a non-nilable property" do - assert_error "Configuration value 'test.connection' is missing required value for 'port' of type 'Int32'.", <<-'CR' + assert_compile_time_error "Configuration value 'test.connection' is missing required value for 'port' of type 'Int32'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -60,7 +60,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "errors if there is a type mismatch" do - assert_error "Expected configuration value 'test.connection.hostname' to be a 'String', but got 'Int32'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.connection.hostname' to be a 'String', but got 'Int32'.", <<-'CR' module Schema include ADI::Extension::Schema property connection : NamedTuple(hostname: String) @@ -77,7 +77,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "errors if there is a type mismatch within an array type" do - assert_error "Expected configuration value 'test.connection.ports[1]' to be a 'Int32', but got 'String'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.connection.ports[1]' to be a 'Int32', but got 'String'.", <<-'CR' module Schema include ADI::Extension::Schema property connection : NamedTuple(ports: Array(Int32)) @@ -97,7 +97,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "errors if there is a type mismatch within a nilable array type" do - assert_error "Expected configuration value 'test.connection.ports[1]' to be a 'Int32', but got 'String'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.connection.ports[1]' to be a 'Int32', but got 'String'.", <<-'CR' module Schema include ADI::Extension::Schema property connection : NamedTuple(ports: Array(Int32)?) @@ -119,7 +119,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do describe "array_of" do it "errors on type mismatch in array within array_of object" do - assert_error "Expected configuration value 'test.rules[0].priorities[2]' to be a 'String', but got 'Int32'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.rules[0].priorities[2]' to be a 'String', but got 'Int32'.", <<-'CR' require "../spec_helper" module Schema @@ -144,7 +144,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do describe "object_of" do it "errors on type mismatch in array within object_of object" do - assert_error "Expected configuration value 'test.rule.priorities[2]' to be a 'String', but got 'Int32'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.rule.priorities[2]' to be a 'String', but got 'Int32'.", <<-'CR' require "../spec_helper" module Schema diff --git a/src/components/dependency_injection/spec/compiler_passes/validate_generics_spec.cr b/src/components/dependency_injection/spec/compiler_passes/validate_generics_spec.cr index 9b291a723..3b147307b 100644 --- a/src/components/dependency_injection/spec/compiler_passes/validate_generics_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/validate_generics_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -19,14 +19,14 @@ end describe ADI::ServiceContainer::ValidateGenerics do describe "compiler errors", tags: "compiled" do it "errors if the generic service does not provide the generic arguments." do - assert_error "Failed to register service 'foo_service'. Generic services must provide the types to use via the 'generics' field.", <<-CR + assert_compile_time_error "Failed to register service 'foo_service'. Generic services must provide the types to use via the 'generics' field.", <<-CR @[ADI::Register(name: "foo_service")] record Foo(T) CR end it "errors if there is a generic argument count mismatch." do - assert_error "Failed to register service 'foo_service'. Expected 1 generics types got 2.", <<-CR + assert_compile_time_error "Failed to register service 'foo_service'. Expected 1 generics types got 2.", <<-CR @[ADI::Register(String, Bool, name: "foo_service")] record Foo(T) CR diff --git a/src/components/dependency_injection/spec/extension_spec.cr b/src/components/dependency_injection/spec/extension_spec.cr index 6fbf8267f..94a413940 100644 --- a/src/components/dependency_injection/spec/extension_spec.cr +++ b/src/components/dependency_injection/spec/extension_spec.cr @@ -7,8 +7,8 @@ private def assert_success(code : String, *, line : Int32 = __LINE__) : Nil CR end -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "./spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -117,7 +117,7 @@ describe ADI::Extension, tags: "compiled" do end it "errors if a required configuration value has not been provided" do - assert_error "Configuration value 'test.connection' is missing required value for 'port' of type 'Int32'.", <<-'CR' + assert_compile_time_error "Configuration value 'test.connection' is missing required value for 'port' of type 'Int32'.", <<-'CR' module Schema include ADI::Extension::Schema @@ -138,7 +138,7 @@ describe ADI::Extension, tags: "compiled" do end it "errors if a configuration value has been provided a value of the wrong type" do - assert_error "Expected configuration value 'test.connection.port' to be a 'Int32', but got 'Bool'.", <<-'CR' + assert_compile_time_error "Expected configuration value 'test.connection.port' to be a 'Int32', but got 'Bool'.", <<-'CR' module Schema include ADI::Extension::Schema diff --git a/src/components/event_dispatcher/spec/compiler_spec.cr b/src/components/event_dispatcher/spec/compiler_spec.cr index ee6e34c40..e1376acf8 100644 --- a/src/components/event_dispatcher/spec/compiler_spec.cr +++ b/src/components/event_dispatcher/spec/compiler_spec.cr @@ -4,7 +4,7 @@ require "./spec_helper" describe Athena::EventDispatcher do describe "compiler errors", tags: "compiled" do it "when the listener method is static" do - ASPEC::Methods.assert_error "Event listener methods can only be defined as instance methods. Did you mean 'MyListener#listener'?", <<-CR + ASPEC::Methods.assert_compile_time_error "Event listener methods can only be defined as instance methods. Did you mean 'MyListener#listener'?", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener] @@ -16,7 +16,7 @@ describe Athena::EventDispatcher do end it "with no parameters" do - ASPEC::Methods.assert_error "Expected 'MyListener#listener' to have 1..2 parameters, got '0'.", <<-CR + ASPEC::Methods.assert_compile_time_error "Expected 'MyListener#listener' to have 1..2 parameters, got '0'.", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener] @@ -28,7 +28,7 @@ describe Athena::EventDispatcher do end it "with too many parameters" do - ASPEC::Methods.assert_error "Expected 'MyListener#listener' to have 1..2 parameters, got '3'.", <<-CR + ASPEC::Methods.assert_compile_time_error "Expected 'MyListener#listener' to have 1..2 parameters, got '3'.", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener] @@ -40,7 +40,7 @@ describe Athena::EventDispatcher do end it "first parameter unrestricted" do - ASPEC::Methods.assert_error "Expected parameter #1 of 'MyListener#listener' to have a type restriction of an 'AED::Event' instance, but it is not restricted.", <<-CR + ASPEC::Methods.assert_compile_time_error "Expected parameter #1 of 'MyListener#listener' to have a type restriction of an 'AED::Event' instance, but it is not restricted.", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener] @@ -52,7 +52,7 @@ describe Athena::EventDispatcher do end it "first parameter non AED::Event restriction" do - ASPEC::Methods.assert_error "Expected parameter #1 of 'MyListener#listener' to have a type restriction of an 'AED::Event' instance, not 'String'.", <<-CR + ASPEC::Methods.assert_compile_time_error "Expected parameter #1 of 'MyListener#listener' to have a type restriction of an 'AED::Event' instance, not 'String'.", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener] @@ -64,7 +64,7 @@ describe Athena::EventDispatcher do end it "second parameter unrestricted" do - ASPEC::Methods.assert_error "Expected parameter #2 of 'MyListener#listener' to have a type restriction of 'AED::EventDispatcherInterface', but it is not restricted.", <<-CR + ASPEC::Methods.assert_compile_time_error "Expected parameter #2 of 'MyListener#listener' to have a type restriction of 'AED::EventDispatcherInterface', but it is not restricted.", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener] @@ -76,7 +76,7 @@ describe Athena::EventDispatcher do end it "second parameter non AED::EventDispatcherInterface restriction" do - ASPEC::Methods.assert_error "Expected parameter #2 of 'MyListener#listener' to have a type restriction of 'AED::EventDispatcherInterface', not 'String'.", <<-CR + ASPEC::Methods.assert_compile_time_error "Expected parameter #2 of 'MyListener#listener' to have a type restriction of 'AED::EventDispatcherInterface', not 'String'.", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener] @@ -88,7 +88,7 @@ describe Athena::EventDispatcher do end it "non integer priority field" do - ASPEC::Methods.assert_error "Event listener method 'MyListener#listener' expects a 'NumberLiteral' for its 'AEDA::AsEventListener#priority' field, but got a 'StringLiteral'.", <<-CR + ASPEC::Methods.assert_compile_time_error "Event listener method 'MyListener#listener' expects a 'NumberLiteral' for its 'AEDA::AsEventListener#priority' field, but got a 'StringLiteral'.", <<-CR require "./spec_helper.cr" class MyListener @[AEDA::AsEventListener(priority: "foo")] diff --git a/src/components/framework/spec/bundle_spec.cr b/src/components/framework/spec/bundle_spec.cr index 6978c3e29..986229387 100644 --- a/src/components/framework/spec/bundle_spec.cr +++ b/src/components/framework/spec/bundle_spec.cr @@ -1,7 +1,7 @@ require "./spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "./spec_helper.cr" #{code} CR @@ -17,7 +17,7 @@ end describe ATH::Bundle, tags: "compiled" do describe ATH::Listeners::CORS do it "wildcard allow_headers with allow_credentials" do - assert_error "'expose_headers' cannot contain a wildcard ('*') when 'allow_credentials' is 'true'.", <<-'CODE' + assert_compile_time_error "'expose_headers' cannot contain a wildcard ('*') when 'allow_credentials' is 'true'.", <<-'CODE' ATH.configure({ framework: { cors: { @@ -33,7 +33,7 @@ describe ATH::Bundle, tags: "compiled" do end it "does not exist if not enabled" do - assert_error "undefined method 'athena_framework_listeners_cors'", <<-CODE + assert_compile_time_error "undefined method 'athena_framework_listeners_cors'", <<-CODE ADI.container.athena_framework_listeners_cors CODE end diff --git a/src/components/framework/spec/compiler_spec.cr b/src/components/framework/spec/compiler_spec.cr index 62505c118..30361602b 100644 --- a/src/components/framework/spec/compiler_spec.cr +++ b/src/components/framework/spec/compiler_spec.cr @@ -1,7 +1,7 @@ require "./spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line require "./spec_helper.cr" #{code} ATH.run @@ -19,7 +19,7 @@ end describe Athena::Framework do describe "compiler errors", tags: "compiled" do it "action parameter missing type restriction" do - assert_error "Route action parameter 'CompileController#action:id' must have a type restriction.", <<-CODE + assert_compile_time_error "Route action parameter 'CompileController#action:id' must have a type restriction.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/:id")] def action(id) : Int32 @@ -30,7 +30,7 @@ describe Athena::Framework do end it "action missing return type" do - assert_error "Route action return type must be set for 'CompileController#action'.", <<-CODE + assert_compile_time_error "Route action return type must be set for 'CompileController#action'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action @@ -41,7 +41,7 @@ describe Athena::Framework do end it "class method action" do - assert_error "Routes can only be defined as instance methods. Did you mean 'CompileController#class_method'?", <<-CODE + assert_compile_time_error "Routes can only be defined as instance methods. Did you mean 'CompileController#class_method'?", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def self.class_method : Int32 @@ -52,7 +52,7 @@ describe Athena::Framework do end it "when action does not have a path" do - assert_error "Route action 'CompileController#action' is missing its path.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' is missing its path.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get] def action : Int32 @@ -64,7 +64,7 @@ describe Athena::Framework do describe "when a controller action is mistakenly overridden" do it "within the same controller" do - assert_error "A controller action named '#action' already exists within 'CompileController'.", <<-CODE + assert_compile_time_error "A controller action named '#action' already exists within 'CompileController'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/foo")] def action : String @@ -100,7 +100,7 @@ describe Athena::Framework do describe ARTA::Route do it "when there is a prefix for a controller action with a locale that does not have a route" do - assert_error "Route action 'CompileController#action' is missing paths for locale(s) 'de'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' is missing paths for locale(s) 'de'.", <<-CODE @[ARTA::Route(path: {"de" => "/german", "fr" => "/france"})] class CompileController < ATH::Controller @[ARTA::Get(path: {"fr" => ""})] @@ -111,7 +111,7 @@ describe Athena::Framework do end it "when a controller action has a locale that is missing a prefix" do - assert_error "Route action 'CompileController#action' is missing a corresponding route prefix for the 'de' locale.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' is missing a corresponding route prefix for the 'de' locale.", <<-CODE @[ARTA::Route(path: {"fr" => "/france"})] class CompileController < ATH::Controller @[ARTA::Get(path: {"de" => "/foo", "fr" => "/bar"})] @@ -122,7 +122,7 @@ describe Athena::Framework do end it "has an unexpected type as the #methods" do - assert_error "Route action 'CompileController#action' expects a 'StringLiteral | ArrayLiteral | TupleLiteral' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | ArrayLiteral | TupleLiteral' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Route("/", methods: 123)] def action : Nil @@ -132,7 +132,7 @@ describe Athena::Framework do end it "requires ARTA::Route to use 'methods'" do - assert_error "Route action 'CompileController#action' cannot change the required methods when _NOT_ using the 'ARTA::Route' annotation.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' cannot change the required methods when _NOT_ using the 'ARTA::Route' annotation.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get("/", methods: "SEARCH")] def action : Nil; end @@ -143,7 +143,7 @@ describe Athena::Framework do describe "invalid field types" do describe "path" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Route#path' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Route#path' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(path: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -153,7 +153,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Get#path' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Get#path' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: 10)] def action : Nil; end @@ -164,7 +164,7 @@ describe Athena::Framework do describe "defaults" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(defaults: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -174,7 +174,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Get#defaults' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Get#defaults' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(defaults: 10)] def action : Nil; end @@ -185,7 +185,7 @@ describe Athena::Framework do describe "locale" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(locale: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -195,7 +195,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#locale' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#locale' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(locale: 10)] def action : Nil; end @@ -206,7 +206,7 @@ describe Athena::Framework do describe "format" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(format: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -216,7 +216,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#format' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#format' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(format: 10)] def action : Nil; end @@ -227,7 +227,7 @@ describe Athena::Framework do describe "stateless" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(stateless: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -237,7 +237,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'BoolLiteral' for its 'ARTA::Get#stateless' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'BoolLiteral' for its 'ARTA::Get#stateless' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(stateless: 10)] def action : Nil; end @@ -248,7 +248,7 @@ describe Athena::Framework do describe "name" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(name: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -258,7 +258,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#name' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#name' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/", name: 10)] def action : Nil; end @@ -269,7 +269,7 @@ describe Athena::Framework do describe "requirements" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(requirements: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -279,7 +279,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Get#requirements' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Get#requirements' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/", requirements: 10)] def action : Nil; end @@ -290,7 +290,7 @@ describe Athena::Framework do describe "schemes" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#schemes' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#schemes' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(schemes: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -302,7 +302,7 @@ describe Athena::Framework do describe "methods" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(methods: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -314,7 +314,7 @@ describe Athena::Framework do describe "host" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Route#host' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Route#host' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(host: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -324,7 +324,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Get#host' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Get#host' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/", host: 10)] def action : Nil; end @@ -335,7 +335,7 @@ describe Athena::Framework do describe "condition" do it "controller ann" do - assert_error "Route action 'CompileController' expects an 'ART::Route::Condition' for its 'ARTA::Route#condition' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects an 'ART::Route::Condition' for its 'ARTA::Route#condition' field, but got a 'NumberLiteral'.", <<-CODE @[ARTA::Route(condition: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -345,7 +345,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects an 'ART::Route::Condition' for its 'ARTA::Get#condition' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects an 'ART::Route::Condition' for its 'ARTA::Get#condition' field, but got a 'NumberLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(path: "/", condition: 10)] def action : Nil; end @@ -356,7 +356,7 @@ describe Athena::Framework do describe "priority" do it "controller ann" do - assert_error "Route action 'CompileController' expects a 'NumberLiteral' for its 'ARTA::Route#priority' field, but got a 'BoolLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'NumberLiteral' for its 'ARTA::Route#priority' field, but got a 'BoolLiteral'.", <<-CODE @[ARTA::Route(priority: true)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] @@ -366,7 +366,7 @@ describe Athena::Framework do end it "route ann" do - assert_error "Route action 'CompileController#action' expects a 'NumberLiteral' for its 'ARTA::Get#priority' field, but got a 'BoolLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'NumberLiteral' for its 'ARTA::Get#priority' field, but got a 'BoolLiteral'.", <<-CODE class CompileController < ATH::Controller @[ARTA::Get(priority: false)] def action : Nil; end @@ -379,7 +379,7 @@ describe Athena::Framework do describe ATHR::RequestBody do it "when the action parameter is not serializable" do - assert_error " The annotation '@[ATHA::MapRequestBody]' cannot be applied to 'CompileController#action:foo : Foo' since the 'Athena::Framework::Controller::ValueResolvers::RequestBody' resolver only supports parameters of type 'Athena::Serializer::Serializable | JSON::Serializable | URI::Params::Serializable'.", <<-CODE + assert_compile_time_error " The annotation '@[ATHA::MapRequestBody]' cannot be applied to 'CompileController#action:foo : Foo' since the 'Athena::Framework::Controller::ValueResolvers::RequestBody' resolver only supports parameters of type 'Athena::Serializer::Serializable | JSON::Serializable | URI::Params::Serializable'.", <<-CODE record Foo, text : String class CompileController < ATH::Controller diff --git a/src/components/framework/spec/ext/console/register_commands_spec.cr b/src/components/framework/spec/ext/console/register_commands_spec.cr index 36553d18b..e4144e0a6 100644 --- a/src/components/framework/spec/ext/console/register_commands_spec.cr +++ b/src/components/framework/spec/ext/console/register_commands_spec.cr @@ -1,7 +1,7 @@ require "../../spec_helper" -private def assert_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line, file: file +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line, file: file require "../../spec_helper.cr" #{code} @@ -69,7 +69,7 @@ end describe ATH do describe "Console", tags: "compiled" do it "errors if no name is provided" do - assert_error "Console command 'TestCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR + assert_compile_time_error "Console command 'TestCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR require "../../spec_helper.cr" @[ADI::Register] diff --git a/src/components/routing/spec/requirement/enum_spec.cr b/src/components/routing/spec/requirement/enum_spec.cr index 380366873..3cf4addbd 100644 --- a/src/components/routing/spec/requirement/enum_spec.cr +++ b/src/components/routing/spec/requirement/enum_spec.cr @@ -26,7 +26,7 @@ struct EnumRequirementTest < ASPEC::TestCase @[Tags("compiled")] def test_constructor_non_enum_type : Nil - self.assert_error "'Int32' is not an Enum type.", <<-CR + self.assert_compile_time_error "'Int32' is not an Enum type.", <<-CR require "../spec_helper" ART::Requirement::Enum(Int32).new CR diff --git a/src/components/spec/CHANGELOG.md b/src/components/spec/CHANGELOG.md index df7bd10dc..f9dc75cb6 100644 --- a/src/components/spec/CHANGELOG.md +++ b/src/components/spec/CHANGELOG.md @@ -20,7 +20,7 @@ _Administrative release, no functional changes_ ### Added -- Add support for using the `CRYSTAL` ENV var to customize binary used for `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` ([#424](https://github.com/athena-framework/athena/pull/424)) (George Dietrich) +- Add support for using the `CRYSTAL` ENV var to customize binary used for `ASPEC::Methods.assert_compile_time_error` and `ASPEC::Methods.assert_success` ([#424](https://github.com/athena-framework/athena/pull/424)) (George Dietrich) ## [0.3.7] - 2024-04-09 @@ -65,7 +65,7 @@ _Administrative release, no functional changes_ ### Added -- Add support for *codegen* for the `ASPEC.assert_error` and `ASPEC.assert_success` methods ([#219](https://github.com/athena-framework/athena/pull/219)) (George Dietrich) +- Add support for *codegen* for the `ASPEC.assert_compile_time_error` and `ASPEC.assert_success` methods ([#219](https://github.com/athena-framework/athena/pull/219)) (George Dietrich) - Add ability to skip running all examples within a test case via the `ASPEC::TestCase::Skip` annotation ([#248](https://github.com/athena-framework/athena/pull/248)) (George Dietrich) ## [0.3.0] - 2022-05-14 @@ -74,7 +74,7 @@ _First release a part of the monorepo._ ### Changed -- **Breaking:** change the `assert_error` to no longer be file based. Code should now be provided as a HEREDOC argument to the method ([#173](https://github.com/athena-framework/athena/pull/173)) (George Dietrich) +- **Breaking:** change the `assert_compile_time_error` to no longer be file based. Code should now be provided as a HEREDOC argument to the method ([#173](https://github.com/athena-framework/athena/pull/173)) (George Dietrich) - Update minimum `crystal` version to `~> 1.4.0` ([#169](https://github.com/athena-framework/athena/pull/169)) (George Dietrich) ### Added diff --git a/src/components/spec/spec/compiler_spec.cr b/src/components/spec/spec/compiler_spec.cr index dde006b48..0d2ff6155 100644 --- a/src/components/spec/spec/compiler_spec.cr +++ b/src/components/spec/spec/compiler_spec.cr @@ -1,7 +1,15 @@ require "./spec_helper" -private def assert_error(message : String, code : String, *, codegen : Bool = false, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_error message, <<-CR, line: line, codegen: codegen +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line + require "./spec_helper.cr" + #{code} + TestTestCase.run + CR +end + +private def assert_runtime_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_runtime_error message, <<-CR, line: line require "./spec_helper.cr" #{code} TestTestCase.run @@ -13,7 +21,7 @@ describe Athena::Spec do describe ASPEC::TestCase::TestWith do describe "args" do it "non tuple value" do - assert_error "Expected argument #0 of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to be a Tuple, but got 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Expected argument #0 of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to be a Tuple, but got 'NumberLiteral'.", <<-CODE struct TestTestCase < ASPEC::TestCase @[TestWith( 125 @@ -25,7 +33,7 @@ describe Athena::Spec do end it "argument count mismatch" do - assert_error "Expected argument #0 of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to contain 2 values, but got 1.", <<-CODE + assert_compile_time_error "Expected argument #0 of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to contain 2 values, but got 1.", <<-CODE struct TestTestCase < ASPEC::TestCase @[TestWith( {125} @@ -39,7 +47,7 @@ describe Athena::Spec do describe "named args" do it "non tuple value" do - assert_error " Expected the value of argument 'value' of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to be a Tuple, but got 'NumberLiteral'.", <<-CODE + assert_compile_time_error " Expected the value of argument 'value' of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to be a Tuple, but got 'NumberLiteral'.", <<-CODE struct TestTestCase < ASPEC::TestCase @[TestWith( value: 125 @@ -51,7 +59,7 @@ describe Athena::Spec do end it "argument count mismatch" do - assert_error "Expected the value of argument 'value' of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to contain 2 values, but got 1.", <<-CODE + assert_compile_time_error "Expected the value of argument 'value' of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to contain 2 values, but got 1.", <<-CODE struct TestTestCase < ASPEC::TestCase @[TestWith( value: {125} @@ -66,7 +74,7 @@ describe Athena::Spec do describe "exception during initialize" do it "reports the errors once per test case" do - assert_error "oh noes", <<-CODE, codegen: true + assert_runtime_error "oh noes", <<-CODE struct TestTestCase < ASPEC::TestCase def initialize raise "oh noes" @@ -84,7 +92,7 @@ describe Athena::Spec do end it "reports actual failing tests" do - assert_error " Expected: 2\n got: 1", <<-CODE, codegen: true + assert_runtime_error " Expected: 2\n got: 1", <<-CODE struct TestTestCase < ASPEC::TestCase def test_one 1.should eq 2 @@ -95,7 +103,7 @@ describe Athena::Spec do end it "errors if defining a non-argless initializer" do - assert_error "`ASPEC::TestCase` initializers must be argless and non-yielding.", <<-CODE + assert_compile_time_error "`ASPEC::TestCase` initializers must be argless and non-yielding.", <<-CODE struct TestTestCase < ASPEC::TestCase def initialize(id : Int32); end end @@ -103,7 +111,7 @@ describe Athena::Spec do end it "errors if defining a yielding initializer" do - assert_error "`ASPEC::TestCase` initializers must be argless and non-yielding.", <<-CODE + assert_compile_time_error "`ASPEC::TestCase` initializers must be argless and non-yielding.", <<-CODE struct TestTestCase < ASPEC::TestCase def initialize(&); end end diff --git a/src/components/spec/spec/methods_spec.cr b/src/components/spec/spec/methods_spec.cr index 562980ac9..d4cdc9c3e 100644 --- a/src/components/spec/spec/methods_spec.cr +++ b/src/components/spec/spec/methods_spec.cr @@ -1,34 +1,38 @@ require "./spec_helper" describe ASPEC::Methods do - describe ".assert_error", tags: "compiled" do + describe ".assert_compile_time_error", tags: "compiled" do it "allows customizing crystal binary via CRYSTAL env var" do + old_val = ENV["CRYSTAL"]?.presence + begin ENV["CRYSTAL"] = "/path/to/crystal" expect_raises File::NotFoundError do - assert_error "", "" + assert_compile_time_error "", "" end ensure - ENV.delete "CRYSTAL" + if old_val + ENV["CRYSTAL"] = old_val + else + ENV.delete "CRYSTAL" + end end end - describe "no codegen" do - it do - assert_error "can't instantiate abstract class Foo", <<-CR + it do + assert_compile_time_error "can't instantiate abstract class Foo", <<-CR abstract class Foo; end Foo.new CR - end end + end - describe "with codegen" do - it do - assert_error "Oh no", <<-CR, codegen: true + describe ".assert_runtime_error", tags: "compiled" do + it do + assert_runtime_error "Oh no", <<-CR raise "Oh no" CR - end end end diff --git a/src/components/spec/src/methods.cr b/src/components/spec/src/methods.cr index 07a095a5e..9a9779c4d 100644 --- a/src/components/spec/src/methods.cr +++ b/src/components/spec/src/methods.cr @@ -9,33 +9,58 @@ module Athena::Spec::Methods extend self - # Executes the provided Crystal *code* asserts it errors with the provided *message*. - # The main purpose of this method is to test compile time errors. + # Executes the provided Crystal *code* asserts it results in a compile time error with the provided *message*. # # ``` - # ASPEC::Methods.assert_error "can't instantiate abstract class Foo", <<-CR + # ASPEC::Methods.assert_compile_time_error "can't instantiate abstract class Foo", <<-CR # abstract class Foo; end # Foo.new # CR # ``` # # NOTE: When files are required within the *code*, they are relative to the file calling this method. + def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil + macro_coverage_output_dir = ENV["ATHENA_SPEC_COVERAGE_OUTPUT_DIR"]?.presence + + std_out = IO::Memory.new + std_err = IO::Memory.new + result = execute code, std_out, std_err, file, false, true + + fail std_err.to_s, line: line if result.success? + std_err.to_s.should contain(message), line: line + std_err.close + + # Ignore coverage report output if the output dir is not defined, or if there is no report. + # TODO: Maybe default this to something? + if macro_coverage_output_dir && !std_out.empty? + File.open ::Path[macro_coverage_output_dir, "macro_coverage.#{Time.utc.to_unix_ms}.codecov.json"], "w" do |file| + IO.copy std_out.rewind, file + end + end + + std_out.close + end + + # Executes the provided Crystal *code* asserts it results in a runtime error with the provided *message*. + # This can be helpful in order to test something in isolation, without affecting other test cases. # - # By default this method does not perform any codegen; meaning it only validates that the code can be successfully compiled, - # excluding any runtime exceptions. + # ``` + # ASPEC::Methods.assert_runtime_error "Oh noes!", <<-CR + # raise "Oh noes!" + # CR + # ``` # - # The *codegen* option can be used to enable codegen, thus allowing runtime logic to also be tested. - # This can be helpful in order to test something in isolation, without affecting other test cases. - def assert_error(message : String, code : String, *, codegen : Bool = false, line : Int32 = __LINE__, file : String = __FILE__) : Nil + # NOTE: When files are required within the *code*, they are relative to the file calling this method. + def assert_runtime_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil buffer = IO::Memory.new - result = execute code, buffer, file, codegen + result = execute code, buffer, buffer, file, true fail buffer.to_s, line: line if result.success? buffer.to_s.should contain(message), line: line buffer.close end - # Similar to `.assert_error`, but asserts the provided Crystal *code* successfully compiles. + # Similar to `.assert_compile_time_error`, but asserts the provided Crystal *code* successfully compiles. # # ``` # ASPEC::Methods.assert_success <<-CR @@ -52,27 +77,30 @@ module Athena::Spec::Methods # This can be helpful in order to test something in isolation, without affecting other test cases. def assert_success(code : String, *, codegen : Bool = false, line : Int32 = __LINE__, file : String = __FILE__) : Nil buffer = IO::Memory.new - result = execute code, buffer, file, codegen + result = execute code, buffer, buffer, file, codegen fail buffer.to_s, line: line unless result.success? buffer.close end - private def execute(code : String, buffer : IO, file : String, codegen : Bool) : Process::Status + private def execute(code : String, std_out : IO, std_err : IO, file : String, codegen : Bool, macro_code_coverage : Bool = false) : Process::Status input = IO::Memory.new <<-CR #{code} CR - args = [ - "run", - "--no-color", - "--stdin-filename", - "#{file}", - ] + args = [] of String + + if macro_code_coverage + args.push "tool", "macro_code_coverage" + else + args << "run" + end - args << "--no-codegen" unless codegen + args << "--no-color" + args.push "--stdin-filename", file + args << "--no-codegen" if !macro_code_coverage && !codegen - Process.run(ENV["CRYSTAL"]? || "crystal", args, input: input.rewind, output: buffer, error: buffer) + Process.run(ENV["CRYSTAL"]? || "crystal", args, input: input.rewind, output: std_out, error: std_err) end # Runs the executable at the given *path*, optionally with the provided *args*. From 339764a83f562fe262b6dab7a78823e9beecc104 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sun, 22 Jun 2025 16:00:24 -0400 Subject: [PATCH 02/10] Break `ASPEC::Methods.assert_success` into two dedicated methods --- scripts/test.sh | 4 +- .../compiler_passes/merge_configs_spec.cr | 2 +- .../merge_extension_config_spec.cr | 22 +++++------ .../normalize_definitions_spec.cr | 2 +- .../compiler_passes/process_aliases_spec.cr | 14 +++---- .../process_parameters_spec.cr | 6 +-- .../validate_arguments_spec.cr | 8 ++-- .../spec/extension_spec.cr | 24 ++++++------ src/components/framework/spec/bundle_spec.cr | 8 ++-- .../framework/spec/compiler_spec.cr | 6 +-- .../ext/console/register_commands_spec.cr | 4 +- src/components/spec/spec/methods_spec.cr | 24 ++++-------- src/components/spec/src/methods.cr | 38 ++++++++++++------- 13 files changed, 82 insertions(+), 80 deletions(-) diff --git a/scripts/test.sh b/scripts/test.sh index cb9eebd91..758b9f877 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -83,8 +83,8 @@ then fi exit $? fi -$HAS_KCOV -for component in $(find src/components/ -maxdepth 2 -type f -name shard.yml -print0 | xargs -0 -I{} dirname {} | xargs -0 -I{} basename {} | sort); do + +for component in $(find src/components/ -maxdepth 2 -type f -name shard.yml | xargs -I{} dirname {} | xargs -I{} basename {} | sort); do echo "::group::$component" if [ "$HAS_KCOV" = "true" ] diff --git a/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr b/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr index 08280d579..009a9321c 100644 --- a/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr @@ -2,7 +2,7 @@ require "../spec_helper" describe ADI::ServiceContainer::MergeConfigs do it "deep merges consecutive `ADI.configure` call", tags: "compiled" do - ASPEC::Methods.assert_success <<-CR + ASPEC::Methods.assert_compiles <<-CR require "../spec_helper" module Schema diff --git a/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr b/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr index 6f52594b2..42492d5e2 100644 --- a/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr @@ -296,7 +296,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "extension configuration value resolution" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" enum Color @@ -354,7 +354,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "does not error if nothing is configured, but all properties have defaults or are nilable" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -378,7 +378,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "inherits type of arrays from property if not explicitly set" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -408,7 +408,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "allows using NoReturn to type empty arrays in schema" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -432,7 +432,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "allows customizing values when using NoReturn to type empty arrays defaults in schema" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -462,7 +462,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "expands schema to include expected structure/defaults if not configuration is provided" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -497,7 +497,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "expands schema to include expected structure/defaults if not explicitly provided" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -550,7 +550,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "merges missing array_of defaults" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -582,7 +582,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "merges missing array_of defaults in time for other compiler passes" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -626,7 +626,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "fills in missing nilable keys with `nil`" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -658,7 +658,7 @@ describe ADI::ServiceContainer::MergeExtensionConfig, tags: "compiled" do end it "fills in missing nilable keys with `nil` when missing from default value" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema diff --git a/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr b/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr index c593e58b7..728a61d20 100644 --- a/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr @@ -41,7 +41,7 @@ describe ADI::ServiceContainer::NormalizeDefinitions, tags: "compiled" do end it "applies defaults to missing properties" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper.cr" module MySchema diff --git a/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr b/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr index 751f347a9..a983eab5f 100644 --- a/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" -private def assert_success(code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_success <<-CR, line: line +private def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compiles <<-CR, line: line require "../spec_helper.cr" #{code} ADI::ServiceContainer.new @@ -32,7 +32,7 @@ describe ADI::ServiceContainer::ProcessAliases, tags: "compiled" do end it "allows explicit string alias name" do - assert_success <<-'CR' + assert_compiles <<-'CR' @[ADI::Register] @[ADI::AsAlias("bar")] class Foo; end @@ -50,7 +50,7 @@ describe ADI::ServiceContainer::ProcessAliases, tags: "compiled" do end it "allows explicit const alias name" do - assert_success <<-'CR' + assert_compiles <<-'CR' BAR = "bar" @[ADI::Register] @@ -70,7 +70,7 @@ describe ADI::ServiceContainer::ProcessAliases, tags: "compiled" do end it "allows explicit TypeNode alias name" do - assert_success <<-'CR' + assert_compiles <<-'CR' module SomeInterface; end @[ADI::Register] @@ -92,7 +92,7 @@ describe ADI::ServiceContainer::ProcessAliases, tags: "compiled" do end it "uses included interface type as alias name if there is only 1" do - assert_success <<-'CR' + assert_compiles <<-'CR' module SomeInterface; end @[ADI::Register] @@ -114,7 +114,7 @@ describe ADI::ServiceContainer::ProcessAliases, tags: "compiled" do end it "allows aliasing more than one interface" do - assert_success <<-'CR' + assert_compiles <<-'CR' module SomeInterface; end module OtherInterface; end diff --git a/src/components/dependency_injection/spec/compiler_passes/process_parameters_spec.cr b/src/components/dependency_injection/spec/compiler_passes/process_parameters_spec.cr index 3e77ae689..3a8d6771e 100644 --- a/src/components/dependency_injection/spec/compiler_passes/process_parameters_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/process_parameters_spec.cr @@ -2,7 +2,7 @@ require "../spec_helper" describe ADI::ServiceContainer::ProcessParameters, tags: "compiled" do it "populates parameter information of registered services" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper.cr" @[ADI::Register(_id: 123)] @@ -34,7 +34,7 @@ describe ADI::ServiceContainer::ProcessParameters, tags: "compiled" do end it "does not override value of manually wired up parameters" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper.cr" class SomeService @@ -70,7 +70,7 @@ describe ADI::ServiceContainer::ProcessParameters, tags: "compiled" do end it "does not override value of manually wired up parameters with default value" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper.cr" class SomeService diff --git a/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr b/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr index 43abaee0d..3b19e67a4 100644 --- a/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr @@ -166,7 +166,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "sets missing NT keys to `nil` if the type is nilable" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -196,7 +196,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "properly checks type within array of array_of object" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -219,7 +219,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "properly checks type within array of object_of object" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -239,7 +239,7 @@ describe ADI::ServiceContainer::ValidateArguments, tags: "compiled" do end it "allows calls to `String` parameters" do - ASPEC::Methods.assert_success <<-'CR' + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" @[ADI::Register(_value: 123.to_s, public: true)] diff --git a/src/components/dependency_injection/spec/extension_spec.cr b/src/components/dependency_injection/spec/extension_spec.cr index 94a413940..651cfab6c 100644 --- a/src/components/dependency_injection/spec/extension_spec.cr +++ b/src/components/dependency_injection/spec/extension_spec.cr @@ -1,7 +1,7 @@ require "./spec_helper" -private def assert_success(code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_success <<-CR, line: line +private def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compiles <<-CR, line: line require "./spec_helper.cr" #{code} CR @@ -17,7 +17,7 @@ end describe ADI::Extension, tags: "compiled" do it "happy path" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema property id : Int32 @@ -54,7 +54,7 @@ describe ADI::Extension, tags: "compiled" do end it "allows using NoReturn array default to inherit type of the array" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema property values : Array(Int32 | String) = [] of NoReturn @@ -82,7 +82,7 @@ describe ADI::Extension, tags: "compiled" do describe "object_of / object_of?" do it "is able to resolve parameters from the object value" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema object_of connection, username : String, password : String, port : Int32 = 1234 @@ -160,7 +160,7 @@ describe ADI::Extension, tags: "compiled" do end it "object_of" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema object_of rule, id : Int32, stop : Bool = false @@ -203,7 +203,7 @@ describe ADI::Extension, tags: "compiled" do end it "object_of with assign" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema object_of rule = {id: 999}, id : Int32, stop : Bool = false @@ -238,7 +238,7 @@ describe ADI::Extension, tags: "compiled" do end it "object_of?" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema object_of? rule, id : Int32, stop : Bool = false @@ -273,7 +273,7 @@ describe ADI::Extension, tags: "compiled" do end it "object_of? with assign" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema object_of? rule = {id: 999}, id : Int32, stop : Bool = false @@ -310,7 +310,7 @@ describe ADI::Extension, tags: "compiled" do describe "array_of / array_of?" do it "array_of" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema array_of rules, id : Int32, stop : Bool = false @@ -343,7 +343,7 @@ describe ADI::Extension, tags: "compiled" do end it "array_of with assign" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema array_of rules = [{id: 10}], id : Int32, stop : Bool = false @@ -378,7 +378,7 @@ describe ADI::Extension, tags: "compiled" do end it "array_of?" do - assert_success <<-'CR' + assert_compiles <<-'CR' module Schema include ADI::Extension::Schema array_of? rules, id : Int32, stop : Bool = false diff --git a/src/components/framework/spec/bundle_spec.cr b/src/components/framework/spec/bundle_spec.cr index 986229387..7943d62d8 100644 --- a/src/components/framework/spec/bundle_spec.cr +++ b/src/components/framework/spec/bundle_spec.cr @@ -7,8 +7,8 @@ private def assert_compile_time_error(message : String, code : String, *, line : CR end -private def assert_success(code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_success <<-CR, line: line +private def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compiles <<-CR, line: line require "./spec_helper.cr" #{code} CR @@ -39,7 +39,7 @@ describe ATH::Bundle, tags: "compiled" do end it "correctly wires up the listener based on its configuration" do - assert_success <<-'CODE' + assert_compiles <<-'CODE' ATH.configure({ framework: { cors: { @@ -77,7 +77,7 @@ describe ATH::Bundle, tags: "compiled" do describe ATH::Listeners::Format do it "correctly wires up the listener based on its configuration" do - assert_success <<-'CODE' + assert_compiles <<-'CODE' ATH.configure({ framework: { format_listener: { diff --git a/src/components/framework/spec/compiler_spec.cr b/src/components/framework/spec/compiler_spec.cr index 30361602b..665ad440a 100644 --- a/src/components/framework/spec/compiler_spec.cr +++ b/src/components/framework/spec/compiler_spec.cr @@ -8,8 +8,8 @@ private def assert_compile_time_error(message : String, code : String, *, line : CR end -private def assert_success(code : String, *, line : Int32 = __LINE__) : Nil - ASPEC::Methods.assert_success <<-CR, line: line +private def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compiles <<-CR, line: line require "./spec_helper.cr" #{code} ATH.run @@ -80,7 +80,7 @@ describe Athena::Framework do end it "within a different controller" do - assert_success <<-CODE + assert_compiles <<-CODE class ExampleController < ATH::Controller @[ARTA::Get(path: "/foo")] def action : String diff --git a/src/components/framework/spec/ext/console/register_commands_spec.cr b/src/components/framework/spec/ext/console/register_commands_spec.cr index e4144e0a6..a4ff10b79 100644 --- a/src/components/framework/spec/ext/console/register_commands_spec.cr +++ b/src/components/framework/spec/ext/console/register_commands_spec.cr @@ -8,8 +8,8 @@ private def assert_compile_time_error(message : String, code : String, *, line : CR end -private def assert_success(code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil - ASPEC::Methods.assert_success <<-CR, line: line, file: file +private def assert_compiles(code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil + ASPEC::Methods.assert_compiles <<-CR, line: line, file: file require "../../spec_helper.cr" #{code} diff --git a/src/components/spec/spec/methods_spec.cr b/src/components/spec/spec/methods_spec.cr index d4cdc9c3e..b6ada3368 100644 --- a/src/components/spec/spec/methods_spec.cr +++ b/src/components/spec/spec/methods_spec.cr @@ -36,27 +36,19 @@ describe ASPEC::Methods do end end - describe ".assert_success", tags: "compiled" do - describe "no codegen" do - it do - assert_success <<-CR - pp 1 + 1 - CR - end - - it do - assert_success <<-CR + describe ".assert_compiles", tags: "compiled" do + it do + assert_compiles <<-CR raise "Oh no" CR - end end + end - describe "with codegen" do - it do - assert_success <<-CR, codegen: true - pp 1 + 1 + describe ".assert_executes", tags: "compiled" do + it do + assert_executes <<-CR + puts 1 + 1 CR - end end end diff --git a/src/components/spec/src/methods.cr b/src/components/spec/src/methods.cr index 9a9779c4d..ea4babd2e 100644 --- a/src/components/spec/src/methods.cr +++ b/src/components/spec/src/methods.cr @@ -20,11 +20,10 @@ module Athena::Spec::Methods # # NOTE: When files are required within the *code*, they are relative to the file calling this method. def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil - macro_coverage_output_dir = ENV["ATHENA_SPEC_COVERAGE_OUTPUT_DIR"]?.presence - std_out = IO::Memory.new std_err = IO::Memory.new - result = execute code, std_out, std_err, file, false, true + + result = execute code, std_out, std_err, file, codegen: false, macro_code_coverage: true fail std_err.to_s, line: line if result.success? std_err.to_s.should contain(message), line: line @@ -32,9 +31,9 @@ module Athena::Spec::Methods # Ignore coverage report output if the output dir is not defined, or if there is no report. # TODO: Maybe default this to something? - if macro_coverage_output_dir && !std_out.empty? - File.open ::Path[macro_coverage_output_dir, "macro_coverage.#{Time.utc.to_unix_ms}.codecov.json"], "w" do |file| - IO.copy std_out.rewind, file + if !std_out.empty? && (macro_coverage_output_dir = ENV["ATHENA_SPEC_COVERAGE_OUTPUT_DIR"]?.presence) + File.open ::Path[macro_coverage_output_dir, "macro_coverage.#{Time.utc.to_unix_ms}.codecov.json"], "w" do |coverage_report| + IO.copy std_out.rewind, coverage_report end end @@ -53,7 +52,7 @@ module Athena::Spec::Methods # NOTE: When files are required within the *code*, they are relative to the file calling this method. def assert_runtime_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil buffer = IO::Memory.new - result = execute code, buffer, buffer, file, true + result = execute code, buffer, buffer, file, codegen: true fail buffer.to_s, line: line if result.success? buffer.to_s.should contain(message), line: line @@ -63,21 +62,32 @@ module Athena::Spec::Methods # Similar to `.assert_compile_time_error`, but asserts the provided Crystal *code* successfully compiles. # # ``` - # ASPEC::Methods.assert_success <<-CR + # ASPEC::Methods.assert_compiles <<-CR # puts 2 + 2 # CR # ``` # # NOTE: When files are required within the *code*, they are relative to the file calling this method. + def assert_compiles(code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil + buffer = IO::Memory.new + result = execute code, buffer, buffer, file, codegen: false + + fail buffer.to_s, line: line unless result.success? + buffer.close + end + + # Similar to `.assert_runtime_error`, but asserts the provided Crystal *code* successfully executes. # - # By default this method does not perform any codegen; meaning it only validates that the code can be successfully compiled, - # excluding any runtime exceptions. + # ``` + # ASPEC::Methods.assert_executes <<-CR + # puts 2 + 2 + # CR + # ``` # - # The *codegen* option can be used to enable codegen, thus allowing runtime logic to also be tested. - # This can be helpful in order to test something in isolation, without affecting other test cases. - def assert_success(code : String, *, codegen : Bool = false, line : Int32 = __LINE__, file : String = __FILE__) : Nil + # NOTE: When files are required within the *code*, they are relative to the file calling this method. + def assert_executes(code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil buffer = IO::Memory.new - result = execute code, buffer, buffer, file, codegen + result = execute code, buffer, buffer, file, codegen: true fail buffer.to_s, line: line unless result.success? buffer.close From b2815beea196d299e18e193ba8115aa1edec4220 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sun, 22 Jun 2025 16:18:46 -0400 Subject: [PATCH 03/10] Some cleanup and polish --- .../compiler_passes/merge_configs_spec.cr | 13 +- src/components/framework/spec/bundle_spec.cr | 16 +-- .../framework/spec/compiler_spec.cr | 132 +++++++++--------- .../ext/console/register_commands_spec.cr | 8 -- src/components/spec/CHANGELOG.md | 6 +- 5 files changed, 86 insertions(+), 89 deletions(-) diff --git a/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr b/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr index 009a9321c..5b5cf4797 100644 --- a/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr @@ -2,7 +2,7 @@ require "../spec_helper" describe ADI::ServiceContainer::MergeConfigs do it "deep merges consecutive `ADI.configure` call", tags: "compiled" do - ASPEC::Methods.assert_compiles <<-CR + ASPEC::Methods.assert_compiles <<-'CR' require "../spec_helper" module Schema @@ -53,9 +53,14 @@ describe ADI::ServiceContainer::MergeConfigs do macro finished macro finished - it { \\{{ADI::CONFIG["test"]["default_locale"]}}.should eq "en" } - it { \\{{ADI::CONFIG["test"]["cors"]["defaults"]["allow_credentials"]}}.should be_true } - it { \\{{ADI::CONFIG["test"]["cors"]["defaults"]["allow_credentials"]}}.should eq ["*"] } + \{% + config = ADI::CONFIG["test"] + + raise "#{config}" unless config["default_locale"] == "en" + raise "#{config}" unless config["cors"]["defaults"]["allow_credentials"] == true + raise "#{config}" unless config["cors"]["defaults"]["allow_origin"].size == 1 + raise "#{config}" unless config["cors"]["defaults"]["allow_origin"][0] == "*" + %} end end CR diff --git a/src/components/framework/spec/bundle_spec.cr b/src/components/framework/spec/bundle_spec.cr index 7943d62d8..982b50821 100644 --- a/src/components/framework/spec/bundle_spec.cr +++ b/src/components/framework/spec/bundle_spec.cr @@ -17,7 +17,7 @@ end describe ATH::Bundle, tags: "compiled" do describe ATH::Listeners::CORS do it "wildcard allow_headers with allow_credentials" do - assert_compile_time_error "'expose_headers' cannot contain a wildcard ('*') when 'allow_credentials' is 'true'.", <<-'CODE' + assert_compile_time_error "'expose_headers' cannot contain a wildcard ('*') when 'allow_credentials' is 'true'.", <<-'CR' ATH.configure({ framework: { cors: { @@ -29,17 +29,17 @@ describe ATH::Bundle, tags: "compiled" do }, }, }) - CODE + CR end it "does not exist if not enabled" do - assert_compile_time_error "undefined method 'athena_framework_listeners_cors'", <<-CODE + assert_compile_time_error "undefined method 'athena_framework_listeners_cors'", <<-CR ADI.container.athena_framework_listeners_cors - CODE + CR end it "correctly wires up the listener based on its configuration" do - assert_compiles <<-'CODE' + assert_compiles <<-'CR' ATH.configure({ framework: { cors: { @@ -71,13 +71,13 @@ describe ATH::Bundle, tags: "compiled" do %} end end - CODE + CR end end describe ATH::Listeners::Format do it "correctly wires up the listener based on its configuration" do - assert_compiles <<-'CODE' + assert_compiles <<-'CR' ATH.configure({ framework: { format_listener: { @@ -144,7 +144,7 @@ describe ATH::Bundle, tags: "compiled" do %} end end - CODE + CR end end end diff --git a/src/components/framework/spec/compiler_spec.cr b/src/components/framework/spec/compiler_spec.cr index 665ad440a..a84c82586 100644 --- a/src/components/framework/spec/compiler_spec.cr +++ b/src/components/framework/spec/compiler_spec.cr @@ -19,52 +19,52 @@ end describe Athena::Framework do describe "compiler errors", tags: "compiled" do it "action parameter missing type restriction" do - assert_compile_time_error "Route action parameter 'CompileController#action:id' must have a type restriction.", <<-CODE + assert_compile_time_error "Route action parameter 'CompileController#action:id' must have a type restriction.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/:id")] def action(id) : Int32 123 end end - CODE + CR end it "action missing return type" do - assert_compile_time_error "Route action return type must be set for 'CompileController#action'.", <<-CODE + assert_compile_time_error "Route action return type must be set for 'CompileController#action'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action 123 end end - CODE + CR end it "class method action" do - assert_compile_time_error "Routes can only be defined as instance methods. Did you mean 'CompileController#class_method'?", <<-CODE + assert_compile_time_error "Routes can only be defined as instance methods. Did you mean 'CompileController#class_method'?", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def self.class_method : Int32 123 end end - CODE + CR end it "when action does not have a path" do - assert_compile_time_error "Route action 'CompileController#action' is missing its path.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' is missing its path.", <<-CR class CompileController < ATH::Controller @[ARTA::Get] def action : Int32 123 end end - CODE + CR end describe "when a controller action is mistakenly overridden" do it "within the same controller" do - assert_compile_time_error "A controller action named '#action' already exists within 'CompileController'.", <<-CODE + assert_compile_time_error "A controller action named '#action' already exists within 'CompileController'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/foo")] def action : String @@ -76,11 +76,11 @@ describe Athena::Framework do "bar" end end - CODE + CR end it "within a different controller" do - assert_compiles <<-CODE + assert_compiles <<-CR class ExampleController < ATH::Controller @[ARTA::Get(path: "/foo")] def action : String @@ -94,284 +94,284 @@ describe Athena::Framework do "bar" end end - CODE + CR end end describe ARTA::Route do it "when there is a prefix for a controller action with a locale that does not have a route" do - assert_compile_time_error "Route action 'CompileController#action' is missing paths for locale(s) 'de'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' is missing paths for locale(s) 'de'.", <<-CR @[ARTA::Route(path: {"de" => "/german", "fr" => "/france"})] class CompileController < ATH::Controller @[ARTA::Get(path: {"fr" => ""})] def action : Nil end end - CODE + CR end it "when a controller action has a locale that is missing a prefix" do - assert_compile_time_error "Route action 'CompileController#action' is missing a corresponding route prefix for the 'de' locale.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' is missing a corresponding route prefix for the 'de' locale.", <<-CR @[ARTA::Route(path: {"fr" => "/france"})] class CompileController < ATH::Controller @[ARTA::Get(path: {"de" => "/foo", "fr" => "/bar"})] def action : Nil end end - CODE + CR end it "has an unexpected type as the #methods" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | ArrayLiteral | TupleLiteral' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | ArrayLiteral | TupleLiteral' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Route("/", methods: 123)] def action : Nil end end - CODE + CR end it "requires ARTA::Route to use 'methods'" do - assert_compile_time_error "Route action 'CompileController#action' cannot change the required methods when _NOT_ using the 'ARTA::Route' annotation.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' cannot change the required methods when _NOT_ using the 'ARTA::Route' annotation.", <<-CR class CompileController < ATH::Controller @[ARTA::Get("/", methods: "SEARCH")] def action : Nil; end end - CODE + CR end describe "invalid field types" do describe "path" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Route#path' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Route#path' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(path: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Get#path' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Get#path' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: 10)] def action : Nil; end end - CODE + CR end end describe "defaults" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(defaults: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Get#defaults' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Get#defaults' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(defaults: 10)] def action : Nil; end end - CODE + CR end end describe "locale" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(locale: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#locale' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#locale' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(locale: 10)] def action : Nil; end end - CODE + CR end end describe "format" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(format: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#format' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#format' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(format: 10)] def action : Nil; end end - CODE + CR end end describe "stateless" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(stateless: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'BoolLiteral' for its 'ARTA::Get#stateless' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'BoolLiteral' for its 'ARTA::Get#stateless' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(stateless: 10)] def action : Nil; end end - CODE + CR end end describe "name" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(name: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#name' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#name' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/", name: 10)] def action : Nil; end end - CODE + CR end end describe "requirements" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(requirements: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Get#requirements' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Get#requirements' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/", requirements: 10)] def action : Nil; end end - CODE + CR end end describe "schemes" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#schemes' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#schemes' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(schemes: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end end describe "methods" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(methods: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end end describe "host" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Route#host' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Route#host' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(host: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Get#host' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Get#host' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/", host: 10)] def action : Nil; end end - CODE + CR end end describe "condition" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects an 'ART::Route::Condition' for its 'ARTA::Route#condition' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects an 'ART::Route::Condition' for its 'ARTA::Route#condition' field, but got a 'NumberLiteral'.", <<-CR @[ARTA::Route(condition: 10)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects an 'ART::Route::Condition' for its 'ARTA::Get#condition' field, but got a 'NumberLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects an 'ART::Route::Condition' for its 'ARTA::Get#condition' field, but got a 'NumberLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(path: "/", condition: 10)] def action : Nil; end end - CODE + CR end end describe "priority" do it "controller ann" do - assert_compile_time_error "Route action 'CompileController' expects a 'NumberLiteral' for its 'ARTA::Route#priority' field, but got a 'BoolLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController' expects a 'NumberLiteral' for its 'ARTA::Route#priority' field, but got a 'BoolLiteral'.", <<-CR @[ARTA::Route(priority: true)] class CompileController < ATH::Controller @[ARTA::Get(path: "/")] def action : Nil; end end - CODE + CR end it "route ann" do - assert_compile_time_error "Route action 'CompileController#action' expects a 'NumberLiteral' for its 'ARTA::Get#priority' field, but got a 'BoolLiteral'.", <<-CODE + assert_compile_time_error "Route action 'CompileController#action' expects a 'NumberLiteral' for its 'ARTA::Get#priority' field, but got a 'BoolLiteral'.", <<-CR class CompileController < ATH::Controller @[ARTA::Get(priority: false)] def action : Nil; end end - CODE + CR end end end @@ -379,7 +379,7 @@ describe Athena::Framework do describe ATHR::RequestBody do it "when the action parameter is not serializable" do - assert_compile_time_error " The annotation '@[ATHA::MapRequestBody]' cannot be applied to 'CompileController#action:foo : Foo' since the 'Athena::Framework::Controller::ValueResolvers::RequestBody' resolver only supports parameters of type 'Athena::Serializer::Serializable | JSON::Serializable | URI::Params::Serializable'.", <<-CODE + assert_compile_time_error " The annotation '@[ATHA::MapRequestBody]' cannot be applied to 'CompileController#action:foo : Foo' since the 'Athena::Framework::Controller::ValueResolvers::RequestBody' resolver only supports parameters of type 'Athena::Serializer::Serializable | JSON::Serializable | URI::Params::Serializable'.", <<-CR record Foo, text : String class CompileController < ATH::Controller @@ -388,7 +388,7 @@ describe Athena::Framework do foo end end - CODE + CR end end end diff --git a/src/components/framework/spec/ext/console/register_commands_spec.cr b/src/components/framework/spec/ext/console/register_commands_spec.cr index a4ff10b79..6a9750d42 100644 --- a/src/components/framework/spec/ext/console/register_commands_spec.cr +++ b/src/components/framework/spec/ext/console/register_commands_spec.cr @@ -8,14 +8,6 @@ private def assert_compile_time_error(message : String, code : String, *, line : CR end -private def assert_compiles(code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil - ASPEC::Methods.assert_compiles <<-CR, line: line, file: file - require "../../spec_helper.cr" - - #{code} - CR -end - @[ADI::Register] class EagerlyInitializedCommand < ACON::Command class_getter initialized = false diff --git a/src/components/spec/CHANGELOG.md b/src/components/spec/CHANGELOG.md index f9dc75cb6..df7bd10dc 100644 --- a/src/components/spec/CHANGELOG.md +++ b/src/components/spec/CHANGELOG.md @@ -20,7 +20,7 @@ _Administrative release, no functional changes_ ### Added -- Add support for using the `CRYSTAL` ENV var to customize binary used for `ASPEC::Methods.assert_compile_time_error` and `ASPEC::Methods.assert_success` ([#424](https://github.com/athena-framework/athena/pull/424)) (George Dietrich) +- Add support for using the `CRYSTAL` ENV var to customize binary used for `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` ([#424](https://github.com/athena-framework/athena/pull/424)) (George Dietrich) ## [0.3.7] - 2024-04-09 @@ -65,7 +65,7 @@ _Administrative release, no functional changes_ ### Added -- Add support for *codegen* for the `ASPEC.assert_compile_time_error` and `ASPEC.assert_success` methods ([#219](https://github.com/athena-framework/athena/pull/219)) (George Dietrich) +- Add support for *codegen* for the `ASPEC.assert_error` and `ASPEC.assert_success` methods ([#219](https://github.com/athena-framework/athena/pull/219)) (George Dietrich) - Add ability to skip running all examples within a test case via the `ASPEC::TestCase::Skip` annotation ([#248](https://github.com/athena-framework/athena/pull/248)) (George Dietrich) ## [0.3.0] - 2022-05-14 @@ -74,7 +74,7 @@ _First release a part of the monorepo._ ### Changed -- **Breaking:** change the `assert_compile_time_error` to no longer be file based. Code should now be provided as a HEREDOC argument to the method ([#173](https://github.com/athena-framework/athena/pull/173)) (George Dietrich) +- **Breaking:** change the `assert_error` to no longer be file based. Code should now be provided as a HEREDOC argument to the method ([#173](https://github.com/athena-framework/athena/pull/173)) (George Dietrich) - Update minimum `crystal` version to `~> 1.4.0` ([#169](https://github.com/athena-framework/athena/pull/169)) (George Dietrich) ### Added From 6af392770943b121b4b1bab558eaba2a5adbed96 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sun, 22 Jun 2025 19:55:41 -0400 Subject: [PATCH 04/10] Bump shard Doc updates --- shard.yml | 2 +- src/components/spec/CHANGELOG.md | 11 +++++++++++ src/components/spec/UPGRADING.md | 11 +++++++++++ src/components/spec/docs/README.md | 2 +- src/components/spec/shard.yml | 4 ++-- src/components/spec/src/athena-spec.cr | 2 +- src/components/spec/src/methods.cr | 6 +++--- 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/shard.yml b/shard.yml index e41012bab..dd6a77d76 100644 --- a/shard.yml +++ b/shard.yml @@ -51,4 +51,4 @@ development_dependencies: version: ~> 1.6.3 athena-spec: github: athena-framework/spec - version: ~> 0.3.2 + version: ~> 0.4.0 diff --git a/src/components/spec/CHANGELOG.md b/src/components/spec/CHANGELOG.md index df7bd10dc..50840c18c 100644 --- a/src/components/spec/CHANGELOG.md +++ b/src/components/spec/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.4.0] - 2025-??-?? + +### Added + +- Add ability to generate macro code coverage reports for `ASPEC::Methods.assert_compile_time_error` usages ([#551](https://github.com/athena-framework/athena/pull/551)) (George Dietrich) + +### Removed + +- **Breaking:** Remove `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` methods ([#551](https://github.com/athena-framework/athena/pull/551)) (George Dietrich) + ## [0.3.11] - 2025-05-19 ### Fixed @@ -130,6 +140,7 @@ _First release a part of the monorepo._ _Initial release._ +[0.4.0]: https://github.com/athena-framework/spec/releases/tag/v0.4.0 [0.3.11]: https://github.com/athena-framework/spec/releases/tag/v0.3.11 [0.3.10]: https://github.com/athena-framework/spec/releases/tag/v0.3.10 [0.3.9]: https://github.com/athena-framework/spec/releases/tag/v0.3.9 diff --git a/src/components/spec/UPGRADING.md b/src/components/spec/UPGRADING.md index 9de1ea178..0f29323dd 100644 --- a/src/components/spec/UPGRADING.md +++ b/src/components/spec/UPGRADING.md @@ -2,6 +2,17 @@ Documents the changes that may be required when upgrading to a newer component version. +## Upgrade to 0.4.0 + +### Replace `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` + +The `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` methods have been removed in favor new methods that more clearly show intent: + +* If using `.assert_error` _without_ the `codegen` argument (the default), use `.assert_compile_time_error` instead +* If using `.assert_error` _with_ `codegen: true` argument, use `.assert_runtime_error` instead +* If using `.assert_success` _without_ the `codegen` argument (the default), use `.assert_compiles` instead +* If using `.assert_success` _with_ `codegen: true` argument, use `.assert_executes` instead + ## Upgrade to 0.3.10 ### `ASPEC::TestCase#initialize` must be argless diff --git a/src/components/spec/docs/README.md b/src/components/spec/docs/README.md index 351f7d348..7ed031501 100644 --- a/src/components/spec/docs/README.md +++ b/src/components/spec/docs/README.md @@ -10,7 +10,7 @@ First, install the component by adding the following to your `shard.yml`, then r dependencies: athena-spec: github: athena-framework/spec - version: ~> 0.3.0 + version: ~> 0.4.0 ``` ## Usage diff --git a/src/components/spec/shard.yml b/src/components/spec/shard.yml index 81a5b9cd2..26f9510d6 100644 --- a/src/components/spec/shard.yml +++ b/src/components/spec/shard.yml @@ -1,8 +1,8 @@ name: athena-spec -version: 0.3.11 +version: 0.4.0 -crystal: ~> 1.4 +crystal: ~> 1.17 license: MIT diff --git a/src/components/spec/src/athena-spec.cr b/src/components/spec/src/athena-spec.cr index de41dae89..17221a1e2 100644 --- a/src/components/spec/src/athena-spec.cr +++ b/src/components/spec/src/athena-spec.cr @@ -6,7 +6,7 @@ require "./test_case" # A set of common [Spec](https://crystal-lang.org/api/Spec.html) compliant testing utilities/types. module Athena::Spec - VERSION = "0.3.11" + VERSION = "0.4.0" # Runs all `ASPEC::TestCase`s. # diff --git a/src/components/spec/src/methods.cr b/src/components/spec/src/methods.cr index ea4babd2e..7cf6c5c45 100644 --- a/src/components/spec/src/methods.cr +++ b/src/components/spec/src/methods.cr @@ -9,7 +9,7 @@ module Athena::Spec::Methods extend self - # Executes the provided Crystal *code* asserts it results in a compile time error with the provided *message*. + # Executes the provided Crystal *code* and asserts it results in a compile time error with the provided *message*. # # ``` # ASPEC::Methods.assert_compile_time_error "can't instantiate abstract class Foo", <<-CR @@ -40,7 +40,7 @@ module Athena::Spec::Methods std_out.close end - # Executes the provided Crystal *code* asserts it results in a runtime error with the provided *message*. + # Executes the provided Crystal *code* and asserts it results in a runtime error with the provided *message*. # This can be helpful in order to test something in isolation, without affecting other test cases. # # ``` @@ -63,7 +63,7 @@ module Athena::Spec::Methods # # ``` # ASPEC::Methods.assert_compiles <<-CR - # puts 2 + 2 + # raise "Still passes" # CR # ``` # From 163d1107d2eb137bd9476b61e86b891e1dc047d8 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Mon, 23 Jun 2025 08:23:41 -0400 Subject: [PATCH 05/10] Use spec file + line in report output file name instead of timestamp --- src/components/spec/src/methods.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/spec/src/methods.cr b/src/components/spec/src/methods.cr index 7cf6c5c45..df0752858 100644 --- a/src/components/spec/src/methods.cr +++ b/src/components/spec/src/methods.cr @@ -32,7 +32,7 @@ module Athena::Spec::Methods # Ignore coverage report output if the output dir is not defined, or if there is no report. # TODO: Maybe default this to something? if !std_out.empty? && (macro_coverage_output_dir = ENV["ATHENA_SPEC_COVERAGE_OUTPUT_DIR"]?.presence) - File.open ::Path[macro_coverage_output_dir, "macro_coverage.#{Time.utc.to_unix_ms}.codecov.json"], "w" do |coverage_report| + File.open ::Path[macro_coverage_output_dir, "macro_coverage.#{Path[file].stem}:#{line}.codecov.json"], "w" do |coverage_report| IO.copy std_out.rewind, coverage_report end end From b13210796454b2f41ad440ae6d8c3ca855d4d9e0 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Thu, 26 Jun 2025 22:14:05 -0400 Subject: [PATCH 06/10] Leverage var for picking when channel to run coverage reporting on --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27a9185c1..3e6cccb94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: - uses: actions/checkout@v4 - uses: extractions/setup-just@v3 - name: Install kcov - if: matrix.crystal == 'latest' + if: ${{ matrix.crystal == vars.COVERAGE_CHANNEL }} run: | sudo apt-get update && sudo apt-get install binutils-dev libssl-dev libcurl4-openssl-dev libelf-dev libstdc++-12-dev zlib1g-dev libdw-dev libiberty-dev @@ -91,7 +91,7 @@ jobs: run: just test-compiled shell: bash - uses: codecov/codecov-action@v5 - if: matrix.crystal == 'latest' && github.event_name != 'schedule' # Only want to upload coverage report once in the matrix + if: ${{ matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -100,7 +100,7 @@ jobs: flags: compiled verbose: true - uses: codecov/test-results-action@v1 - if: matrix.crystal == 'latest' && github.event_name != 'schedule' # Only want to upload coverage report once in the matrix + if: ${{ matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -129,7 +129,7 @@ jobs: fetch-depth: 0 - uses: extractions/setup-just@v3 - name: Install kcov - if: matrix.os == 'ubuntu-latest' && matrix.crystal == 'latest' + if: ${{ matrix.os == 'ubuntu-latest' && matrix.crystal == vars.COVERAGE_CHANNEL }} run: | sudo apt-get update && sudo apt-get install binutils-dev libssl-dev libcurl4-openssl-dev libelf-dev libstdc++-12-dev zlib1g-dev libdw-dev libiberty-dev @@ -160,7 +160,7 @@ jobs: run: just test-unit shell: bash - uses: codecov/codecov-action@v5 - if: matrix.os == 'ubuntu-latest' && matrix.crystal == 'latest' && github.event_name != 'schedule' # Only want to upload coverage report once in the matrix + if: ${{ matrix.os == 'ubuntu-latest' && matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -169,7 +169,7 @@ jobs: flags: unit verbose: true - uses: codecov/test-results-action@v1 - if: matrix.os == 'ubuntu-latest' && matrix.crystal == 'latest' && github.event_name != 'schedule' # Only want to upload coverage report once in the matrix + if: ${{ matrix.os == 'ubuntu-latest' && matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true From 7c191ce42d1a026b8279262e0256e48b5310789e Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Fri, 27 Jun 2025 00:20:23 -0400 Subject: [PATCH 07/10] Avoid suffix conditionals with macro raise --- .../console/src/helper/helper_set.cr | 6 ++- src/components/console/src/question/base.cr | 6 ++- .../src/athena-dependency_injection.cr | 4 +- .../process_autoconfigure_annotations.cr | 4 +- .../src/compiler_passes/register_services.cr | 13 +++-- .../src/compiler_passes/validate_arguments.cr | 4 +- .../event_dispatcher/src/event_dispatcher.cr | 24 +++++++-- .../framework/src/abstract_bundle.cr | 8 ++- .../ext/routing/annotation_route_loader.cr | 50 +++++++++++++++---- .../routing/src/requirement/enum.cr | 6 ++- 10 files changed, 99 insertions(+), 26 deletions(-) diff --git a/src/components/console/src/helper/helper_set.cr b/src/components/console/src/helper/helper_set.cr index 1d41de803..b3d37c929 100644 --- a/src/components/console/src/helper/helper_set.cr +++ b/src/components/console/src/helper/helper_set.cr @@ -31,7 +31,11 @@ class Athena::Console::Helper::HelperSet # Returns the helper of the provided *helper_class*, or `nil` if it is not defined. def []?(helper_class : T.class) : T? forall T - {% T.raise "Helper class type '#{T}' is not an 'ACON::Helper::Interface'." unless T <= ACON::Helper::Interface %} + {% + unless T <= ACON::Helper::Interface + T.raise "Helper class type '#{T}' is not an 'ACON::Helper::Interface'." + end + %} @helpers[helper_class]?.as? T end diff --git a/src/components/console/src/question/base.cr b/src/components/console/src/question/base.cr index a1f98643b..aebfde469 100644 --- a/src/components/console/src/question/base.cr +++ b/src/components/console/src/question/base.cr @@ -34,7 +34,11 @@ module Athena::Console::Question::Base(T) property? trimmable : Bool = true def initialize(@question : String, @default : T) - {% T.raise "An ACON::Question generic argument cannot be 'Nil'. Use 'String?' instead." if T == Nil %} + {% + if T == Nil + T.raise "An ACON::Question generic argument cannot be 'Nil'. Use 'String?' instead." + end + %} end # :nodoc: diff --git a/src/components/dependency_injection/src/athena-dependency_injection.cr b/src/components/dependency_injection/src/athena-dependency_injection.cr index 96f505847..0a4072106 100644 --- a/src/components/dependency_injection/src/athena-dependency_injection.cr +++ b/src/components/dependency_injection/src/athena-dependency_injection.cr @@ -117,7 +117,9 @@ module Athena::DependencyInjection {% pass_type = pass.resolve - pass.raise "Pass type must be a module." unless pass_type.module? + unless pass_type.module? + pass.raise "Pass type must be a module." + end type = type || :before_optimization priority = priority || 0 diff --git a/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr b/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr index e3e9a26e9..87c7fbd2a 100644 --- a/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr +++ b/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr @@ -110,7 +110,9 @@ module Athena::DependencyInjection::ServiceContainer::ProcessAutoconfigureAnnota elsif tag.is_a?(Path) {tag.resolve.id.stringify, {} of Nil => Nil} elsif tag.is_a?(NamedTupleLiteral) || tag.is_a?(HashLiteral) - tag.raise "Failed to auto register service '#{service_id.id}'. All tags must have a name." unless tag[:name] + unless tag[:name] + tag.raise "Failed to auto register service '#{service_id.id}'. All tags must have a name." + end # Resolve a constant to its value if used as a tag name if tag["name"].is_a? Path diff --git a/src/components/dependency_injection/src/compiler_passes/register_services.cr b/src/components/dependency_injection/src/compiler_passes/register_services.cr index aa311360d..78121e41a 100644 --- a/src/components/dependency_injection/src/compiler_passes/register_services.cr +++ b/src/components/dependency_injection/src/compiler_passes/register_services.cr @@ -40,8 +40,13 @@ module Athena::DependencyInjection::ServiceContainer::RegisterServices if factory factory_class, factory_method = factory - raise "Failed to auto register service '#{service_id.id}'. Factory method '#{factory_method.id}' within '#{factory_class}' is an instance method." if factory_class.instance.has_method? factory_method - raise "Failed to auto register service '#{service_id.id}'. Factory method '#{factory_method.id}' within '#{factory_class}' does not exist." unless factory_class.class.has_method? factory_method + if factory_class.instance.has_method? factory_method + raise "Failed to auto register service '#{service_id.id}'. Factory method '#{factory_method.id}' within '#{factory_class}' is an instance method." + end + + unless factory_class.class.has_method? factory_method + raise "Failed to auto register service '#{service_id.id}'. Factory method '#{factory_method.id}' within '#{factory_class}' does not exist." + end end %} @@ -60,7 +65,9 @@ module Athena::DependencyInjection::ServiceContainer::RegisterServices elsif tag.is_a?(Path) {tag.resolve.id.stringify, {} of Nil => Nil} elsif tag.is_a?(NamedTupleLiteral) || tag.is_a?(HashLiteral) - tag.raise "Failed to auto register service '#{service_id.id}'. All tags must have a name." unless tag[:name] + unless tag[:name] + tag.raise "Failed to auto register service '#{service_id.id}'. All tags must have a name." + end # Resolve a constant to its value if used as a tag name if tag["name"].is_a? Path diff --git a/src/components/dependency_injection/src/compiler_passes/validate_arguments.cr b/src/components/dependency_injection/src/compiler_passes/validate_arguments.cr index 4fd2c0ce7..f8176cd47 100644 --- a/src/components/dependency_injection/src/compiler_passes/validate_arguments.cr +++ b/src/components/dependency_injection/src/compiler_passes/validate_arguments.cr @@ -26,7 +26,9 @@ module Athena::DependencyInjection::ServiceContainer::ValidateArguments error = "Failed to resolve value for parameter '#{param["declaration"]}' of service '#{service_id.id}' (#{definition["class"]})." end - param["declaration"].raise error if error + if error + param["declaration"].raise error + end end end %} diff --git a/src/components/event_dispatcher/src/event_dispatcher.cr b/src/components/event_dispatcher/src/event_dispatcher.cr index 3e071d131..1afa59c44 100644 --- a/src/components/event_dispatcher/src/event_dispatcher.cr +++ b/src/components/event_dispatcher/src/event_dispatcher.cr @@ -40,7 +40,11 @@ class Athena::EventDispatcher::EventDispatcher # :inherit: def listener(event_class : E.class, *, priority : Int32 = 0, name : String? = nil, &block : E, AED::EventDispatcherInterface -> Nil) : AED::Callable forall E - {% @def.args[0].raise "expected argument #1 to '#{@def.name}' to be #{AED::Event.class}, not #{E}." unless E <= AED::Event %} + {% + unless E <= AED::Event + @def.args[0].raise "expected argument #1 to '#{@def.name}' to be #{AED::Event.class}, not #{E}." + end + %} self.add_callable AED::Callable::EventDispatcher(E).new block, priority, name end @@ -67,12 +71,22 @@ class Athena::EventDispatcher::EventDispatcher event_arg = m.args[0] # Validate the type restriction of the first parameter, if present - event_arg.raise "Expected parameter #1 of '#{T.name}##{m.name}' to have a type restriction of an 'AED::Event' instance, but it is not restricted." if event_arg.restriction.is_a?(Nop) - event_arg.raise "Expected parameter #1 of '#{T.name}##{m.name}' to have a type restriction of an 'AED::Event' instance, not '#{event_arg.restriction}'." if !(event_arg.restriction.resolve <= AED::Event) + if event_arg.restriction.is_a?(Nop) + event_arg.raise "Expected parameter #1 of '#{T.name}##{m.name}' to have a type restriction of an 'AED::Event' instance, but it is not restricted." + end + + if !(event_arg.restriction.resolve <= AED::Event) + event_arg.raise "Expected parameter #1 of '#{T.name}##{m.name}' to have a type restriction of an 'AED::Event' instance, not '#{event_arg.restriction}'." + end if dispatcher_arg = m.args[1] - event_arg.raise "Expected parameter #2 of '#{T.name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', but it is not restricted." if dispatcher_arg.restriction.is_a?(Nop) - event_arg.raise "Expected parameter #2 of '#{T.name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', not '#{dispatcher_arg.restriction}'." if !(dispatcher_arg.restriction.resolve <= AED::EventDispatcherInterface) + if dispatcher_arg.restriction.is_a?(Nop) + event_arg.raise "Expected parameter #2 of '#{T.name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', but it is not restricted." + end + + if !(dispatcher_arg.restriction.resolve <= AED::EventDispatcherInterface) + event_arg.raise "Expected parameter #2 of '#{T.name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', not '#{dispatcher_arg.restriction}'." + end end priority = m.annotation(AEDA::AsEventListener)[:priority] || 0 diff --git a/src/components/framework/src/abstract_bundle.cr b/src/components/framework/src/abstract_bundle.cr index 6ab27eade..606fad372 100644 --- a/src/components/framework/src/abstract_bundle.cr +++ b/src/components/framework/src/abstract_bundle.cr @@ -11,11 +11,15 @@ module Athena::Framework {% resolved_bundle = bundle.resolve - bundle.raise "Must be a child of 'ATH::AbstractBundle'." unless resolved_bundle <= AbstractBundle + unless resolved_bundle <= AbstractBundle + bundle.raise "Must be a child of 'ATH::AbstractBundle'." + end ann = resolved_bundle.annotation Athena::Framework::Annotations::Bundle - bundle.raise "Unable to determine extension name." unless name = ann[0] || ann["name"] + unless name = ann[0] || ann["name"] + bundle.raise "Unable to determine extension name." + end %} ADI.register_extension {{name}}, {{"#{bundle.resolve.id}::Schema".id}} diff --git a/src/components/framework/src/ext/routing/annotation_route_loader.cr b/src/components/framework/src/ext/routing/annotation_route_loader.cr index f06f5462f..50d0a0e8c 100644 --- a/src/components/framework/src/ext/routing/annotation_route_loader.cr +++ b/src/components/framework/src/ext/routing/annotation_route_loader.cr @@ -52,34 +52,52 @@ module Athena::Framework::Routing::AnnotationRouteLoader end if (value = controller_ann[:defaults]) != nil - value.raise "Route action '#{klass.name}' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a '#{value.class_name.id}'." unless value.is_a? HashLiteral + unless value.is_a? HashLiteral + value.raise "Route action '#{klass.name}' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a '#{value.class_name.id}'." + end + globals[:defaults] = value end if (value = controller_ann[:locale]) != nil - value.raise "Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a '#{value.class_name.id}'." unless value.is_a? StringLiteral + unless value.is_a? StringLiteral + value.raise "Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a '#{value.class_name.id}'." + end + globals[:defaults]["_locale"] = value end if (value = controller_ann[:format]) != nil - value.raise "Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a '#{value.class_name.id}'." unless value.is_a? StringLiteral + unless value.is_a? StringLiteral + value.raise "Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a '#{value.class_name.id}'." + end + globals[:defaults]["_format"] = value end if controller_ann[:stateless] != nil value = controller_ann[:stateless] - value.raise "Route action '#{klass.name}' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a '#{value.class_name.id}'." unless value.is_a? BoolLiteral + unless value.is_a? BoolLiteral + value.raise "Route action '#{klass.name}' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a '#{value.class_name.id}'." + end + globals[:defaults]["_stateless"] = value end if (value = controller_ann[:name]) != nil - value.raise "Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a '#{value.class_name.id}'." unless value.is_a? StringLiteral + unless value.is_a? StringLiteral + value.raise "Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a '#{value.class_name.id}'." + end + globals[:name] = value end if (value = controller_ann[:requirements]) != nil - value.raise "Route action '#{klass.name}' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a '#{value.class_name.id}'." unless value.is_a? HashLiteral + unless value.is_a? HashLiteral + value.raise "Route action '#{klass.name}' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a '#{value.class_name.id}'." + end + globals[:requirements] = value end @@ -217,7 +235,10 @@ module Athena::Framework::Routing::AnnotationRouteLoader parameter_annotation_configurations[ann_class.resolve] = "(#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase)".id unless annotations.empty? end - arg.raise "Route action parameter '#{klass.name}##{m.name}:#{arg.name}' must have a type restriction." if arg.restriction.is_a? Nop + if arg.restriction.is_a? Nop + arg.raise "Route action parameter '#{klass.name}##{m.name}:#{arg.name}' must have a type restriction." + end + parameters << %(ATH::Controller::ParameterMetadata(#{arg.restriction}).new( #{arg.name.stringify}, #{!arg.default_value.is_a? Nop}, @@ -298,19 +319,28 @@ module Athena::Framework::Routing::AnnotationRouteLoader globals[:requirements].each { |k, v| requirements[k] = v } if (value = route_def[:locale]) != nil - value.raise "Route action '#{klass.name}##{m.name}' expects a 'StringLiteral' for its '#{route_def.name}#locale' field, but got a '#{value.class_name.id}'." unless value.is_a? StringLiteral + unless value.is_a? StringLiteral + value.raise "Route action '#{klass.name}##{m.name}' expects a 'StringLiteral' for its '#{route_def.name}#locale' field, but got a '#{value.class_name.id}'." + end + defaults["_locale"] = value end if (value = route_def[:format]) != nil - value.raise "Route action '#{klass.name}##{m.name}' expects a 'StringLiteral' for its '#{route_def.name}#format' field, but got a '#{value.class_name.id}'." unless value.is_a? StringLiteral + unless value.is_a? StringLiteral + value.raise "Route action '#{klass.name}##{m.name}' expects a 'StringLiteral' for its '#{route_def.name}#format' field, but got a '#{value.class_name.id}'." + end + defaults["_format"] = value end if route_def[:stateless] != nil value = route_def[:stateless] - value.raise "Route action '#{klass.name}##{m.name}' expects a 'BoolLiteral' for its '#{route_def.name}#stateless' field, but got a '#{value.class_name.id}'." unless value.is_a? BoolLiteral + unless value.is_a? BoolLiteral + value.raise "Route action '#{klass.name}##{m.name}' expects a 'BoolLiteral' for its '#{route_def.name}#stateless' field, but got a '#{value.class_name.id}'." + end + defaults["_stateless"] = value end if ann_defaults = route_def[:defaults] diff --git a/src/components/routing/src/requirement/enum.cr b/src/components/routing/src/requirement/enum.cr index eb384730b..003b1cf33 100644 --- a/src/components/routing/src/requirement/enum.cr +++ b/src/components/routing/src/requirement/enum.cr @@ -50,7 +50,11 @@ struct Athena::Routing::Requirement::Enum(EnumType) end def initialize(@members : Set(EnumType)? = nil) - {% raise "'#{EnumType}' is not an Enum type." unless EnumType <= ::Enum %} + {% + unless EnumType <= ::Enum + raise "'#{EnumType}' is not an Enum type." + end + %} end # :nodoc: From 187caba9fef6672eb632ba77e7020fe9c975d2c2 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Mon, 30 Jun 2025 22:27:45 -0400 Subject: [PATCH 08/10] Add coverage for missing lines --- .../console/spec/helper/helper_set_spec.cr | 13 ++++++++ .../console/spec/question/question_spec.cr | 9 ++++++ .../spec/athena-dependency_injection_spec.cr | 10 +++++++ .../process_auto_configurations_spec.cr | 12 ++++++++ .../process_autoconfigure_annotations.cr | 5 ++-- .../spec/event_dispatcher_spec.cr | 10 +++++++ .../framework/spec/abstract_bundle_spec.cr | 30 +++++++++++++++++++ .../framework/src/abstract_bundle.cr | 4 +-- 8 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 src/components/console/spec/helper/helper_set_spec.cr create mode 100644 src/components/framework/spec/abstract_bundle_spec.cr diff --git a/src/components/console/spec/helper/helper_set_spec.cr b/src/components/console/spec/helper/helper_set_spec.cr new file mode 100644 index 000000000..54baec9ce --- /dev/null +++ b/src/components/console/spec/helper/helper_set_spec.cr @@ -0,0 +1,13 @@ +require "../spec_helper" + +describe ACON::Helper::HelperSet do + describe "compiler errors", tags: "compiled" do + it "when the provided helper type is not an `ACON::Helper::Interface`" do + ASPEC::Methods.assert_compile_time_error "Helper class type 'String' is not an 'ACON::Helper::Interface'.", <<-CR + require "../spec_helper.cr" + + ACON::Helper::HelperSet.new[String]? + CR + end + end +end diff --git a/src/components/console/spec/question/question_spec.cr b/src/components/console/spec/question/question_spec.cr index a2f333479..3fd5b9e5d 100644 --- a/src/components/console/spec/question/question_spec.cr +++ b/src/components/console/spec/question/question_spec.cr @@ -22,6 +22,15 @@ struct QuestionTest < ASPEC::TestCase @question = ACON::Question(String?).new "Test Question", nil end + @[Tags("compiled")] + def test_nil_generic_arg : Nil + ASPEC::Methods.assert_compile_time_error "An ACON::Question generic argument cannot be 'Nil'. Use 'String?' instead.", <<-CR + require "../spec_helper.cr" + + ACON::Question(Nil).new "Nil Question", nil + CR + end + def test_default : Nil @question.default.should be_nil default = ACON::Question(String).new("Test Question", "FOO").default diff --git a/src/components/dependency_injection/spec/athena-dependency_injection_spec.cr b/src/components/dependency_injection/spec/athena-dependency_injection_spec.cr index 621a018e5..1d32ee616 100644 --- a/src/components/dependency_injection/spec/athena-dependency_injection_spec.cr +++ b/src/components/dependency_injection/spec/athena-dependency_injection_spec.cr @@ -6,6 +6,16 @@ class ValueStore end describe Athena::DependencyInjection do + describe "compiler errors", tags: "compiled" do + it "errors when passing a non-module to add_compiler_pass" do + ASPEC::Methods.assert_compile_time_error "Pass type must be a module.", <<-CR + require "./spec_helper.cr" + + ADI.add_compiler_pass String + CR + end + end + describe ".container" do it "returns a container" do ADI.container.should be_a ADI::ServiceContainer diff --git a/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr b/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr index 15f04e0b6..d76f8e90b 100644 --- a/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr +++ b/src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr @@ -244,6 +244,18 @@ describe ADI::ServiceContainer::ProcessAutoconfigureAnnotations do CR end + it "errors if not all tags have a name" do + assert_compile_time_error "Failed to auto register service 'foo' (Foo). All tags must have a name.", <<-CR + @[ADI::Autoconfigure(tags: [{ name: "A" }, { priority: 123 }])] + module Test; end + + @[ADI::Register] + record Foo do + include Test + end + CR + end + describe ADI::TaggedIterator do it "errors if used with unsupported collection type" do assert_compile_time_error "Failed to register service 'fqn_tagged_iterator_named_client' (FQNTaggedIteratorNamedClient). Collection parameter '@[ADI::TaggedIterator] services : Set(String)' type must be one of `Indexable`, `Iterator`, or `Enumerable`. Got 'Set'.", <<-CR diff --git a/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr b/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr index 87c7fbd2a..3ef78f1cb 100644 --- a/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr +++ b/src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr @@ -31,6 +31,8 @@ module Athena::DependencyInjection::ServiceContainer::ProcessAutoconfigureAnnota types_to_process = types_to_process.uniq SERVICE_HASH.each do |service_id, definition| + klass = definition["class"] + types_to_process.each do |t| if definition["class"] <= t tags = [] of Nil @@ -77,7 +79,6 @@ module Athena::DependencyInjection::ServiceContainer::ProcessAutoconfigureAnnota v.each do |call| method = call[0] args = call[1] || nil - klass = definition["class"] if method.empty? method.raise "Method name cannot be empty." @@ -111,7 +112,7 @@ module Athena::DependencyInjection::ServiceContainer::ProcessAutoconfigureAnnota {tag.resolve.id.stringify, {} of Nil => Nil} elsif tag.is_a?(NamedTupleLiteral) || tag.is_a?(HashLiteral) unless tag[:name] - tag.raise "Failed to auto register service '#{service_id.id}'. All tags must have a name." + tag.raise "Failed to auto register service '#{service_id.id}' (#{klass}). All tags must have a name." end # Resolve a constant to its value if used as a tag name diff --git a/src/components/event_dispatcher/spec/event_dispatcher_spec.cr b/src/components/event_dispatcher/spec/event_dispatcher_spec.cr index 63286eac4..9efaeeb55 100644 --- a/src/components/event_dispatcher/spec/event_dispatcher_spec.cr +++ b/src/components/event_dispatcher/spec/event_dispatcher_spec.cr @@ -36,6 +36,16 @@ struct EventDispatcherTest < ASPEC::TestCase @dispatcher = AED::EventDispatcher.new end + @[Tags("compiled")] + def test_listener_not_passed_event_class : Nil + ASPEC::Methods.assert_compile_time_error "expected argument #1 to 'listener' to be Athena::EventDispatcher::Event.class, not String.", <<-CR + require "./spec_helper.cr" + + AED::EventDispatcher.new.listener String do + end + CR + end + def test_initial_state : Nil @dispatcher.listeners.should be_empty @dispatcher.has_listeners?.should be_false diff --git a/src/components/framework/spec/abstract_bundle_spec.cr b/src/components/framework/spec/abstract_bundle_spec.cr new file mode 100644 index 000000000..43e89f915 --- /dev/null +++ b/src/components/framework/spec/abstract_bundle_spec.cr @@ -0,0 +1,30 @@ +require "./spec_helper" + +private def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil + ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line + require "./spec_helper.cr" + #{code} + CR +end + +describe ATH::AbstractBundle do + describe "compiler errors", tags: "compiled" do + describe ATH::Bundle do + it "when the bundle does not inherit from ATH::AbstractBundle" do + assert_compile_time_error "The provided bundle 'String' be inherit from 'ATH::AbstractBundle'.", <<-CR + ATH.register_bundle String + CR + end + + it "when the bundle does not provide its name" do + assert_compile_time_error "Unable to determine extension name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR + @[Athena::Framework::Annotations::Bundle] + struct MyBundle < Athena::Framework::AbstractBundle + end + + ATH.register_bundle MyBundle + CR + end + end + end +end diff --git a/src/components/framework/src/abstract_bundle.cr b/src/components/framework/src/abstract_bundle.cr index 606fad372..4dcde13e6 100644 --- a/src/components/framework/src/abstract_bundle.cr +++ b/src/components/framework/src/abstract_bundle.cr @@ -12,13 +12,13 @@ module Athena::Framework resolved_bundle = bundle.resolve unless resolved_bundle <= AbstractBundle - bundle.raise "Must be a child of 'ATH::AbstractBundle'." + bundle.raise "The provided bundle '#{bundle}' be inherit from 'ATH::AbstractBundle'." end ann = resolved_bundle.annotation Athena::Framework::Annotations::Bundle unless name = ann[0] || ann["name"] - bundle.raise "Unable to determine extension name." + bundle.raise "Unable to determine extension name. It was not provided as the first positional argument nor via the 'name' field." end %} From 1c3232d7ab6348873466b2c709479aa205ff4a37 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Mon, 30 Jun 2025 22:45:24 -0400 Subject: [PATCH 09/10] Resolve last missed line --- src/components/spec/spec/methods_spec.cr | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/spec/spec/methods_spec.cr b/src/components/spec/spec/methods_spec.cr index b6ada3368..5b5db6e2a 100644 --- a/src/components/spec/spec/methods_spec.cr +++ b/src/components/spec/spec/methods_spec.cr @@ -3,21 +3,14 @@ require "./spec_helper" describe ASPEC::Methods do describe ".assert_compile_time_error", tags: "compiled" do it "allows customizing crystal binary via CRYSTAL env var" do - old_val = ENV["CRYSTAL"]?.presence + # Do this in its own sub-process to avoid mucking with ENV. + assert_runtime_error "'/path/to/crystal': No such file or directory", <<-CR + require "./spec_helper" - begin ENV["CRYSTAL"] = "/path/to/crystal" - expect_raises File::NotFoundError do - assert_compile_time_error "", "" - end - ensure - if old_val - ENV["CRYSTAL"] = old_val - else - ENV.delete "CRYSTAL" - end - end + assert_compile_time_error "", "" + CR end it do From 73576ead4f9dd1030379fb935d5819974cfdb754 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sat, 26 Jul 2025 21:03:28 -0400 Subject: [PATCH 10/10] Add informational min rystal version to root `shard.yml` --- shard.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shard.yml b/shard.yml index dd6a77d76..70fe77456 100644 --- a/shard.yml +++ b/shard.yml @@ -1,6 +1,9 @@ name: athena-ecosystem version: 0.0.0 +# Min Crystal version required to run the test suite/develop on Athena. +crystal: ~> 1.17 + license: MIT repository: https://github.com/athena-framework/athena