diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a24396..f39c833 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,30 +3,20 @@ on: pull_request: branches: - main + paths-ignore: + - 'docs/**' push: branches: - main tags: '*' + paths-ignore: + - 'docs/**' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: - test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - strategy: - fail-fast: false - matrix: - version: - - 'min' - - 'lts' - - '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia. - - 'pre' - os: - - ubuntu-latest - arch: - - x64 - uses: "SciML/.github/.github/workflows/tests.yml@v1" - with: - julia-version: "${{ matrix.version }}" - julia-arch: "${{ matrix.arch }}" - os: "${{ matrix.os }}" + tests: + uses: "SciML/.github/.github/workflows/grouped-tests.yml@v1" secrets: "inherit" docs: name: Documentation @@ -34,18 +24,3 @@ jobs: with: coverage: false secrets: "inherit" - runic: - name: Runic - uses: "SciML/.github/.github/workflows/runic.yml@v1" - secrets: "inherit" - spellcheck: - name: Spell Check - uses: "SciML/.github/.github/workflows/spellcheck.yml@v1" - secrets: "inherit" - downgrade: - name: Downgrade - uses: "SciML/.github/.github/workflows/downgrade.yml@v1" - with: - julia-version: "lts" - skip: "Pkg,TOML" - secrets: "inherit" diff --git a/Project.toml b/Project.toml index 0287cae..128a729 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,8 @@ authors = ["Jonnie Diegelman and contributors"] version = "0.2.4" [compat] -julia = "1.1" +Test = "1" +julia = "1.10" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/core.jl b/test/core.jl new file mode 100644 index 0000000..4b29fa7 --- /dev/null +++ b/test/core.jl @@ -0,0 +1,237 @@ +using ConcreteStructs +using ConcreteStructs: _parse_head, _parse_struct_def, _parse_line +using ConcreteStructs: _strip_super, _get_subparams, _get_constructor_params +using LinearAlgebra: Adjoint +using Suppressor +using Test + + +# # Unit Tests + +# Test setup +line_number_node = LineNumberNode(1) +annotated_abstract = :(b::AbstractFloat) +annotated_concrete = :(b::Float64) +struct_name = :(MyStruct) +sub_typed_params = :(MyStruct{T1, T2 <: AbstractVector{T1}}) +sub_typed = :(MyStruct{D} <: Number) +kitchen_sink = :(MyStruct{T1, T2 <: AbstractVector{T1}} <: AbstractVector{T1}) +plain_assignment = :(a = 1) +annotated_assignment = :(a::Float64 = 2.0) +subtype_annotated_assignment = :(a <: Real = 2.0) + + +# Run tests +@testset "Unit tests" begin + @testset "_parse_line" begin + @test _parse_line(line_number_node) == (line_number_node, nothing) + # @test _parse_line(annotated_abstract)[1].args[2] != :AbstractFloat + # @test _parse_line(annotated_abstract)[2].args[2] == :AbstractFloat + @test _parse_line(annotated_concrete)[1].args[2] == :Float64 + @test _parse_line(annotated_concrete)[2] === nothing + + parsed_sym = _parse_line(:a) + @test parsed_sym[1].args[2] == parsed_sym[2] + + @test _parse_line(plain_assignment)[1].head == :(=) + @test _parse_line(plain_assignment)[1].args[1].args[2] == _parse_line(plain_assignment)[2] + @test _parse_line(annotated_assignment)[1].head == :(=) + @test _parse_line(annotated_assignment)[2] === nothing + @test _parse_line(subtype_annotated_assignment)[1].head == :(=) + @test _parse_line(subtype_annotated_assignment)[2].args[2] == :Real + end + + @testset "_parse_struct_def" begin + @test _parse_struct_def(struct_name) == (struct_name, []) + @test _parse_struct_def(sub_typed_params) == (struct_name, [:T1, :(T2 <: AbstractVector{T1})]) + end + + @testset "_parse_head" begin + @test _parse_head(struct_name) == (struct_name, [], :Any) + @test _parse_head(sub_typed) == (struct_name, Any[:D], :Number) + @test _parse_head(kitchen_sink) == (struct_name, [:T1, :(T2 <: AbstractVector{T1})], :(AbstractVector{T1})) + end + + @testset "_strip_super" begin + @test _strip_super(struct_name) == struct_name + @test _strip_super(kitchen_sink) == :(MyStruct{T1, T2 <: AbstractVector{T1}}) + @test _strip_super([struct_name, sub_typed]) == [struct_name, :(MyStruct{D})] + end + + @testset "_get_subparams" begin + @test _get_subparams(struct_name) == [] + @test _get_subparams([:T1, :(T2 <: AbstractArray{T1, N}), :(T3 <: Complex{T1})]) == Any[:T1, :N, :T1] + end + + @testset "_get_constructor_params" begin + @test _get_constructor_params([:T, :(A <: AbstractVector{T})], [:A, :B]) == [] + @test _get_constructor_params([:iip, :T, :(A <: AbstractVector{T})], [:A, :B]) == [:iip] + @test _get_constructor_params([:iip, :T, :(A <: AbstractVector{T}), :B], [:B]) == [:iip, :A] + end +end + + +# # End-to-end tests + +# Test setup +@concrete struct Plain end +plain = Plain() + +@concrete terse struct PlainTerse end +plain_terse = PlainTerse() + +"test" +@concrete struct PlainDoc end +plain_doc = PlainDoc() + +"test" +@concrete terse struct PlainTerseDoc end +plain_terse_doc = PlainTerseDoc() + +@concrete struct Args + a + b +end +args = Args(1 + im, "hi") + +@concrete terse struct ArgsTerse <: Number + a + b +end +args_terse = ArgsTerse(1 + im, "hi") + +@concrete mutable struct SubtypedMutable <: Number + a + b +end +subtyped_mutable = SubtypedMutable(3.0, 4.0f0) + +@concrete struct Partial{A} + # Hey there, I'm a comment + a::A + + "And I'm a docsctring" + b +end +partial = Partial(:yo, 1 // 2) + +@concrete mutable struct ConstructorMutable{iip, C} + a + b + c::C +end +ConstructorMutable(a, b, c) = ConstructorMutable{true}(a, b, eltype(a)(c)) +constructor_mutable = ConstructorMutable([1.0 + im, 2], 'r', 1.5) +constructor_mutable.b = 'h' + +@concrete terse struct TerseSameType{A} + a::A + b::A +end +function TerseSameType(a::A, b::B) where {A, B} + T = promote_type(A, B) + return TerseSameType{T}(T(a), T(b)) +end +terse_same_type = TerseSameType(1 + im, 5.0f0) + +@concrete terse struct FullyParameterized{B} + a::Symbol + b::B +end +fully_parameterized = FullyParameterized(:sine, sin) + +@concrete mutable struct ParameterizedSubtyped{T, N, B <: AbstractArray{T, N}} <: AbstractArray{T, N} + a + b::B + c::T +end +parameterized_subtyped = ParameterizedSubtyped(:🏦, [1, 2, 3], 4) +Base.size(x::ParameterizedSubtyped) = size(x.b) +Base.getindex(x::ParameterizedSubtyped, i...) = x.b[i...] + +@concrete terse struct HangingTypeParam{iip} + a + b +end +hanging_type_param = HangingTypeParam{true}(1, 2.0) + +@concrete terse struct HangingTypeParam2{iip, B} + a + b::B +end +hanging_type_param2 = HangingTypeParam2{true}(1, 2.0) + + +Base.@kwdef @concrete struct KWDef + a = "ayyy" + b::Float64 = 4.0 + c <: Real = 3 + d + e::Float64 + f <: Real +end +kwdef = KWDef(d = "hi", e = 2.0, f = 1 // 2) + + +# Run tests +@testset "End-to-end tests" begin + @test_throws ErrorException args.a = 2 + im + @test_throws MethodError subtyped_mutable.a = "hi" + + @test typeof(partial) |> isconcretetype + @test typeof(terse_same_type.a) === typeof(terse_same_type.b) + @test typeof(fully_parameterized.a) |> isconcretetype + @test eltype(parameterized_subtyped.b) === typeof(parameterized_subtyped.c) + @test typeof(kwdef) |> isconcretetype + + @test @capture_out(show(stdout, MIME("text/plain"), typeof(fully_parameterized))) == "FullyParameterized{typeof(sin)}" + @test @capture_out(show(stdout, FullyParameterized)) == "FullyParameterized" + @test @capture_out(show(stdout, hanging_type_param)) == "HangingTypeParam{true}(1, 2.0)" + @test @capture_out(show(stdout, MIME("text/plain"), ArgsTerse)) == "ArgsTerse" +end + +@testset "Issue #3" begin + @concrete struct Foo{T <: Real} + x::T + y <: Real + z <: AbstractMatrix{T} + end + + foo = Foo(1, 1.0, [1, 2]') + + @test typeof(foo) === Foo{Int, Float64, Adjoint{Int, Vector{Int}}} +end + + +@testset "Commutation with other Macros" begin + # this has been working for a while already + Base.@kwdef @concrete struct Foo2{T <: Real} + x::T = 1.0 + y = true + z = 42 + end + + foo = Foo2() + @test typeof(foo) === Foo2{Float64, Bool, Int64} + + # this is new + @concrete Base.@kwdef struct Foo3{T <: Real} + x::T = 1.0 + y = true + z = 42 + end + + foo = Foo3() + @test typeof(foo) === Foo3{Float64, Bool, Int64} + + foo = Foo3(z = [1, 2, 3]) + @test typeof(foo) === Foo3{Float64, Bool, Vector{Int64}} + + # terse test case, unfortunately doesn't work the other way around. But also never did. + @concrete terse Base.@kwdef struct TerseKWDef + a = 1.0 + b = true + end + terse_kw_def = TerseKWDef(b = false) + @test typeof(terse_kw_def) === TerseKWDef{Float64, Bool} +end diff --git a/test/qa/Project.toml b/test/qa/Project.toml new file mode 100644 index 0000000..f38d12c --- /dev/null +++ b/test/qa/Project.toml @@ -0,0 +1,14 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[sources] +ConcreteStructs = {path = "../.."} + +[compat] +Aqua = "0.8" +JET = "0.9, 0.10, 0.11" +Test = "1" +julia = "1.10" diff --git a/test/qa/qa.jl b/test/qa/qa.jl new file mode 100644 index 0000000..73eea9e --- /dev/null +++ b/test/qa/qa.jl @@ -0,0 +1,12 @@ +using ConcreteStructs +using Aqua +using JET +using Test + +@testset "Code quality (Aqua.jl)" begin + Aqua.test_all(ConcreteStructs) +end + +@testset "Code linting (JET.jl)" begin + JET.test_package(ConcreteStructs; target_defined_modules = true) +end diff --git a/test/runtests.jl b/test/runtests.jl index 4b29fa7..f1872f8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,237 +1,15 @@ -using ConcreteStructs -using ConcreteStructs: _parse_head, _parse_struct_def, _parse_line -using ConcreteStructs: _strip_super, _get_subparams, _get_constructor_params -using LinearAlgebra: Adjoint -using Suppressor -using Test +using Pkg +const GROUP = get(ENV, "GROUP", "All") -# # Unit Tests - -# Test setup -line_number_node = LineNumberNode(1) -annotated_abstract = :(b::AbstractFloat) -annotated_concrete = :(b::Float64) -struct_name = :(MyStruct) -sub_typed_params = :(MyStruct{T1, T2 <: AbstractVector{T1}}) -sub_typed = :(MyStruct{D} <: Number) -kitchen_sink = :(MyStruct{T1, T2 <: AbstractVector{T1}} <: AbstractVector{T1}) -plain_assignment = :(a = 1) -annotated_assignment = :(a::Float64 = 2.0) -subtype_annotated_assignment = :(a <: Real = 2.0) - - -# Run tests -@testset "Unit tests" begin - @testset "_parse_line" begin - @test _parse_line(line_number_node) == (line_number_node, nothing) - # @test _parse_line(annotated_abstract)[1].args[2] != :AbstractFloat - # @test _parse_line(annotated_abstract)[2].args[2] == :AbstractFloat - @test _parse_line(annotated_concrete)[1].args[2] == :Float64 - @test _parse_line(annotated_concrete)[2] === nothing - - parsed_sym = _parse_line(:a) - @test parsed_sym[1].args[2] == parsed_sym[2] - - @test _parse_line(plain_assignment)[1].head == :(=) - @test _parse_line(plain_assignment)[1].args[1].args[2] == _parse_line(plain_assignment)[2] - @test _parse_line(annotated_assignment)[1].head == :(=) - @test _parse_line(annotated_assignment)[2] === nothing - @test _parse_line(subtype_annotated_assignment)[1].head == :(=) - @test _parse_line(subtype_annotated_assignment)[2].args[2] == :Real - end - - @testset "_parse_struct_def" begin - @test _parse_struct_def(struct_name) == (struct_name, []) - @test _parse_struct_def(sub_typed_params) == (struct_name, [:T1, :(T2 <: AbstractVector{T1})]) - end - - @testset "_parse_head" begin - @test _parse_head(struct_name) == (struct_name, [], :Any) - @test _parse_head(sub_typed) == (struct_name, Any[:D], :Number) - @test _parse_head(kitchen_sink) == (struct_name, [:T1, :(T2 <: AbstractVector{T1})], :(AbstractVector{T1})) - end - - @testset "_strip_super" begin - @test _strip_super(struct_name) == struct_name - @test _strip_super(kitchen_sink) == :(MyStruct{T1, T2 <: AbstractVector{T1}}) - @test _strip_super([struct_name, sub_typed]) == [struct_name, :(MyStruct{D})] - end - - @testset "_get_subparams" begin - @test _get_subparams(struct_name) == [] - @test _get_subparams([:T1, :(T2 <: AbstractArray{T1, N}), :(T3 <: Complex{T1})]) == Any[:T1, :N, :T1] - end - - @testset "_get_constructor_params" begin - @test _get_constructor_params([:T, :(A <: AbstractVector{T})], [:A, :B]) == [] - @test _get_constructor_params([:iip, :T, :(A <: AbstractVector{T})], [:A, :B]) == [:iip] - @test _get_constructor_params([:iip, :T, :(A <: AbstractVector{T}), :B], [:B]) == [:iip, :A] - end -end - - -# # End-to-end tests - -# Test setup -@concrete struct Plain end -plain = Plain() - -@concrete terse struct PlainTerse end -plain_terse = PlainTerse() - -"test" -@concrete struct PlainDoc end -plain_doc = PlainDoc() - -"test" -@concrete terse struct PlainTerseDoc end -plain_terse_doc = PlainTerseDoc() - -@concrete struct Args - a - b -end -args = Args(1 + im, "hi") - -@concrete terse struct ArgsTerse <: Number - a - b -end -args_terse = ArgsTerse(1 + im, "hi") - -@concrete mutable struct SubtypedMutable <: Number - a - b -end -subtyped_mutable = SubtypedMutable(3.0, 4.0f0) - -@concrete struct Partial{A} - # Hey there, I'm a comment - a::A - - "And I'm a docsctring" - b -end -partial = Partial(:yo, 1 // 2) - -@concrete mutable struct ConstructorMutable{iip, C} - a - b - c::C -end -ConstructorMutable(a, b, c) = ConstructorMutable{true}(a, b, eltype(a)(c)) -constructor_mutable = ConstructorMutable([1.0 + im, 2], 'r', 1.5) -constructor_mutable.b = 'h' - -@concrete terse struct TerseSameType{A} - a::A - b::A -end -function TerseSameType(a::A, b::B) where {A, B} - T = promote_type(A, B) - return TerseSameType{T}(T(a), T(b)) -end -terse_same_type = TerseSameType(1 + im, 5.0f0) - -@concrete terse struct FullyParameterized{B} - a::Symbol - b::B -end -fully_parameterized = FullyParameterized(:sine, sin) - -@concrete mutable struct ParameterizedSubtyped{T, N, B <: AbstractArray{T, N}} <: AbstractArray{T, N} - a - b::B - c::T -end -parameterized_subtyped = ParameterizedSubtyped(:🏦, [1, 2, 3], 4) -Base.size(x::ParameterizedSubtyped) = size(x.b) -Base.getindex(x::ParameterizedSubtyped, i...) = x.b[i...] - -@concrete terse struct HangingTypeParam{iip} - a - b -end -hanging_type_param = HangingTypeParam{true}(1, 2.0) - -@concrete terse struct HangingTypeParam2{iip, B} - a - b::B -end -hanging_type_param2 = HangingTypeParam2{true}(1, 2.0) - - -Base.@kwdef @concrete struct KWDef - a = "ayyy" - b::Float64 = 4.0 - c <: Real = 3 - d - e::Float64 - f <: Real -end -kwdef = KWDef(d = "hi", e = 2.0, f = 1 // 2) - - -# Run tests -@testset "End-to-end tests" begin - @test_throws ErrorException args.a = 2 + im - @test_throws MethodError subtyped_mutable.a = "hi" - - @test typeof(partial) |> isconcretetype - @test typeof(terse_same_type.a) === typeof(terse_same_type.b) - @test typeof(fully_parameterized.a) |> isconcretetype - @test eltype(parameterized_subtyped.b) === typeof(parameterized_subtyped.c) - @test typeof(kwdef) |> isconcretetype - - @test @capture_out(show(stdout, MIME("text/plain"), typeof(fully_parameterized))) == "FullyParameterized{typeof(sin)}" - @test @capture_out(show(stdout, FullyParameterized)) == "FullyParameterized" - @test @capture_out(show(stdout, hanging_type_param)) == "HangingTypeParam{true}(1, 2.0)" - @test @capture_out(show(stdout, MIME("text/plain"), ArgsTerse)) == "ArgsTerse" -end - -@testset "Issue #3" begin - @concrete struct Foo{T <: Real} - x::T - y <: Real - z <: AbstractMatrix{T} - end - - foo = Foo(1, 1.0, [1, 2]') - - @test typeof(foo) === Foo{Int, Float64, Adjoint{Int, Vector{Int}}} +if GROUP == "All" || GROUP == "Core" + include("core.jl") end - -@testset "Commutation with other Macros" begin - # this has been working for a while already - Base.@kwdef @concrete struct Foo2{T <: Real} - x::T = 1.0 - y = true - z = 42 - end - - foo = Foo2() - @test typeof(foo) === Foo2{Float64, Bool, Int64} - - # this is new - @concrete Base.@kwdef struct Foo3{T <: Real} - x::T = 1.0 - y = true - z = 42 - end - - foo = Foo3() - @test typeof(foo) === Foo3{Float64, Bool, Int64} - - foo = Foo3(z = [1, 2, 3]) - @test typeof(foo) === Foo3{Float64, Bool, Vector{Int64}} - - # terse test case, unfortunately doesn't work the other way around. But also never did. - @concrete terse Base.@kwdef struct TerseKWDef - a = 1.0 - b = true - end - terse_kw_def = TerseKWDef(b = false) - @test typeof(terse_kw_def) === TerseKWDef{Float64, Bool} +if GROUP == "All" || GROUP == "QA" + # The QA group carries its own dependencies (Aqua, JET) in test/qa/Project.toml. + Pkg.activate(joinpath(@__DIR__, "qa")) + Pkg.develop(PackageSpec(path = joinpath(@__DIR__, ".."))) + Pkg.instantiate() + include(joinpath(@__DIR__, "qa", "qa.jl")) end diff --git a/test/test_groups.toml b/test/test_groups.toml new file mode 100644 index 0000000..78dc46c --- /dev/null +++ b/test/test_groups.toml @@ -0,0 +1,9 @@ +# Test-group matrix consumed by SciML/.github grouped-tests.yml. Each group is +# dispatched into runtests.jl via the GROUP env var and runs on every Julia +# version listed here. The QA group (Aqua + JET) carries its own dependencies in +# test/qa/Project.toml and is not part of the default `All` run's main test env. +[Core] +versions = ["lts", "1", "pre"] + +[QA] +versions = ["lts", "1"]