diff --git a/test/suite/core/test_core.jl b/test/suite/core/test_core.jl deleted file mode 100644 index 3cbb6b11..00000000 --- a/test/suite/core/test_core.jl +++ /dev/null @@ -1,24 +0,0 @@ -module TestCore - -import Test -import CTBase.Core - -const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true -const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true - -function test_core() - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Core" begin - Test.@testset "Default value of the display during resolution" begin - Test.@test Core.__display() - end - - Test.@testset "Type aliases" begin - Test.@test Core.ctNumber === Real - Test.@test Core.ctNumber === Real - end - end -end - -end # module - -test_core() = TestCore.test_core() diff --git a/test/suite/core/test_core_types.jl b/test/suite/core/test_core_types.jl new file mode 100644 index 00000000..05ddd850 --- /dev/null +++ b/test/suite/core/test_core_types.jl @@ -0,0 +1,21 @@ +module TestCoreTypes + +import Test +import CTBase.Core + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_types() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Type aliases" begin + Test.@test Core.ctNumber == Real + Test.@test Core.ctNumber === Real + Test.@test 1 isa Core.ctNumber + Test.@test 1.0 isa Core.ctNumber + end + return nothing +end + +end # module + +test_core_types() = TestCoreTypes.test_types() diff --git a/test/suite/core/test_utils.jl b/test/suite/core/test_utils.jl new file mode 100644 index 00000000..016e63a2 --- /dev/null +++ b/test/suite/core/test_utils.jl @@ -0,0 +1,18 @@ +module TestCoreUtils + +import Test +import CTBase.Core + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_utils() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Default value of the display during resolution" begin + Test.@test Core.__display() + end + return nothing +end + +end # module + +test_utils() = TestCoreUtils.test_utils() diff --git a/test/suite/descriptions/test_display_description.jl b/test/suite/descriptions/test_display_description.jl index 8bdbf573..70182f3c 100644 --- a/test/suite/descriptions/test_display_description.jl +++ b/test/suite/descriptions/test_display_description.jl @@ -1,6 +1,7 @@ module TestDisplayDescription import Test +import CTBase.Descriptions: Descriptions const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true diff --git a/test/suite/exceptions/test_exception_display.jl b/test/suite/exceptions/test_exception_display.jl index e3b9e8a0..0ec180e6 100644 --- a/test/suite/exceptions/test_exception_display.jl +++ b/test/suite/exceptions/test_exception_display.jl @@ -413,6 +413,73 @@ function test_exception_display() Test.@test contains(output, "Suggestion:") Test.@test contains(output, "Increase max iterations") end + + Test.@testset "IncorrectArgument - got without expected" begin + io = IOBuffer() + e = Exceptions.IncorrectArgument("wrong value", got="bad_value") + Test.@test_nowarn showerror(io, e) + output = String(take!(io)) + + Test.@test contains(output, "Got:") + Test.@test contains(output, "bad_value") + Test.@test !contains(output, "Expected:") + end + + Test.@testset "AmbiguousDescription - diagnostic rendering" begin + # "empty catalog" branch + io = IOBuffer() + e = Exceptions.AmbiguousDescription( + (:a,); diagnostic="empty catalog" + ) + Test.@test_nowarn showerror(io, e) + output = String(take!(io)) + Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "Empty catalog") + + # "unknown symbols" branch + io = IOBuffer() + e = Exceptions.AmbiguousDescription( + (:z,); diagnostic="unknown symbols" + ) + Test.@test_nowarn showerror(io, e) + output = String(take!(io)) + Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "Unknown symbols") + + # "no complete match" branch + io = IOBuffer() + e = Exceptions.AmbiguousDescription( + (:a, :z); diagnostic="no complete match" + ) + Test.@test_nowarn showerror(io, e) + output = String(take!(io)) + Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "No complete match") + + # else branch — arbitrary diagnostic value + io = IOBuffer() + e = Exceptions.AmbiguousDescription( + (:a,); diagnostic="custom diagnostic" + ) + Test.@test_nowarn showerror(io, e) + output = String(take!(io)) + Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "custom diagnostic") + end + + Test.@testset "AmbiguousDescription - closest-matches inline suggestion" begin + io = IOBuffer() + e = Exceptions.AmbiguousDescription( + (:b, :f); + candidates=["(:a, :b, :c)", "(:a, :d, :e)", "(:x, :y, :z)"], + suggestion="Try one of the closest matches:", + ) + Test.@test_nowarn showerror(io, e) + output = String(take!(io)) + + Test.@test contains(output, "closest matches") + Test.@test contains(output, "(:a, :b, :c)") + end end end diff --git a/test/suite/exceptions/test_types.jl b/test/suite/exceptions/test_exception_types.jl similarity index 99% rename from test/suite/exceptions/test_types.jl rename to test/suite/exceptions/test_exception_types.jl index f900ee50..3ecd3a68 100644 --- a/test/suite/exceptions/test_types.jl +++ b/test/suite/exceptions/test_exception_types.jl @@ -262,4 +262,4 @@ end end # module -test_types() = TestExceptionTypes.test_exception_types() +test_exception_types() = TestExceptionTypes.test_exception_types() diff --git a/test/suite/exceptions/test_extension_error.jl b/test/suite/exceptions/test_extension_error.jl new file mode 100644 index 00000000..645f8999 --- /dev/null +++ b/test/suite/exceptions/test_extension_error.jl @@ -0,0 +1,64 @@ +module TestExtensionError + +import Test +import CTBase.Exceptions + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_extension_error() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ExtensionError Contract Implementation" begin + # Test constructor throws if no dependencies provided + Test.@test_throws Exceptions.PreconditionError Exceptions.ExtensionError() + + # Test enriched ExtensionError creation + e = Exceptions.ExtensionError( + :Documenter, + :Markdown; + message="to generate documentation", + feature="automatic documentation", + context="reference generation", + ) + + Test.@test e isa Exceptions.ExtensionError + Test.@test e.weakdeps == (:Documenter, :Markdown) + Test.@test e.msg == "missing dependencies to generate documentation" + Test.@test e.feature == "automatic documentation" + Test.@test e.context == "reference generation" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ExtensionError Constructor Validation" begin + Test.@testset "No dependencies provided" begin + Test.@test_throws Exceptions.PreconditionError Exceptions.ExtensionError() + + try + Exceptions.ExtensionError() + Test.@test false # Should not reach here + catch e + Test.@test e isa Exceptions.PreconditionError + Test.@test occursin("weak dependence", e.msg) + Test.@test occursin("ExtensionError called without dependencies", e.reason) + end + end + + Test.@testset "Single dependency" begin + e = Exceptions.ExtensionError(:MyExt) + Test.@test e isa Exceptions.ExtensionError + Test.@test e.weakdeps == (:MyExt,) + Test.@test e.msg == "missing dependencies" + end + + Test.@testset "Multiple dependencies with message" begin + e = Exceptions.ExtensionError(:Ext1, :Ext2; message="to enable feature X") + Test.@test e isa Exceptions.ExtensionError + Test.@test e.weakdeps == (:Ext1, :Ext2) + Test.@test e.msg == "missing dependencies to enable feature X" + end + end + + return nothing +end + +end # module + +test_extension_error() = TestExtensionError.test_extension_error() diff --git a/test/suite/extensions/test_coverage_edge_cases.jl b/test/suite/extensions/test_coverage_edge_cases.jl deleted file mode 100644 index 0f8195f2..00000000 --- a/test/suite/extensions/test_coverage_edge_cases.jl +++ /dev/null @@ -1,208 +0,0 @@ -module TestCoverageEdgeCases - -import Test -import CTBase -import Documenter -import Coverage - -# Access internal modules via get_extension -const CP = Base.get_extension(CTBase, :CoveragePostprocessing) -const DR = Base.get_extension(CTBase, :DocumenterReference) -const TR = Base.get_extension(CTBase, :TestRunner) - -const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true -const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true - -function test_coverage_edge_cases() - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Coverage and Test Edge Cases" begin - - # ---------------------------------------------------------------------------------- - # CoveragePostprocessing: trigger error at line 92 - # Error: "Coverage requested but no usable .cov files were found after cleanup." - # Strategy: Mocking fails because the function is likely inlined. - # We skip this test as the line is unreachable under normal conditions. - # ---------------------------------------------------------------------------------- - # Test.@testset "CoveragePostprocessing: clean_stale_cov_files! deletes all" begin - # Test.@test CP !== nothing - # mktempdir() do tmp - # cd(tmp) do - # mkpath("src") - # mkpath("coverage") - # # Create one valid cov file to pass the first check (n_cov > 0) - # touch(joinpath("src", "valid.jl.123.cov")) - - # # We need to redefine _clean_stale_cov_files! temporarily. - # original_clean = CP._clean_stale_cov_files! - - # try - # # Redefine to delete everything - # Base.eval( - # CP, - # quote - # function _clean_stale_cov_files!(source_dirs) - # for dir in source_dirs - # for (root, _, files) in walkdir(dir) - # for f in files - # endswith(f, ".cov") && - # rm(joinpath(root, f); force=true) - # end - # end - # end - # end - # end, - # ) - - # err = try - # CTBase.postprocess_coverage( - # CTBase.Extensions.CoveragePostprocessingTag(); - # generate_report=false, - # root_dir=tmp, - # ) - # nothing - # catch e - # e - # end - - # Test.@test err isa ErrorException - # Test.@test occursin("no usable .cov files", err.msg) - - # finally - # # Restore original function by defining a method that calls the captured original - # Base.eval( - # CP, - # quote - # function _clean_stale_cov_files!(source_dirs) - # return $(original_clean)(source_dirs) - # end - # end, - # ) - # end - # end - # end - # end - - # ---------------------------------------------------------------------------------- - # TestRunner: trigger error at line 424 - # Error: "Test file ... not found for test ..." inside _run_single_test - # Strategy: Create a file that exists during discovery but is deleted before execution. - # ---------------------------------------------------------------------------------- - Test.@testset "TestRunner: file exists then disappears" begin - Test.@test TR !== nothing - mktempdir() do tmp - # Create a test file that will be deleted before execution - test_file = joinpath(tmp, "phantom_test.jl") - touch(test_file) - - # Delete the file before calling _run_single_test - rm(test_file) - - err = try - TR._run_single_test( - :phantom_test; - filename_builder=identity, - funcname_builder=identity, - eval_mode=false, - test_dir=tmp, - ) - nothing - catch e - e - end - - Test.@test err isa CTBase.Exceptions.IncorrectArgument - Test.@test occursin("not found", err.msg) - end - end - - # ---------------------------------------------------------------------------------- - # DocumenterReference: Missing coverage - # ---------------------------------------------------------------------------------- - Test.@testset "DocumenterReference: Edge cases" begin - - # Line 327: Documenter.Selectors.order(::Type{APIBuilder}) - # Explicit call to ensure coverage - Test.@test Documenter.Selectors.order(DR.APIBuilder) == 0.5 - - # Line 539: _exported_symbols getfield failure - # Used BrokenExportMod defined at top level - - # This should catch the error and skip the symbol, covering the catch block - syms = DR._exported_symbols(BrokenExportMod) - # Verify undefined_sym is not in the result - Test.@test !any(p -> first(p) == :undefined_sym, syms.exported) - - # Line 607: _get_source_from_docstring - # Used HackDocMod defined at top level - - binding = Base.Docs.Binding(HackDocMod, :f) - # Retrieve the MultiDoc using Base.Docs.meta - meta = Base.Docs.meta(HackDocMod) - if haskey(meta, binding) - mdoc = meta[binding] - if !isempty(mdoc.docs) - # Get the DocStr (it's the first one usually, mapped to sig) - docstr = first(mdoc.docs)[2] - - if docstr isa Base.Docs.DocStr - # Save original path - orig_path = get(docstr.data, :path, nothing) - - try - # Remove path from metadata - docstr.data[:path] = nothing - - # Should return nothing now - src = DR._get_source_from_docstring(HackDocMod, :f) - Test.@test src === nothing - - finally - # Restore - if orig_path !== nothing - docstr.data[:path] = orig_path - end - end - end - end - end - - # ---------------------------------------------------------------------------------- - # New tests for Lines 617-626: _get_source_from_methods filtering - # ---------------------------------------------------------------------------------- - # We construct a fake object that mimics a Method-like behavior or mocked logic, - # but since `methods(obj)` returns a MethodList, we can't easily mock `methods()`. - # Instead, we define a dummy object and check built-in behavior, - # OR we can manually invoke the filtering logic if we could isolate it. - # - # Ideally, we want to test: - # if file != "" && file != "none" && !startswith(file, ".") - - # It's hard to force a method to have file="" in pure Julia without C. - # However, we can use `Core.intrinsics` or similar if needed, - # but they don't usually have methods attached in the same way. - - # Alternative: Define a method in a REPL-like way (often "none" or "REPL[1]") - # or rely on the fact that `+` usually has built-in methods. - - # Let's inspect `+` methods. - path_plus = DR._get_source_from_methods(+) - # valid outcome is either nothing (if all are built-in) or a path (if some are extended). - # This at least runs the loop. - Test.@test path_plus === nothing || path_plus isa String - end - end -end - -# Define helper modules at top-level -module BrokenExportMod - export undefined_sym -# undefined_sym is not defined -end - -module HackDocMod - "My doc" - f() = 1 -end - -end # module - -test_coverage_edge_cases() = TestCoverageEdgeCases.test_coverage_edge_cases() diff --git a/test/suite/extensions/test_coverage_helpers.jl b/test/suite/extensions/test_coverage_helpers.jl new file mode 100644 index 00000000..6461bc96 --- /dev/null +++ b/test/suite/extensions/test_coverage_helpers.jl @@ -0,0 +1,92 @@ +module TestCoverageHelpers + +import Test +import CTBase +import Coverage + +const CP = Base.get_extension(CTBase, :CoveragePostprocessing) + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_coverage_helpers() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_reset_coverage_dir" begin + mktempdir() do temp_dir + cov_dir = joinpath(temp_dir, "coverage") + cov_storage_dir = joinpath(temp_dir, "cov") + mkpath(cov_dir) + touch(joinpath(cov_dir, "test.jl.123.cov")) + + redirect_stdout(devnull) do + CP._reset_coverage_dir(cov_dir, cov_storage_dir) + end + Test.@test !isdir(cov_dir) + Test.@test isdir(cov_storage_dir) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_count_cov_files" begin + mktempdir() do temp_dir + # Create some .cov files + touch(joinpath(temp_dir, "test1.jl.123.cov")) + touch(joinpath(temp_dir, "test2.jl.456.cov")) + touch(joinpath(temp_dir, "readme.md")) # Should be ignored + + count = CP._count_cov_files([temp_dir]) + Test.@test count == 2 + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_clean_stale_cov_files!" begin + mktempdir() do temp_dir + # Create .cov files with same PID (should be kept) + touch(joinpath(temp_dir, "test1.jl.123.cov")) + touch(joinpath(temp_dir, "test2.jl.123.cov")) + touch(joinpath(temp_dir, "readme.md")) + + # Clean should keep files with same PID + CP._clean_stale_cov_files!([temp_dir]) + Test.@test isfile(joinpath(temp_dir, "test1.jl.123.cov")) + Test.@test isfile(joinpath(temp_dir, "test2.jl.123.cov")) + Test.@test isfile(joinpath(temp_dir, "readme.md")) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_collect_and_move_cov_files!" begin + mktempdir() do temp_dir + src_dir = joinpath(temp_dir, "src") + cov_dir = joinpath(temp_dir, "coverage") + mkpath(src_dir) + mkpath(cov_dir) + + # Create .cov files in src + touch(joinpath(src_dir, "test1.jl.123.cov")) + touch(joinpath(src_dir, "test2.jl.456.cov")) + + # Collect and move + CP._collect_and_move_cov_files!([src_dir], cov_dir) + + # Files should be moved to cov_dir + Test.@test !isfile(joinpath(src_dir, "test1.jl.123.cov")) + Test.@test !isfile(joinpath(src_dir, "test2.jl.456.cov")) + Test.@test isfile(joinpath(cov_dir, "test1.jl.123.cov")) + Test.@test isfile(joinpath(cov_dir, "test2.jl.456.cov")) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_get_pid_suffix" begin + # Test that _get_pid_suffix extracts PID from file path + suffix = CP._get_pid_suffix("src/test.jl.12345.cov") + Test.@test suffix == "12345" + + # Test with no PID suffix + suffix_empty = CP._get_pid_suffix("src/test.jl.cov") + Test.@test suffix_empty == "" + end + + return nothing +end + +end # module + +test_coverage_helpers() = TestCoverageHelpers.test_coverage_helpers() diff --git a/test/suite/extensions/test_documenter_reference.jl b/test/suite/extensions/test_documenter_reference.jl index c5f567bb..c4f79874 100644 --- a/test/suite/extensions/test_documenter_reference.jl +++ b/test/suite/extensions/test_documenter_reference.jl @@ -2,7 +2,6 @@ module TestDocumenterReference import Test import CTBase -import CTBase.Exceptions: Exceptions import CTBase.Extensions: Extensions import Documenter @@ -12,813 +11,18 @@ const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) const DR = DocumenterReference -# ------------------------------------------------------------------------------------------ # -""" -Docstring for the main test module used to validate module-level documentation -in the generated API pages. -""" -module DocumenterReferenceTestMod - -""" -Simple documented function used to test source file detection and inclusion. -""" -myfun(x) = x - -""" -Function that should be kept by _iterate_over_symbols. -""" -keep(x) = x - -""" -Function that will be excluded via the `exclude` configuration. -""" -skip(x) = x - -# No docstring: this function should be skipped by _iterate_over_symbols. -no_doc(x) = x - -""" -Test submodule with a docstring but no associated source file information. -Used to exercise include_without_source behaviour for modules. -""" -module SubModule end - -abstract type AbstractFoo end - -struct Foo <: AbstractFoo - x::Int +# Test module for integration tests +module DocumenterReferenceIntegrationTestMod + """ + Docstring for the main test module used to validate module-level documentation + in the generated API pages. + """ + myfun(x) = x end - -const MYCONST = 42 - -end -using .DocumenterReferenceTestMod -# ------------------------------------------------------------------------------------------ # - -# ------------------------------------------------------------------------------------------ # -module DRTypeFormatTestMod -struct Simple end -struct Parametric{T} end -struct WithValue{T,N} end -end -# ------------------------------------------------------------------------------------------ # - -# ------------------------------------------------------------------------------------------ # -module DRMethodTestMod -f() = 1 -f(x::Int) = x -g(x::Int, y::String) = x -h(xs::Int...) = length(xs) -end -# ------------------------------------------------------------------------------------------ # - -# ------------------------------------------------------------------------------------------ # -module DRExternalTestMod -extfun(x::Int) = x -extfun(x::String) = length(x) -end -# ------------------------------------------------------------------------------------------ # - -# ------------------------------------------------------------------------------------------ # -module DRNoDocModule -module Inner end -end -# ------------------------------------------------------------------------------------------ # - -# ------------------------------------------------------------------------------------------ # -module DRUnionAllTestMod -f(x::T) where {T} = x -end -# ------------------------------------------------------------------------------------------ # +using .DocumenterReferenceIntegrationTestMod function test_documenter_reference() - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Invalid primary_modules input" begin - redirect_stdout(devnull) do - redirect_stderr(devnull) do - Test.@test_throws Extensions.IncorrectArgument Extensions.automatic_reference_documentation( - Extensions.DocumenterReferenceTag(); - subdirectory="ref", - primary_modules=["invalid_string"], # String is not Module or Pair - title="My API", - ) - end - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "reset_config! clears CONFIG" begin - DR.reset_config!() - Test.@test isempty(DR.CONFIG) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_default_basename and _build_page_path" begin - Test.@test DR._default_basename("manual", true, true) == "manual" - Test.@test DR._default_basename("", true, true) == "api" - Test.@test DR._default_basename("", true, false) == "public" - Test.@test DR._default_basename("", false, true) == "private" - - Test.@test DR._build_page_path("api", "public") == "api/public" - Test.@test DR._build_page_path(".", "public") == "public" - Test.@test DR._build_page_path("", "public") == "public" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_classify_symbol and _to_string" begin - Test.@test DR._classify_symbol(nothing, "@mymacro") == DR.DOCTYPE_MACRO - Test.@test DR._classify_symbol(DocumenterReferenceTestMod.SubModule, "SubModule") == - DR.DOCTYPE_MODULE - Test.@test DR._classify_symbol(DocumenterReferenceTestMod.AbstractFoo, "AbstractFoo") == - DR.DOCTYPE_ABSTRACT_TYPE - Test.@test DR._classify_symbol(DocumenterReferenceTestMod.Foo, "Foo") == - DR.DOCTYPE_STRUCT - Test.@test DR._classify_symbol(DocumenterReferenceTestMod.myfun, "myfun") == - DR.DOCTYPE_FUNCTION - Test.@test DR._classify_symbol(DocumenterReferenceTestMod.MYCONST, "MYCONST") == - DR.DOCTYPE_CONSTANT - - Test.@test DR._to_string(DR.DOCTYPE_ABSTRACT_TYPE) == "abstract type" - Test.@test DR._to_string(DR.DOCTYPE_CONSTANT) == "constant" - Test.@test DR._to_string(DR.DOCTYPE_FUNCTION) == "function" - Test.@test DR._to_string(DR.DOCTYPE_MACRO) == "macro" - Test.@test DR._to_string(DR.DOCTYPE_MODULE) == "module" - Test.@test DR._to_string(DR.DOCTYPE_STRUCT) == "struct" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_get_source_file" begin - path = DR._get_source_file(DocumenterReferenceTestMod, :myfun, DR.DOCTYPE_FUNCTION) - Test.@test path === abspath(@__FILE__) - - # No docstring => should use method-based resolution - path2 = DR._get_source_file( - DocumenterReferenceTestMod, :no_doc, DR.DOCTYPE_FUNCTION - ) - Test.@test path2 === abspath(@__FILE__) - - # Missing symbol should be caught and return nothing - missing_path = DR._get_source_file( - DocumenterReferenceTestMod, :__does_not_exist__, DR.DOCTYPE_FUNCTION - ) - Test.@test missing_path === nothing - - const_path = DR._get_source_file( - DocumenterReferenceTestMod, :MYCONST, DR.DOCTYPE_CONSTANT - ) - Test.@test const_path === nothing - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_get_source_from_methods" begin - # For built-in operators like +, source detection behavior may vary by Julia version. - # In Julia 1.12+, it may return a source file path even for +. - # We just verify the function runs without error and returns String or nothing. - result = DR._get_source_from_methods(+) - Test.@test result === nothing || result isa String - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_parse_primary_modules with Pair" begin - d = DR._parse_primary_modules([DocumenterReferenceTestMod => @__FILE__]) - Test.@test d[DocumenterReferenceTestMod] == [abspath(@__FILE__)] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_has_documentation: module documented elsewhere" begin - modules = Dict(DRNoDocModule.Inner => Any[]) - Test.@test DR._has_documentation(DRNoDocModule, :Inner, DR.DOCTYPE_MODULE, modules) == - true - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Type formatting helpers" begin - Test.@test DR._format_datatype_for_docs(UInt) == "::UInt" - - # Parametric types without concrete params are UnionAll, use _format_type_for_docs - param_result = DR._format_type_for_docs(DRTypeFormatTestMod.Parametric) - Test.@test occursin("Parametric", param_result) - - # Concrete params => kept (WithValue{Int,2} is a DataType) - concrete_result = DR._format_datatype_for_docs(DRTypeFormatTestMod.WithValue{Int,2}) - Test.@test occursin("WithValue", concrete_result) - Test.@test occursin("Int", concrete_result) - Test.@test occursin("2", concrete_result) - - # Fallback branch for non-type inputs - Test.@test DR._format_type_for_docs(1) == "::1" - - # TypeVar formatting in _format_type_param - need to unwrap UnionAll first - unwrapped = Base.unwrap_unionall(DRTypeFormatTestMod.Parametric) - tv = unwrapped.parameters[1] - Test.@test tv isa TypeVar - Test.@test DR._format_type_param(tv) == "T" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_method_signature_string handles UnionAll" begin - m = first(methods(DRUnionAllTestMod.f)) - s = DR._method_signature_string(m, DRUnionAllTestMod, :f) - Test.@test occursin("DRUnionAllTestMod.f", s) - Test.@test occursin("T", s) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_iterate_over_symbols filtering" begin - current_module = DocumenterReferenceTestMod - modules = Dict(current_module => Any[]) - exclude = Set{Symbol}([:skip]) - sort_by(x) = x - source_files = String[] - - config = DR._Config( - current_module, - "api", - modules, - sort_by, - exclude, - true, - true, - "API Reference", - "API Reference", - source_files, - "api", - false, - Module[], - "", - "", - "", - "", - ) - - symbols = [ - :keep => DR.DOCTYPE_FUNCTION, - :skip => DR.DOCTYPE_FUNCTION, - :no_doc => DR.DOCTYPE_FUNCTION, - ] - - seen = Symbol[] - redirect_stdout(devnull) do - redirect_stderr(devnull) do - DR._iterate_over_symbols(config, symbols) do key, type - return push!(seen, key) - end - end - end - - Test.@test :keep in seen - Test.@test :skip ∉ seen - Test.@test :no_doc ∉ seen - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_iterate_over_symbols with source_files and include_without_source" begin - current_module = DocumenterReferenceTestMod - modules = Dict(current_module => Any[]) - sort_by(x) = x - allowed = [abspath(@__FILE__)] - - # Case 1: Only symbols whose source is in `allowed` are kept - config1 = DR._Config( - current_module, - "api", - modules, - sort_by, - Set{Symbol}(), - true, - true, - "API Reference", - "API Reference", - allowed, - "api", - false, - Module[], - "", - "", - "", - "", - ) - - symbols1 = [:myfun => DR.DOCTYPE_FUNCTION] - - seen1 = Symbol[] - redirect_stdout(devnull) do - redirect_stderr(devnull) do - DR._iterate_over_symbols(config1, symbols1) do key, type - return push!(seen1, key) - end - end - end - Test.@test :myfun in seen1 - - # Case 2: Module symbols without source are kept only when include_without_source=true - config2 = DR._Config( - current_module, - "api", - modules, - sort_by, - Set{Symbol}(), - true, - true, - "API Reference", - "API Reference", - allowed, - "api", - false, - Module[], - "", - "", - "", - "", - ) - - config3 = DR._Config( - current_module, - "api", - modules, - sort_by, - Set{Symbol}(), - true, - true, - "API Reference", - "API Reference", - allowed, - "api", - true, - Module[], - "", - "", - "", - "", - ) - - symbols_module = [:SubModule => DR.DOCTYPE_MODULE] - - seen2 = Symbol[] - redirect_stdout(devnull) do - redirect_stderr(devnull) do - DR._iterate_over_symbols(config2, symbols_module) do key, type - return push!(seen2, key) - end - end - end - Test.@test isempty(seen2) - - seen3 = Symbol[] - redirect_stdout(devnull) do - redirect_stderr(devnull) do - DR._iterate_over_symbols(config3, symbols_module) do key, type - return push!(seen3, key) - end - end - end - Test.@test :SubModule in seen3 - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "automatic_reference_documentation configuration" begin - DR.reset_config!() - - # Single-module, public-only - pages1 = redirect_stdout(devnull) do - redirect_stderr(devnull) do - Extensions.automatic_reference_documentation( - Extensions.DocumenterReferenceTag(); - subdirectory="ref", - primary_modules=[DocumenterReferenceTestMod], - public=true, - private=false, - title="My API", - ) - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "automatic_reference_documentation default tag" begin - DR.reset_config!() - - pages_default = redirect_stdout(devnull) do - redirect_stderr(devnull) do - Extensions.automatic_reference_documentation(; - subdirectory="ref", - primary_modules=[DocumenterReferenceTestMod], - public=true, - private=false, - title="My API", - ) - end - end - - Test.@test length(DR.CONFIG) == 1 - cfg_default = DR.CONFIG[1] - Test.@test cfg_default.current_module === DocumenterReferenceTestMod - Test.@test cfg_default.subdirectory == "ref" - Test.@test cfg_default.public == true - Test.@test cfg_default.private == false - Test.@test cfg_default.filename == "public" - Test.@test pages_default == pages1 - end - - Test.@test length(DR.CONFIG) == 1 - cfg1 = DR.CONFIG[1] - Test.@test cfg1.current_module === DocumenterReferenceTestMod - Test.@test cfg1.subdirectory == "ref" - Test.@test cfg1.public == true - Test.@test cfg1.private == false - Test.@test cfg1.filename == "public" - Test.@test pages1 == ("My API" => "ref/public.md") - - # Both public and private pages - DR.reset_config!() - pages2 = redirect_stdout(devnull) do - redirect_stderr(devnull) do - Extensions.automatic_reference_documentation( - Extensions.DocumenterReferenceTag(); - subdirectory="ref", - primary_modules=[DocumenterReferenceTestMod], - public=true, - private=true, - title="All API", - ) - end - end - - Test.@test length(DR.CONFIG) == 1 - cfg2 = DR.CONFIG[1] - Test.@test cfg2.filename == "api" - Test.@test cfg2.public == true - Test.@test cfg2.private == true - Test.@test cfg2.title == "All API" - Test.@test pages2 == ( - "All API" => - ["Public" => "ref/api_public.md", "Private" => "ref/api_private.md"] - ) - - # public=false, private=false should error - redirect_stdout(devnull) do - redirect_stderr(devnull) do - Test.@test_throws Extensions.IncorrectArgument Extensions.automatic_reference_documentation( - Extensions.DocumenterReferenceTag(); - subdirectory="ref", - primary_modules=[DocumenterReferenceTestMod], - public=false, - private=false, - ) - end - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Documenter.Selectors.order for APIBuilder" begin - Test.@test Documenter.Selectors.order(DR.APIBuilder) == 0.5 - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_collect_module_docstrings includes module docstring" begin - DR.reset_config!() - - config = DR._Config( - DocumenterReferenceTestMod, - "api", - Dict(DocumenterReferenceTestMod => Any[]), - identity, - Set{Symbol}(), - true, - true, - "API", - "API", - String[], - "api", - false, - Module[], - "", - "", - "", - "", - ) - - symbols = DR._exported_symbols(DocumenterReferenceTestMod).exported - docs = DR._collect_module_docstrings(config, symbols) - - Test.@test any( - doc -> occursin("```@docs\n$(DocumenterReferenceTestMod)\n```", doc), docs - ) - end - - # ============================================================================ - # NEW TESTS: _exported_symbols - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_exported_symbols classification" begin - # Test that _exported_symbols correctly classifies symbols - result = DR._exported_symbols(DocumenterReferenceTestMod) - - # Check structure: should return NamedTuple with exported and private fields - Test.@test haskey(result, :exported) - Test.@test haskey(result, :private) - - # Check that exported list contains expected symbols (none exported in test module) - Test.@test result.exported isa Vector - - # Check that private list contains our test symbols - private_symbols = Dict(result.private) - Test.@test haskey(private_symbols, :myfun) - Test.@test private_symbols[:myfun] == DR.DOCTYPE_FUNCTION - Test.@test haskey(private_symbols, :keep) - Test.@test haskey(private_symbols, :skip) - Test.@test haskey(private_symbols, :no_doc) - Test.@test haskey(private_symbols, :SubModule) - Test.@test private_symbols[:SubModule] == DR.DOCTYPE_MODULE - Test.@test haskey(private_symbols, :AbstractFoo) - Test.@test private_symbols[:AbstractFoo] == DR.DOCTYPE_ABSTRACT_TYPE - Test.@test haskey(private_symbols, :Foo) - Test.@test private_symbols[:Foo] == DR.DOCTYPE_STRUCT - Test.@test haskey(private_symbols, :MYCONST) - Test.@test private_symbols[:MYCONST] == DR.DOCTYPE_CONSTANT - end - - # ============================================================================ - # NEW TESTS: automatic_reference_documentation multi-module - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "automatic_reference_documentation multi-module" begin - DR.reset_config!() - - # Create a second test module for multi-module testing - mod1 = DocumenterReferenceTestMod - - # Test multi-module case (using same module twice as a proxy) - pages = redirect_stdout(devnull) do - redirect_stderr(devnull) do - Extensions.automatic_reference_documentation( - Extensions.DocumenterReferenceTag(); - subdirectory="api", - primary_modules=[mod1, mod1], # Two entries to trigger multi-module path - public=true, - private=true, - title="Multi API", - ) - end - end - - # Should return a Pair with title and list of module pages - Test.@test pages isa Pair - Test.@test first(pages) == "Multi API" - Test.@test last(pages) isa Vector - - # CONFIG should have 1 entry (one per unique module) - Test.@test length(DR.CONFIG) == 1 - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "automatic_reference_documentation multi-module with filename" begin - DR.reset_config!() - - mod1 = DocumenterReferenceTestMod - mod2 = DRMethodTestMod - - # Test multi-module case with explicit filename (combined page - public only) - # Note: DocumenterReference currently splits public/private if both are true, - # ignoring the filename for the split structure. - # So we test public-only to verify filename is respected. - pages = redirect_stdout(devnull) do - redirect_stderr(devnull) do - Extensions.automatic_reference_documentation( - Extensions.DocumenterReferenceTag(); - subdirectory="api", - primary_modules=[mod1, mod2], - public=true, - private=false, - title="Combined Public API", - filename="combined_public", - ) - end - end - - # Should return a Pair with title and path to the combined file - Test.@test pages isa Pair - Test.@test first(pages) == "Combined Public API" - Test.@test last(pages) == "api/combined_public.md" - - # CONFIG should have 2 entries (one per unique module) - Test.@test length(DR.CONFIG) == 2 - Test.@test count(c -> c.current_module == mod1, DR.CONFIG) == 1 - Test.@test count(c -> c.current_module == mod2, DR.CONFIG) == 1 - end - - # ============================================================================ - # NEW TESTS: _get_source_file expanded - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_get_source_file expanded cases" begin - # Function case (already tested, but verify) - func_path = DR._get_source_file( - DocumenterReferenceTestMod, :myfun, DR.DOCTYPE_FUNCTION - ) - Test.@test func_path !== nothing - Test.@test endswith(func_path, "test_documenter_reference.jl") - - # Struct case - should find source via constructor methods - struct_path = DR._get_source_file( - DocumenterReferenceTestMod, :Foo, DR.DOCTYPE_STRUCT - ) - # Structs defined in test file should be found - Test.@test struct_path === nothing || - endswith(struct_path, "test_documenter_reference.jl") - - # Abstract type case - typically returns nothing (no constructor) - abstract_path = DR._get_source_file( - DocumenterReferenceTestMod, :AbstractFoo, DR.DOCTYPE_ABSTRACT_TYPE - ) - Test.@test abstract_path === nothing - - # Module case - modules cannot reliably determine source - mod_path = DR._get_source_file( - DocumenterReferenceTestMod, :SubModule, DR.DOCTYPE_MODULE - ) - Test.@test mod_path === nothing - - # Constant case (already tested) - const_path = DR._get_source_file( - DocumenterReferenceTestMod, :MYCONST, DR.DOCTYPE_CONSTANT - ) - Test.@test const_path === nothing - end - - # ============================================================================ - # NEW TESTS: Edge cases - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Edge cases" begin - # _build_page_path with various inputs - Test.@test DR._build_page_path("", "") == "" - Test.@test DR._build_page_path(".", "") == "" - Test.@test DR._build_page_path("dir", "") == "dir/" - Test.@test DR._build_page_path("a/b/c", "file.md") == "a/b/c/file.md" - - # _default_basename edge cases - Test.@test DR._default_basename("custom", false, false) == "custom" # Custom overrides flags - Test.@test DR._default_basename("", false, false) == "private" # Fallback when both false - - # _classify_symbol with edge cases - Test.@test DR._classify_symbol(42, "fortytwo") == DR.DOCTYPE_CONSTANT # Integer constant - Test.@test DR._classify_symbol("hello", "greeting") == DR.DOCTYPE_CONSTANT # String constant - Test.@test DR._classify_symbol([1, 2, 3], "myarray") == DR.DOCTYPE_CONSTANT # Array constant - - # _to_string covers all enum values (verify no error) - for dt in instances(DR.DocType) - Test.@test DR._to_string(dt) isa String - Test.@test length(DR._to_string(dt)) > 0 - end - - # _iterate_over_symbols with empty list - config = DR._Config( - DocumenterReferenceTestMod, - "api", - Dict(DocumenterReferenceTestMod => Any[]), - identity, - Set{Symbol}(), - true, - true, - "API", - "API", - String[], - "api", - false, - Module[], - "", - "", - "", - "", - ) - seen = Symbol[] - redirect_stdout(devnull) do - redirect_stderr(devnull) do - DR._iterate_over_symbols(config, Pair{Symbol,DR.DocType}[]) do key, type - return push!(seen, key) - end - end - end - Test.@test isempty(seen) - - # reset_config! is idempotent - DR.reset_config!() - DR.reset_config!() - Test.@test isempty(DR.CONFIG) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_type_for_docs and helpers" begin - simple_str = DR._format_type_for_docs(DRTypeFormatTestMod.Simple) - Test.@test startswith(simple_str, "::") - Test.@test occursin("DRTypeFormatTestMod.Simple", simple_str) - - param_type = DRTypeFormatTestMod.Parametric{DRTypeFormatTestMod.Simple} - param_str = DR._format_type_for_docs(param_type) - Test.@test occursin("Parametric", param_str) - Test.@test occursin("Simple", param_str) - - union_str = DR._format_type_for_docs(Union{DRTypeFormatTestMod.Simple,Nothing}) - Test.@test occursin("Union", union_str) - Test.@test occursin("Simple", union_str) - Test.@test occursin("Nothing", union_str) - - value_type = DRTypeFormatTestMod.WithValue{Int,3} - value_str = DR._format_type_for_docs(value_type) - Test.@test occursin("WithValue", value_str) - Test.@test occursin("3", value_str) - - param_fmt = DR._format_type_param(DRTypeFormatTestMod.Simple) - Test.@test occursin("DRTypeFormatTestMod.Simple", param_fmt) - - value_param_fmt = DR._format_type_param(3) - Test.@test value_param_fmt == "3" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_method_signature_string and method collection" begin - methods_f = methods(DRMethodTestMod.f) - m0 = first(filter(m -> length(m.sig.parameters) == 1, methods_f)) - sig0 = DR._method_signature_string(m0, DRMethodTestMod, :f) - Test.@test endswith(sig0, "DRMethodTestMod.f()") - - m1 = first(filter(m -> length(m.sig.parameters) == 2, methods_f)) - sig1 = DR._method_signature_string(m1, DRMethodTestMod, :f) - Test.@test occursin("DRMethodTestMod.f", sig1) - Test.@test occursin("Int", sig1) - - methods_h = methods(DRMethodTestMod.h) - mvar = first(methods_h) - sigv = DR._method_signature_string(mvar, DRMethodTestMod, :h) - Test.@test occursin("Vararg", sigv) - - source_files = [abspath(@__FILE__)] - methods_by_func = DR._collect_methods_from_source_files( - DRMethodTestMod, source_files - ) - Test.@test :f in keys(methods_by_func) - Test.@test :h in keys(methods_by_func) - for ms in values(methods_by_func) - for m in ms - file = String(m.file) - Test.@test abspath(file) in source_files - end - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Page content builders" begin - modules_str = "ModA, ModB" - module_contents_private = [ - (DocumenterReferenceTestMod, String[], ["priv_a"]), - (DRMethodTestMod, String[], ["priv_b1", "priv_b2"]), - ] - # Test with is_split=false (single page) - overview_priv, docs_priv = DR._build_private_page_content( - modules_str, module_contents_private, false - ) - Test.@test occursin("Private API", overview_priv) - Test.@test occursin("ModA, ModB", overview_priv) - Test.@test !isempty(docs_priv) - Test.@test any(occursin("priv_a", s) for s in docs_priv) - Test.@test any(occursin("priv_b1", s) for s in docs_priv) - - module_contents_public = [ - (DocumenterReferenceTestMod, ["pub_a"], String[]), - (DRMethodTestMod, String[], String[]), - ] - # Test with is_split=false (single page) - overview_pub, docs_pub = DR._build_public_page_content( - modules_str, module_contents_public, false - ) - Test.@test occursin("Public API", overview_pub) - Test.@test occursin("ModA, ModB", overview_pub) - Test.@test !isempty(docs_pub) - Test.@test any(occursin("pub_a", s) for s in docs_pub) - - module_contents_combined = [(DocumenterReferenceTestMod, ["pub_a"], ["priv_a"])] - overview_comb, docs_comb = DR._build_combined_page_content( - modules_str, module_contents_combined - ) - Test.@test occursin("API reference", overview_comb) - Test.@test any(occursin("Public API", s) for s in docs_comb) - Test.@test any(occursin("Private API", s) for s in docs_comb) - Test.@test any(occursin("pub_a", s) for s in docs_comb) - Test.@test any(occursin("priv_a", s) for s in docs_comb) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "external_modules_to_document" begin - current_module = DocumenterReferenceTestMod - modules = Dict(current_module => String[]) - sort_by(x) = x - source_files = [abspath(@__FILE__)] - - config = DR._Config( - current_module, - "api_ext", - modules, - sort_by, - Set{Symbol}(), - true, - true, - "Ext API", - "Ext API", - source_files, - "api_ext", - false, - [DRExternalTestMod], - "", - "", - "", - "", - ) - - private_docs = DR._collect_private_docstrings(config, Pair{Symbol,DR.DocType}[]) - Test.@test !isempty(private_docs) - Test.@test any(occursin("DRExternalTestMod.extfun", s) for s in private_docs) - end - + # Keep only integration tests - pipeline integration with Documenter Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "APIBuilder runner integration" begin DR.reset_config!() @@ -827,7 +31,7 @@ function test_documenter_reference() Extensions.automatic_reference_documentation( Extensions.DocumenterReferenceTag(); subdirectory="api_integration", - primary_modules=[DocumenterReferenceTestMod], + primary_modules=[DocumenterReferenceIntegrationTestMod], public=true, private=true, title="Integration API", @@ -855,226 +59,7 @@ function test_documenter_reference() Test.@test any(endswith(k, "api_private.md") for k in keys(doc.blueprint.pages)) end - # ============================================================================ - # Edge Case Tests: Type Formatting Functions - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_type_for_docs edge cases" begin - # Test Union types formatting (covers L775-782) - union_result = DR._format_type_for_docs(Union{Int,String}) - Test.@test occursin("Union", union_result) - Test.@test occursin("Int", union_result) - Test.@test occursin("String", union_result) - - # Test simple non-DataType fallback (covers L782) - Test.@test DR._format_type_for_docs(Any) == "::Any" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_datatype_for_docs with TypeVar" begin - # Test parametric type with concrete parameters - concrete_result = DR._format_datatype_for_docs(Vector{Int}) - Test.@test occursin("Vector", concrete_result) || occursin("Array", concrete_result) - Test.@test occursin("Int", concrete_result) - - # Test parametric type - the function strips parameters when TypeVar present - # Array{T,1} where T has a TypeVar, so it gets stripped - Test.@test DR._format_datatype_for_docs(Int) == "::Int" - Test.@test DR._format_datatype_for_docs(String) == "::String" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_type_param edge cases" begin - # Test Type parameter (covers L824-826) - type_result = DR._format_type_param(Int) - Test.@test type_result == "Int" # Should strip leading :: - - # Test value parameter (covers L830) - e.g., for sized arrays - Test.@test DR._format_type_param(3) == "3" - Test.@test DR._format_type_param(42) == "42" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_method_signature_string edge cases" begin - # Test method signature generation for DRMethodTestMod - m = first(methods(DRMethodTestMod.f)) - sig = DR._method_signature_string(m, DRMethodTestMod, :f) - Test.@test occursin("DRMethodTestMod", sig) - Test.@test occursin("f", sig) - - # Test with multiple arguments - m2 = first(methods(DRMethodTestMod.g)) - sig2 = DR._method_signature_string(m2, DRMethodTestMod, :g) - Test.@test occursin("g", sig2) - end - - # ============================================================================ - # NEW TESTS: Title System with is_split Parameter - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Page content builders with is_split parameter" begin - modules_str = "TestModule" - - # Test private page with is_split=false (single page) - module_contents_private = [(DocumenterReferenceTestMod, String[], ["priv_doc"])] - - overview_priv_single, docs_priv_single = DR._build_private_page_content( - modules_str, module_contents_private, false - ) - Test.@test occursin("# Private API", overview_priv_single) - Test.@test !occursin("# Private\n", overview_priv_single) - Test.@test occursin("non-exported", overview_priv_single) - - # Test private page with is_split=true (split page) - overview_priv_split, docs_priv_split = DR._build_private_page_content( - modules_str, module_contents_private, true - ) - Test.@test occursin("# Private API", overview_priv_split) - Test.@test occursin("non-exported", overview_priv_split) - - # Test public page with is_split=false (single page) - module_contents_public = [(DocumenterReferenceTestMod, ["pub_doc"], String[])] - - overview_pub_single, docs_pub_single = DR._build_public_page_content( - modules_str, module_contents_public, false - ) - Test.@test occursin("# Public API", overview_pub_single) - Test.@test occursin("exported", overview_pub_single) - - # Test public page with is_split=true (split page) - overview_pub_split, docs_pub_split = DR._build_public_page_content( - modules_str, module_contents_public, true - ) - Test.@test occursin("# Public API", overview_pub_split) - Test.@test occursin("exported", overview_pub_split) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Title consistency across different page types" begin - modules_str = "MyModule" - module_contents = [(DocumenterReferenceTestMod, ["pub"], ["priv"])] - - # Single private page should have "Private API" title - overview_priv, _ = DR._build_private_page_content( - modules_str, module_contents, false - ) - Test.@test occursin("# Private API", overview_priv) - - # Single public page should have "Public API" title - overview_pub, _ = DR._build_public_page_content(modules_str, module_contents, false) - Test.@test occursin("# Public API", overview_pub) - - # Split private page should have "Private API" title - overview_priv_split, _ = DR._build_private_page_content( - modules_str, module_contents, true - ) - Test.@test occursin("# Private API", overview_priv_split) - - # Split public page should have "Public API" title - overview_pub_split, _ = DR._build_public_page_content( - modules_str, module_contents, true - ) - Test.@test occursin("# Public API", overview_pub_split) - - # Combined page should have "API reference" title - overview_comb, _ = DR._build_combined_page_content(modules_str, module_contents) - Test.@test occursin("# API reference", overview_comb) - end - - # ============================================================================ - # NEW TESTS: Customization Parameters - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Custom titles for API pages" begin - modules_str = "TestModule" - module_contents = [(DocumenterReferenceTestMod, ["pub_doc"], ["priv_doc"])] - - # Test custom title for private page (single) - overview_priv_custom, _ = DR._build_private_page_content( - modules_str, module_contents, false; custom_title="Internal API" - ) - Test.@test occursin("# Internal API", overview_priv_custom) - Test.@test !occursin("# Private API", overview_priv_custom) - - # Test custom title for public page (single) - overview_pub_custom, _ = DR._build_public_page_content( - modules_str, module_contents, false; custom_title="Exported API" - ) - Test.@test occursin("# Exported API", overview_pub_custom) - Test.@test !occursin("# Public API", overview_pub_custom) - - # Test custom title for private page (split) - overview_priv_split_custom, _ = DR._build_private_page_content( - modules_str, module_contents, true; custom_title="Internal" - ) - Test.@test occursin("# Internal", overview_priv_split_custom) - Test.@test !occursin("# Private API", overview_priv_split_custom) - - # Test custom title for public page (split) - overview_pub_split_custom, _ = DR._build_public_page_content( - modules_str, module_contents, true; custom_title="Exported" - ) - Test.@test occursin("# Exported", overview_pub_split_custom) - Test.@test !occursin("# Public API", overview_pub_split_custom) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Custom descriptions for API pages" begin - modules_str = "TestModule" - module_contents = [(DocumenterReferenceTestMod, ["pub_doc"], ["priv_doc"])] - - # Test custom description for private page - custom_desc_priv = "This page documents internal implementation details." - overview_priv_desc, _ = DR._build_private_page_content( - modules_str, module_contents, false; custom_description=custom_desc_priv - ) - Test.@test occursin(custom_desc_priv, overview_priv_desc) - Test.@test !occursin("non-exported", overview_priv_desc) - - # Test custom description for public page - custom_desc_pub = "This page documents the public interface for end users." - overview_pub_desc, _ = DR._build_public_page_content( - modules_str, module_contents, false; custom_description=custom_desc_pub - ) - Test.@test occursin(custom_desc_pub, overview_pub_desc) - Test.@test !occursin("exported", overview_pub_desc) || - occursin(custom_desc_pub, overview_pub_desc) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Combined custom title and description" begin - modules_str = "TestModule" - module_contents = [(DocumenterReferenceTestMod, ["pub_doc"], ["priv_doc"])] - - # Test both custom title and description together - custom_title = "Developer Reference" - custom_desc = "Advanced documentation for contributors and maintainers." - - overview, _ = DR._build_private_page_content( - modules_str, - module_contents, - false; - custom_title=custom_title, - custom_description=custom_desc, - ) - - Test.@test occursin("# Developer Reference", overview) - Test.@test occursin(custom_desc, overview) - Test.@test !occursin("# Private API", overview) - Test.@test !occursin("non-exported", overview) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Empty customization uses defaults" begin - modules_str = "TestModule" - module_contents = [(DocumenterReferenceTestMod, ["pub_doc"], ["priv_doc"])] - - # Empty strings should use default behavior - overview_priv_empty, _ = DR._build_private_page_content( - modules_str, module_contents, false; custom_title="", custom_description="" - ) - Test.@test occursin("# Private API", overview_priv_empty) - Test.@test occursin("non-exported", overview_priv_empty) - - overview_pub_empty, _ = DR._build_public_page_content( - modules_str, module_contents, false; custom_title="", custom_description="" - ) - Test.@test occursin("# Public API", overview_pub_empty) - Test.@test occursin("exported", overview_pub_empty) - end + return nothing end end # module TestDocumenterReference diff --git a/test/suite/extensions/test_documenter_reference_api.jl b/test/suite/extensions/test_documenter_reference_api.jl new file mode 100644 index 00000000..01f4ff0f --- /dev/null +++ b/test/suite/extensions/test_documenter_reference_api.jl @@ -0,0 +1,164 @@ +module TestDocumenterReferenceAPI + +import Test +import CTBase +import CTBase.Exceptions: Exceptions +import CTBase.Extensions: Extensions +import Documenter + +const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) +const DR = DocumenterReference + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# Test module for API tests +module DocumenterReferenceAPITestMod + myfun(x) = x +end +using .DocumenterReferenceAPITestMod + +function test_documenter_reference_api() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "reset_config!" begin + DR.reset_config!() + Test.@test isempty(DR.CONFIG) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Invalid primary_modules input" begin + redirect_stdout(devnull) do + redirect_stderr(devnull) do + Test.@test_throws Extensions.IncorrectArgument Extensions.automatic_reference_documentation( + Extensions.DocumenterReferenceTag(); + subdirectory="ref", + primary_modules=["invalid_string"], # String is not Module or Pair + title="My API", + ) + end + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "automatic_reference_documentation configuration" begin + DR.reset_config!() + + # Single-module, public-only + pages1 = redirect_stdout(devnull) do + redirect_stderr(devnull) do + Extensions.automatic_reference_documentation( + Extensions.DocumenterReferenceTag(); + subdirectory="ref", + primary_modules=[DocumenterReferenceAPITestMod], + public=true, + private=false, + title="My API", + ) + end + end + + Test.@test length(DR.CONFIG) == 1 + cfg1 = DR.CONFIG[1] + Test.@test cfg1.current_module === DocumenterReferenceAPITestMod + Test.@test cfg1.subdirectory == "ref" + Test.@test cfg1.public == true + Test.@test cfg1.private == false + Test.@test cfg1.filename == "public" + Test.@test pages1 == ("My API" => "ref/public.md") + + # Both public and private pages + DR.reset_config!() + pages2 = redirect_stdout(devnull) do + redirect_stderr(devnull) do + Extensions.automatic_reference_documentation( + Extensions.DocumenterReferenceTag(); + subdirectory="ref", + primary_modules=[DocumenterReferenceAPITestMod], + public=true, + private=true, + title="All API", + ) + end + end + + Test.@test length(DR.CONFIG) == 1 + cfg2 = DR.CONFIG[1] + Test.@test cfg2.filename == "api" + Test.@test cfg2.public == true + Test.@test cfg2.private == true + Test.@test cfg2.title == "All API" + Test.@test pages2 == ( + "All API" => + ["Public" => "ref/api_public.md", "Private" => "ref/api_private.md"] + ) + + # public=false, private=false should error + redirect_stdout(devnull) do + redirect_stderr(devnull) do + Test.@test_throws Extensions.IncorrectArgument Extensions.automatic_reference_documentation( + Extensions.DocumenterReferenceTag(); + subdirectory="ref", + primary_modules=[DocumenterReferenceAPITestMod], + public=false, + private=false, + ) + end + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "automatic_reference_documentation multi-module" begin + DR.reset_config!() + + # Test multi-module case (using same module twice as a proxy) + pages = redirect_stdout(devnull) do + redirect_stderr(devnull) do + Extensions.automatic_reference_documentation( + Extensions.DocumenterReferenceTag(); + subdirectory="api", + primary_modules=[DocumenterReferenceAPITestMod, DocumenterReferenceAPITestMod], + public=true, + private=true, + title="Multi API", + ) + end + end + + # Should return a Pair with title and list of module pages + Test.@test pages isa Pair + Test.@test first(pages) == "Multi API" + Test.@test last(pages) isa Vector + + # CONFIG should have 1 entry (one per unique module) + Test.@test length(DR.CONFIG) == 1 + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "automatic_reference_documentation multi-module with filename" begin + DR.reset_config!() + + # Test multi-module case with explicit filename (combined page - public only) + pages = redirect_stdout(devnull) do + redirect_stderr(devnull) do + Extensions.automatic_reference_documentation( + Extensions.DocumenterReferenceTag(); + subdirectory="api", + primary_modules=[DocumenterReferenceAPITestMod, DocumenterReferenceAPITestMod], + public=true, + private=false, + title="Combined Public API", + filename="combined_public", + ) + end + end + + # Should return a Pair with title and path to the combined file + Test.@test pages isa Pair + Test.@test first(pages) == "Combined Public API" + Test.@test last(pages) == "api/combined_public.md" + + # CONFIG should have 1 entry (one per unique module) + Test.@test length(DR.CONFIG) == 1 + end + + return nothing +end + +end # module + +test_documenter_reference_api() = TestDocumenterReferenceAPI.test_documenter_reference_api() diff --git a/test/suite/extensions/test_documenter_reference_config_helpers.jl b/test/suite/extensions/test_documenter_reference_config_helpers.jl new file mode 100644 index 00000000..978dabe6 --- /dev/null +++ b/test/suite/extensions/test_documenter_reference_config_helpers.jl @@ -0,0 +1,49 @@ +module TestDocumenterReferenceConfigHelpers + +import Test +import CTBase +import Documenter + +const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) +const DR = DocumenterReference + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# Test module for config helpers +module DocumenterReferenceConfigTestMod + myfun(x) = x +end +using .DocumenterReferenceConfigTestMod + +function test_documenter_reference_config_helpers() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_default_basename" begin + Test.@test DR._default_basename("manual", true, true) == "manual" + Test.@test DR._default_basename("", true, true) == "api" + Test.@test DR._default_basename("", true, false) == "public" + Test.@test DR._default_basename("", false, true) == "private" + Test.@test DR._default_basename("custom", false, false) == "custom" + Test.@test DR._default_basename("", false, false) == "private" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_build_page_path" begin + Test.@test DR._build_page_path("api", "public") == "api/public" + Test.@test DR._build_page_path(".", "public") == "public" + Test.@test DR._build_page_path("", "public") == "public" + Test.@test DR._build_page_path("", "") == "" + Test.@test DR._build_page_path(".", "") == "" + Test.@test DR._build_page_path("dir", "") == "dir/" + Test.@test DR._build_page_path("a/b/c", "file.md") == "a/b/c/file.md" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_parse_primary_modules" begin + d = DR._parse_primary_modules([DocumenterReferenceConfigTestMod => @__FILE__]) + Test.@test d[DocumenterReferenceConfigTestMod] == [abspath(@__FILE__)] + end + + return nothing +end + +end # module + +test_documenter_reference_config_helpers() = TestDocumenterReferenceConfigHelpers.test_documenter_reference_config_helpers() diff --git a/test/suite/extensions/test_documenter_reference_page_building.jl b/test/suite/extensions/test_documenter_reference_page_building.jl new file mode 100644 index 00000000..ba36224c --- /dev/null +++ b/test/suite/extensions/test_documenter_reference_page_building.jl @@ -0,0 +1,354 @@ +module TestDocumenterReferencePageBuilding + +import Test +import CTBase +import Documenter + +const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) +const DR = DocumenterReference + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# Test module for page building +module DocumenterReferencePageTestMod + """ + Docstring for the main test module. + """ + myfun(x) = x + keep(x) = x + skip(x) = x + no_doc(x) = x + + """ + Test submodule. + """ + module SubModule end +end +using .DocumenterReferencePageTestMod + +module DRExternalTestMod + extfun(x::Int) = x + extfun(x::String) = length(x) +end + +function test_documenter_reference_page_building() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_collect_module_docstrings" begin + DR.reset_config!() + + config = DR._Config( + DocumenterReferencePageTestMod, + "api", + Dict(DocumenterReferencePageTestMod => Any[]), + identity, + Set{Symbol}(), + true, + true, + "API", + "API", + String[], + "api", + false, + Module[], + "", + "", + "", + "", + ) + + # Test with private symbols (which have docstrings) + symbols = redirect_stderr(devnull) do + DR._exported_symbols(DocumenterReferencePageTestMod).private + end + docs = redirect_stderr(devnull) do + DR._collect_module_docstrings(config, symbols) + end + + # Should collect docstrings for documented symbols + Test.@test !isempty(docs) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Page content builders" begin + modules_str = "ModA, ModB" + module_contents_private = [ + (DocumenterReferencePageTestMod, String[], ["priv_a"]), + (DocumenterReferencePageTestMod, String[], ["priv_b1", "priv_b2"]), + ] + # Test with is_split=false (single page) + overview_priv, docs_priv = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents_private, false + ) + end + Test.@test occursin("Private API", overview_priv) + Test.@test occursin("ModA, ModB", overview_priv) + Test.@test !isempty(docs_priv) + Test.@test any(occursin("priv_a", s) for s in docs_priv) + Test.@test any(occursin("priv_b1", s) for s in docs_priv) + + module_contents_public = [ + (DocumenterReferencePageTestMod, ["pub_a"], String[]), + (DocumenterReferencePageTestMod, String[], String[]), + ] + # Test with is_split=false (single page) + overview_pub, docs_pub = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents_public, false + ) + end + Test.@test occursin("Public API", overview_pub) + Test.@test occursin("ModA, ModB", overview_pub) + Test.@test !isempty(docs_pub) + Test.@test any(occursin("pub_a", s) for s in docs_pub) + + module_contents_combined = [(DocumenterReferencePageTestMod, ["pub_a"], ["priv_a"])] + overview_comb, docs_comb = redirect_stderr(devnull) do + DR._build_combined_page_content( + modules_str, module_contents_combined + ) + end + Test.@test occursin("API reference", overview_comb) + Test.@test any(occursin("Public API", s) for s in docs_comb) + Test.@test any(occursin("Private API", s) for s in docs_comb) + Test.@test any(occursin("pub_a", s) for s in docs_comb) + Test.@test any(occursin("priv_a", s) for s in docs_comb) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Page content builders with is_split parameter" begin + modules_str = "TestModule" + + # Test private page with is_split=false (single page) + module_contents_private = [(DocumenterReferencePageTestMod, String[], ["priv_doc"])] + + overview_priv_single, docs_priv_single = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents_private, false + ) + end + Test.@test occursin("# Private API", overview_priv_single) + Test.@test !occursin("# Private\n", overview_priv_single) + Test.@test occursin("non-exported", overview_priv_single) + + # Test private page with is_split=true (split page) + overview_priv_split, docs_priv_split = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents_private, true + ) + end + Test.@test occursin("# Private API", overview_priv_split) + Test.@test occursin("non-exported", overview_priv_split) + + # Test public page with is_split=false (single page) + module_contents_public = [(DocumenterReferencePageTestMod, ["pub_doc"], String[])] + + overview_pub_single, docs_pub_single = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents_public, false + ) + end + Test.@test occursin("# Public API", overview_pub_single) + Test.@test occursin("exported", overview_pub_single) + + # Test public page with is_split=true (split page) + overview_pub_split, docs_pub_split = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents_public, true + ) + end + Test.@test occursin("# Public API", overview_pub_split) + Test.@test occursin("exported", overview_pub_split) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Title consistency across different page types" begin + modules_str = "MyModule" + module_contents = [(DocumenterReferencePageTestMod, ["pub"], ["priv"])] + + # Single private page should have "Private API" title + overview_priv, _ = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents, false + ) + end + Test.@test occursin("# Private API", overview_priv) + + # Single public page should have "Public API" title + overview_pub, _ = redirect_stderr(devnull) do + DR._build_public_page_content(modules_str, module_contents, false) + end + Test.@test occursin("# Public API", overview_pub) + + # Split private page should have "Private API" title + overview_priv_split, _ = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents, true + ) + end + Test.@test occursin("# Private API", overview_priv_split) + + # Split public page should have "Public API" title + overview_pub_split, _ = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents, true + ) + end + Test.@test occursin("# Public API", overview_pub_split) + + # Combined page should have "API reference" title + overview_comb, _ = redirect_stderr(devnull) do + DR._build_combined_page_content(modules_str, module_contents) + end + Test.@test occursin("# API reference", overview_comb) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Custom titles for API pages" begin + modules_str = "TestModule" + module_contents = [(DocumenterReferencePageTestMod, ["pub_doc"], ["priv_doc"])] + + # Test custom title for private page (single) + overview_priv_custom, _ = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents, false; custom_title="Internal API" + ) + end + Test.@test occursin("# Internal API", overview_priv_custom) + Test.@test !occursin("# Private API", overview_priv_custom) + + # Test custom title for public page (single) + overview_pub_custom, _ = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents, false; custom_title="Exported API" + ) + end + Test.@test occursin("# Exported API", overview_pub_custom) + Test.@test !occursin("# Public API", overview_pub_custom) + + # Test custom title for private page (split) + overview_priv_split_custom, _ = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents, true; custom_title="Internal" + ) + end + Test.@test occursin("# Internal", overview_priv_split_custom) + Test.@test !occursin("# Private API", overview_priv_split_custom) + + # Test custom title for public page (split) + overview_pub_split_custom, _ = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents, true; custom_title="Exported" + ) + end + Test.@test occursin("# Exported", overview_pub_split_custom) + Test.@test !occursin("# Public API", overview_pub_split_custom) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Custom descriptions for API pages" begin + modules_str = "TestModule" + module_contents = [(DocumenterReferencePageTestMod, ["pub_doc"], ["priv_doc"])] + + # Test custom description for private page + custom_desc_priv = "This page documents internal implementation details." + overview_priv_desc, _ = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents, false; custom_description=custom_desc_priv + ) + end + Test.@test occursin(custom_desc_priv, overview_priv_desc) + Test.@test !occursin("non-exported", overview_priv_desc) + + # Test custom description for public page + custom_desc_pub = "This page documents the public interface for end users." + overview_pub_desc, _ = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents, false; custom_description=custom_desc_pub + ) + end + Test.@test occursin(custom_desc_pub, overview_pub_desc) + Test.@test !occursin("exported", overview_pub_desc) || + occursin(custom_desc_pub, overview_pub_desc) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Combined custom title and description" begin + modules_str = "TestModule" + module_contents = [(DocumenterReferencePageTestMod, ["pub_doc"], ["priv_doc"])] + + # Test both custom title and description together + custom_title = "Developer Reference" + custom_desc = "Advanced documentation for contributors and maintainers." + + overview, _ = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, + module_contents, + false; + custom_title=custom_title, + custom_description=custom_desc, + ) + end + + Test.@test occursin("# Developer Reference", overview) + Test.@test occursin(custom_desc, overview) + Test.@test !occursin("# Private API", overview) + Test.@test !occursin("non-exported", overview) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Empty customization uses defaults" begin + modules_str = "TestModule" + module_contents = [(DocumenterReferencePageTestMod, ["pub_doc"], ["priv_doc"])] + + # Empty strings should use default behavior + overview_priv_empty, _ = redirect_stderr(devnull) do + DR._build_private_page_content( + modules_str, module_contents, false; custom_title="", custom_description="" + ) + end + Test.@test occursin("# Private API", overview_priv_empty) + Test.@test occursin("non-exported", overview_priv_empty) + + overview_pub_empty, _ = redirect_stderr(devnull) do + DR._build_public_page_content( + modules_str, module_contents, false; custom_title="", custom_description="" + ) + end + Test.@test occursin("# Public API", overview_pub_empty) + Test.@test occursin("exported", overview_pub_empty) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "external_modules_to_document" begin + current_module = DocumenterReferencePageTestMod + modules = Dict(current_module => String[]) + sort_by(x) = x + source_files = [abspath(@__FILE__)] + + config = DR._Config( + current_module, + "api_ext", + modules, + sort_by, + Set{Symbol}(), + true, + true, + "Ext API", + "Ext API", + source_files, + "api_ext", + false, + [DRExternalTestMod], + "", + "", + "", + "", + ) + + private_docs = redirect_stderr(devnull) do + DR._collect_private_docstrings(config, Pair{Symbol,DR.DocType}[]) + end + Test.@test !isempty(private_docs) + Test.@test any(occursin("DRExternalTestMod.extfun", s) for s in private_docs) + end + + return nothing +end + +end # module + +test_documenter_reference_page_building() = TestDocumenterReferencePageBuilding.test_documenter_reference_page_building() diff --git a/test/suite/extensions/test_documenter_reference_pipeline.jl b/test/suite/extensions/test_documenter_reference_pipeline.jl new file mode 100644 index 00000000..b305998a --- /dev/null +++ b/test/suite/extensions/test_documenter_reference_pipeline.jl @@ -0,0 +1,66 @@ +module TestDocumenterReferencePipeline + +import Test +import CTBase +import CTBase.Extensions: Extensions +import Documenter + +const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) +const DR = DocumenterReference + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# Test module for pipeline tests +module DocumenterReferencePipelineTestMod + myfun(x) = x +end +using .DocumenterReferencePipelineTestMod + +function test_documenter_reference_pipeline() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Documenter.Selectors.order for APIBuilder" begin + Test.@test Documenter.Selectors.order(DR.APIBuilder) == 0.5 + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "APIBuilder runner integration" begin + DR.reset_config!() + + pages = redirect_stdout(devnull) do + redirect_stderr(devnull) do + Extensions.automatic_reference_documentation( + Extensions.DocumenterReferenceTag(); + subdirectory="api_integration", + primary_modules=[DocumenterReferencePipelineTestMod], + public=true, + private=true, + title="Integration API", + ) + end + end + + Test.@test !isempty(DR.CONFIG) + + doc = redirect_stdout(devnull) do + redirect_stderr(devnull) do + Documenter.Document(; + root=pwd(), source="_test_docs_src", build="_test_docs_build", remotes=nothing + ) + end + end + + redirect_stdout(devnull) do + redirect_stderr(devnull) do + Documenter.Selectors.runner(DR.APIBuilder, doc) + end + end + + Test.@test !isempty(doc.blueprint.pages) + Test.@test any(endswith(k, "api_private.md") for k in keys(doc.blueprint.pages)) + end + + return nothing +end + +end # module + +test_documenter_reference_pipeline() = TestDocumenterReferencePipeline.test_documenter_reference_pipeline() diff --git a/test/suite/extensions/test_documenter_reference_symbol_helpers.jl b/test/suite/extensions/test_documenter_reference_symbol_helpers.jl new file mode 100644 index 00000000..e799d924 --- /dev/null +++ b/test/suite/extensions/test_documenter_reference_symbol_helpers.jl @@ -0,0 +1,249 @@ +module TestDocumenterReferenceSymbolHelpers + +import Test +import CTBase +import Documenter + +const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) +const DR = DocumenterReference + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# Test module for symbol helpers +module DocumenterReferenceSymbolTestMod + """ + Simple documented function. + """ + myfun(x) = x + + """ + Function to keep. + """ + keep(x) = x + + """ + Function to skip. + """ + skip(x) = x + + # No docstring + no_doc(x) = x + + """ + Test submodule. + """ + module SubModule end + + abstract type AbstractFoo end + + struct Foo <: AbstractFoo + x::Int + end + + const MYCONST = 42 +end +using .DocumenterReferenceSymbolTestMod + +module DRNoDocModule + module Inner end +end + +function test_documenter_reference_symbol_helpers() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_classify_symbol" begin + Test.@test DR._classify_symbol(nothing, "@mymacro") == DR.DOCTYPE_MACRO + Test.@test DR._classify_symbol(DocumenterReferenceSymbolTestMod.SubModule, "SubModule") == + DR.DOCTYPE_MODULE + Test.@test DR._classify_symbol(DocumenterReferenceSymbolTestMod.AbstractFoo, "AbstractFoo") == + DR.DOCTYPE_ABSTRACT_TYPE + Test.@test DR._classify_symbol(DocumenterReferenceSymbolTestMod.Foo, "Foo") == + DR.DOCTYPE_STRUCT + Test.@test DR._classify_symbol(DocumenterReferenceSymbolTestMod.myfun, "myfun") == + DR.DOCTYPE_FUNCTION + Test.@test DR._classify_symbol(DocumenterReferenceSymbolTestMod.MYCONST, "MYCONST") == + DR.DOCTYPE_CONSTANT + Test.@test DR._classify_symbol(42, "fortytwo") == DR.DOCTYPE_CONSTANT + Test.@test DR._classify_symbol("hello", "greeting") == DR.DOCTYPE_CONSTANT + Test.@test DR._classify_symbol([1, 2, 3], "myarray") == DR.DOCTYPE_CONSTANT + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_to_string" begin + Test.@test DR._to_string(DR.DOCTYPE_ABSTRACT_TYPE) == "abstract type" + Test.@test DR._to_string(DR.DOCTYPE_CONSTANT) == "constant" + Test.@test DR._to_string(DR.DOCTYPE_FUNCTION) == "function" + Test.@test DR._to_string(DR.DOCTYPE_MACRO) == "macro" + Test.@test DR._to_string(DR.DOCTYPE_MODULE) == "module" + Test.@test DR._to_string(DR.DOCTYPE_STRUCT) == "struct" + + # Verify all enum values have string representations + for dt in instances(DR.DocType) + Test.@test DR._to_string(dt) isa String + Test.@test length(DR._to_string(dt)) > 0 + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_get_source_file" begin + path = DR._get_source_file(DocumenterReferenceSymbolTestMod, :myfun, DR.DOCTYPE_FUNCTION) + Test.@test path === abspath(@__FILE__) + + # No docstring => should use method-based resolution + path2 = DR._get_source_file( + DocumenterReferenceSymbolTestMod, :no_doc, DR.DOCTYPE_FUNCTION + ) + Test.@test path2 === abspath(@__FILE__) + + # Missing symbol should be caught and return nothing + missing_path = DR._get_source_file( + DocumenterReferenceSymbolTestMod, :__does_not_exist__, DR.DOCTYPE_FUNCTION + ) + Test.@test missing_path === nothing + + const_path = DR._get_source_file( + DocumenterReferenceSymbolTestMod, :MYCONST, DR.DOCTYPE_CONSTANT + ) + Test.@test const_path === nothing + + # Struct case - should find source via constructor methods + struct_path = DR._get_source_file( + DocumenterReferenceSymbolTestMod, :Foo, DR.DOCTYPE_STRUCT + ) + Test.@test struct_path === nothing || + endswith(struct_path, "test_documenter_reference_symbol_helpers.jl") + + # Abstract type case - typically returns nothing + abstract_path = DR._get_source_file( + DocumenterReferenceSymbolTestMod, :AbstractFoo, DR.DOCTYPE_ABSTRACT_TYPE + ) + Test.@test abstract_path === nothing + + # Module case - modules cannot reliably determine source + mod_path = DR._get_source_file( + DocumenterReferenceSymbolTestMod, :SubModule, DR.DOCTYPE_MODULE + ) + Test.@test mod_path === nothing + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_get_source_from_methods" begin + # For built-in operators like +, source detection behavior may vary by Julia version. + # We just verify the function runs without error and returns String or nothing. + result = DR._get_source_from_methods(+) + Test.@test result === nothing || result isa String + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_has_documentation" begin + modules = Dict(DRNoDocModule.Inner => Any[]) + Test.@test DR._has_documentation(DRNoDocModule, :Inner, DR.DOCTYPE_MODULE, modules) == + true + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_iterate_over_symbols filtering" begin + current_module = DocumenterReferenceSymbolTestMod + modules = Dict(current_module => Any[]) + exclude = Set{Symbol}([:skip]) + sort_by(x) = x + source_files = String[] + + config = DR._Config( + current_module, + "api", + modules, + sort_by, + exclude, + true, + true, + "API Reference", + "API Reference", + source_files, + "api", + false, + Module[], + "", + "", + "", + "", + ) + + symbols = [ + :keep => DR.DOCTYPE_FUNCTION, + :skip => DR.DOCTYPE_FUNCTION, + :no_doc => DR.DOCTYPE_FUNCTION, + ] + + seen = Symbol[] + redirect_stdout(devnull) do + redirect_stderr(devnull) do + DR._iterate_over_symbols(config, symbols) do key, type + return push!(seen, key) + end + end + end + + Test.@test :keep in seen + Test.@test :skip ∉ seen + Test.@test :no_doc ∉ seen + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_iterate_over_symbols with empty list" begin + config = DR._Config( + DocumenterReferenceSymbolTestMod, + "api", + Dict(DocumenterReferenceSymbolTestMod => Any[]), + identity, + Set{Symbol}(), + true, + true, + "API", + "API", + String[], + "api", + false, + Module[], + "", + "", + "", + "", + ) + seen = Symbol[] + redirect_stdout(devnull) do + redirect_stderr(devnull) do + DR._iterate_over_symbols(config, Pair{Symbol,DR.DocType}[]) do key, type + return push!(seen, key) + end + end + end + Test.@test isempty(seen) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_exported_symbols classification" begin + result = DR._exported_symbols(DocumenterReferenceSymbolTestMod) + + # Check structure: should return NamedTuple with exported and private fields + Test.@test haskey(result, :exported) + Test.@test haskey(result, :private) + + # Check that exported list contains expected symbols (none exported in test module) + Test.@test result.exported isa Vector + + # Check that private list contains our test symbols + private_symbols = Dict(result.private) + Test.@test haskey(private_symbols, :myfun) + Test.@test private_symbols[:myfun] == DR.DOCTYPE_FUNCTION + Test.@test haskey(private_symbols, :keep) + Test.@test haskey(private_symbols, :skip) + Test.@test haskey(private_symbols, :no_doc) + Test.@test haskey(private_symbols, :SubModule) + Test.@test private_symbols[:SubModule] == DR.DOCTYPE_MODULE + Test.@test haskey(private_symbols, :AbstractFoo) + Test.@test private_symbols[:AbstractFoo] == DR.DOCTYPE_ABSTRACT_TYPE + Test.@test haskey(private_symbols, :Foo) + Test.@test private_symbols[:Foo] == DR.DOCTYPE_STRUCT + Test.@test haskey(private_symbols, :MYCONST) + Test.@test private_symbols[:MYCONST] == DR.DOCTYPE_CONSTANT + end + + return nothing +end + +end # module + +test_documenter_reference_symbol_helpers() = TestDocumenterReferenceSymbolHelpers.test_documenter_reference_symbol_helpers() diff --git a/test/suite/extensions/test_documenter_reference_type_formatting.jl b/test/suite/extensions/test_documenter_reference_type_formatting.jl new file mode 100644 index 00000000..439637de --- /dev/null +++ b/test/suite/extensions/test_documenter_reference_type_formatting.jl @@ -0,0 +1,120 @@ +module TestDocumenterReferenceTypeFormatting + +import Test +import CTBase +import Documenter + +const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) +const DR = DocumenterReference + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# Test module for type formatting +module DRTypeFormatTestMod + struct Simple end + struct Parametric{T} end + struct WithValue{T,N} end +end + +module DRUnionAllTestMod + f(x::T) where {T} = x +end + +function test_documenter_reference_type_formatting() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Type formatting helpers" begin + Test.@test DR._format_datatype_for_docs(UInt) == "::UInt" + + # Parametric types without concrete params are UnionAll, use _format_type_for_docs + param_result = DR._format_type_for_docs(DRTypeFormatTestMod.Parametric) + Test.@test occursin("Parametric", param_result) + + # Concrete params => kept (WithValue{Int,2} is a DataType) + concrete_result = DR._format_datatype_for_docs(DRTypeFormatTestMod.WithValue{Int,2}) + Test.@test occursin("WithValue", concrete_result) + Test.@test occursin("Int", concrete_result) + Test.@test occursin("2", concrete_result) + + # Fallback branch for non-type inputs + Test.@test DR._format_type_for_docs(1) == "::1" + + # TypeVar formatting in _format_type_param - need to unwrap UnionAll first + unwrapped = Base.unwrap_unionall(DRTypeFormatTestMod.Parametric) + tv = unwrapped.parameters[1] + Test.@test tv isa TypeVar + Test.@test DR._format_type_param(tv) == "T" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_method_signature_string handles UnionAll" begin + m = first(methods(DRUnionAllTestMod.f)) + s = DR._method_signature_string(m, DRUnionAllTestMod, :f) + Test.@test occursin("DRUnionAllTestMod.f", s) + Test.@test occursin("T", s) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_type_for_docs edge cases" begin + # Test Union types formatting + union_result = DR._format_type_for_docs(Union{Int,String}) + Test.@test occursin("Union", union_result) + Test.@test occursin("Int", union_result) + Test.@test occursin("String", union_result) + + # Test simple non-DataType fallback + Test.@test DR._format_type_for_docs(Any) == "::Any" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_datatype_for_docs with TypeVar" begin + # Test parametric type with concrete parameters + concrete_result = DR._format_datatype_for_docs(Vector{Int}) + Test.@test occursin("Vector", concrete_result) || occursin("Array", concrete_result) + Test.@test occursin("Int", concrete_result) + + # Test parametric type - the function strips parameters when TypeVar present + Test.@test DR._format_datatype_for_docs(Int) == "::Int" + Test.@test DR._format_datatype_for_docs(String) == "::String" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_type_param edge cases" begin + # Test Type parameter + type_result = DR._format_type_param(Int) + Test.@test type_result == "Int" # Should strip leading :: + + # Test value parameter - e.g., for sized arrays + Test.@test DR._format_type_param(3) == "3" + Test.@test DR._format_type_param(42) == "42" + + # Test with complex type + param_fmt = DR._format_type_param(DRTypeFormatTestMod.Simple) + Test.@test occursin("DRTypeFormatTestMod.Simple", param_fmt) + + value_param_fmt = DR._format_type_param(3) + Test.@test value_param_fmt == "3" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_format_type_for_docs and helpers" begin + simple_str = DR._format_type_for_docs(DRTypeFormatTestMod.Simple) + Test.@test startswith(simple_str, "::") + Test.@test occursin("DRTypeFormatTestMod.Simple", simple_str) + + param_type = DRTypeFormatTestMod.Parametric{DRTypeFormatTestMod.Simple} + param_str = DR._format_type_for_docs(param_type) + Test.@test occursin("Parametric", param_str) + Test.@test occursin("Simple", param_str) + + union_str = DR._format_type_for_docs(Union{DRTypeFormatTestMod.Simple,Nothing}) + Test.@test occursin("Union", union_str) + Test.@test occursin("Simple", union_str) + Test.@test occursin("Nothing", union_str) + + value_type = DRTypeFormatTestMod.WithValue{Int,3} + value_str = DR._format_type_for_docs(value_type) + Test.@test occursin("WithValue", value_str) + Test.@test occursin("3", value_str) + end + + return nothing +end + +end # module + +test_documenter_reference_type_formatting() = TestDocumenterReferenceTypeFormatting.test_documenter_reference_type_formatting() diff --git a/test/suite/extensions/test_documenter_reference_types.jl b/test/suite/extensions/test_documenter_reference_types.jl new file mode 100644 index 00000000..e0feffcc --- /dev/null +++ b/test/suite/extensions/test_documenter_reference_types.jl @@ -0,0 +1,66 @@ +module TestDocumenterReferenceTypes + +import Test +import CTBase +import Documenter + +const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) +const DR = DocumenterReference + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_documenter_reference_types() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "DocType enum" begin + # Test that all enum values are valid + for dt in instances(DR.DocType) + Test.@test dt isa DR.DocType + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "DOCTYPE_NAMES" begin + Test.@test haskey(DR.DOCTYPE_NAMES, DR.DOCTYPE_MACRO) + Test.@test haskey(DR.DOCTYPE_NAMES, DR.DOCTYPE_MODULE) + Test.@test haskey(DR.DOCTYPE_NAMES, DR.DOCTYPE_ABSTRACT_TYPE) + Test.@test haskey(DR.DOCTYPE_NAMES, DR.DOCTYPE_STRUCT) + Test.@test haskey(DR.DOCTYPE_NAMES, DR.DOCTYPE_FUNCTION) + Test.@test haskey(DR.DOCTYPE_NAMES, DR.DOCTYPE_CONSTANT) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "DOCTYPE_ORDER" begin + Test.@test DR.DOCTYPE_ORDER isa Dict{DR.DocType, Int} + Test.@test length(DR.DOCTYPE_ORDER) > 0 + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_Config struct" begin + config = DR._Config( + Main, + "api", + Dict(Main => Any[]), + identity, + Set{Symbol}(), + true, + true, + "API", + "API", + String[], + "api", + false, + Module[], + "", + "", + "", + "", + ) + Test.@test config.current_module === Main + Test.@test config.subdirectory == "api" + Test.@test config.public == true + Test.@test config.private == true + end + + return nothing +end + +end # module + +test_documenter_reference_types() = TestDocumenterReferenceTypes.test_documenter_reference_types() diff --git a/test/suite/extensions/test_extension_stubs.jl b/test/suite/extensions/test_extension_stubs.jl new file mode 100644 index 00000000..7c7ee002 --- /dev/null +++ b/test/suite/extensions/test_extension_stubs.jl @@ -0,0 +1,45 @@ +module TestExtensionStubs + +import Test +import CTBase.Extensions +import CTBase.Exceptions + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# Fake tag types for extension stub testing (testing-creation.md §6). +# These subtype the abstract tags but are unknown to any extension, +# so the stubs always throw ExtensionError regardless of which extensions are loaded. +struct FakeDocumenterReferenceTag <: Extensions.AbstractDocumenterReferenceTag end +struct FakeCoveragePostprocessingTag <: Extensions.AbstractCoveragePostprocessingTag end +struct FakeTestRunnerTag <: Extensions.AbstractTestRunnerTag end + +function test_extension_stubs() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Extension Function Error Handling" begin + # Test automatic_reference_documentation error + Test.@testset "automatic_reference_documentation" begin + Test.@test_throws Exceptions.ExtensionError Extensions.automatic_reference_documentation( + FakeDocumenterReferenceTag() + ) + end + + # Test postprocess_coverage error + Test.@testset "postprocess_coverage" begin + Test.@test_throws Exceptions.ExtensionError Extensions.postprocess_coverage( + FakeCoveragePostprocessingTag() + ) + end + + # Test run_tests error + Test.@testset "run_tests" begin + Test.@test_throws Exceptions.ExtensionError Extensions.run_tests(FakeTestRunnerTag()) + end + end + + return nothing +end + +end # module + +# Export to outer scope for TestRunner +test_extension_stubs() = TestExtensionStubs.test_extension_stubs() diff --git a/test/suite/extensions/test_extensions_enriched.jl b/test/suite/extensions/test_extensions_enriched.jl deleted file mode 100644 index 492f08ef..00000000 --- a/test/suite/extensions/test_extensions_enriched.jl +++ /dev/null @@ -1,109 +0,0 @@ -module TestExtensionsEnriched - -import Test -import CTBase.Extensions -import CTBase.Exceptions - -const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true -const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true - -# Fake tag types for extension stub testing (testing-creation.md §6). -# These subtype the abstract tags but are unknown to any extension, -# so the stubs always throw ExtensionError regardless of which extensions are loaded. -struct FakeDocumenterReferenceTag <: Extensions.AbstractDocumenterReferenceTag end -struct FakeCoveragePostprocessingTag <: Extensions.AbstractCoveragePostprocessingTag end -struct FakeTestRunnerTag <: Extensions.AbstractTestRunnerTag end - -function test_extensions_enriched() - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Enriched Extension Errors" begin - - # ==================================================================== - # UNIT TESTS - Extension Error Contract - # ==================================================================== - - Test.@testset "ExtensionError Contract Implementation" begin - # Test constructor throws if no dependencies provided - Test.@test_throws Exceptions.PreconditionError Exceptions.ExtensionError() - - # Test enriched ExtensionError creation - e = Exceptions.ExtensionError( - :Documenter, - :Markdown; - message="to generate documentation", - feature="automatic documentation", - context="reference generation", - ) - - Test.@test e isa Exceptions.ExtensionError - Test.@test e.weakdeps == (:Documenter, :Markdown) - Test.@test e.msg == "missing dependencies to generate documentation" - Test.@test e.feature == "automatic documentation" - Test.@test e.context == "reference generation" - end - - # ==================================================================== - # INTEGRATION TESTS - Extension Functions - # ==================================================================== - - Test.@testset "Extension Function Error Handling" begin - # Test automatic_reference_documentation error - Test.@testset "automatic_reference_documentation" begin - Test.@test_throws Exceptions.ExtensionError Extensions.automatic_reference_documentation( - FakeDocumenterReferenceTag() - ) - end - - # Test postprocess_coverage error - Test.@testset "postprocess_coverage" begin - Test.@test_throws Exceptions.ExtensionError Extensions.postprocess_coverage( - FakeCoveragePostprocessingTag() - ) - end - - # Test run_tests error - Test.@testset "run_tests" begin - Test.@test_throws Exceptions.ExtensionError Extensions.run_tests(FakeTestRunnerTag()) - end - end - - # ==================================================================== - # ERROR TESTS - Exception Quality - # ==================================================================== - - Test.@testset "ExtensionError Constructor Validation" begin - Test.@testset "No dependencies provided" begin - Test.@test_throws Exceptions.PreconditionError Exceptions.ExtensionError() - - try - Exceptions.ExtensionError() - Test.@test false # Should not reach here - catch e - Test.@test e isa Exceptions.PreconditionError - Test.@test occursin("weak dependence", e.msg) - Test.@test occursin("ExtensionError called without dependencies", e.reason) - end - end - - Test.@testset "Single dependency" begin - e = Exceptions.ExtensionError(:MyExt) - Test.@test e isa Exceptions.ExtensionError - Test.@test e.weakdeps == (:MyExt,) - Test.@test e.msg == "missing dependencies" - end - - Test.@testset "Multiple dependencies with message" begin - e = Exceptions.ExtensionError(:Ext1, :Ext2; message="to enable feature X") - Test.@test e isa Exceptions.ExtensionError - Test.@test e.weakdeps == (:Ext1, :Ext2) - Test.@test e.msg == "missing dependencies to enable feature X" - end - end - end - - return nothing -end - -end # module - -# Export to outer scope for TestRunner -test_extensions_enriched() = TestExtensionsEnriched.test_extensions_enriched() diff --git a/test/suite/extensions/test_testrunner.jl b/test/suite/extensions/test_testrunner.jl index df139ced..998d9284 100644 --- a/test/suite/extensions/test_testrunner.jl +++ b/test/suite/extensions/test_testrunner.jl @@ -33,1166 +33,6 @@ function test_testrunner() Test.@test err.weakdeps == (:Test,) end - # ============================================================================ - # UNIT TESTS - _parse_test_args - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_parse_test_args" begin - # Access the private function via the loaded extension module - parse_args = TestRunner._parse_test_args - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "empty args with defaults" begin - (sel, all_flag, dry_flag) = parse_args(String[]) - Test.@test sel == String[] - Test.@test all_flag == false - - Test.@test dry_flag == false - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "single test name" begin - (sel, _, _) = parse_args(["utils"]) - Test.@test sel == ["utils"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "multiple test names" begin - (sel, _, _) = parse_args(["utils", "default"]) - Test.@test sel == ["utils", "default"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "coverage flags are filtered out" begin - (sel, _, _) = parse_args(["coverage=true"]) - Test.@test sel == String[] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "CLI flags -a / --all" begin - # -a should set run_all=true - (_, run_all, _) = parse_args(["-a"]) - Test.@test run_all == true - - (_, run_all, _) = parse_args(["--all"]) - Test.@test run_all == true - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "CLI flags -n / --dryrun" begin - # -n should set dry_run=true - (_, _, dry_run) = parse_args(["-n"]) - Test.@test dry_run == true - - (_, _, dry_run) = parse_args(["--dryrun"]) - Test.@test dry_run == true - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "mixed flags and tests" begin - (sel, run_all, dry_run) = parse_args(["utils", "-n", "--all"]) - Test.@test sel == ["utils"] - Test.@test run_all == true - Test.@test dry_run == true - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "test/ prefix is stripped" begin - (sel, _, _) = parse_args(["test/suite/foo"]) - Test.@test sel == ["suite/foo"] - - (sel, _, _) = parse_args(["test/suite"]) - Test.@test sel == ["suite"] - - # No prefix → unchanged - (sel, _, _) = parse_args(["suite/foo"]) - Test.@test sel == ["suite/foo"] - - # Exact "test" (no slash) → unchanged - (sel, _, _) = parse_args(["test"]) - Test.@test sel == ["test"] - - # Multiple args with mixed prefixes - (sel, _, _) = parse_args(["test/suite/a", "suite/b"]) - Test.@test sel == ["suite/a", "suite/b"] - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_strip_test_prefix" begin - strip_prefix = TestRunner._strip_test_prefix - - Test.@test strip_prefix("test/suite/foo") == "suite/foo" - Test.@test strip_prefix("test/a") == "a" - Test.@test strip_prefix("suite/foo") == "suite/foo" - Test.@test strip_prefix("test") == "test" - Test.@test strip_prefix("testing/foo") == "testing/foo" - Test.@test strip_prefix("test/") == "" - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "run_tests (dry run)" begin - mktempdir() do temp_dir - touch(joinpath(temp_dir, "test_dummy.jl")) - - # Use Pipe for redirect_stdout (IOBuffer not supported in Julia 1.12+) - old_stdout = stdout - rd, wr = redirect_stdout() - try - Extensions.run_tests(; - args=["--dryrun", "test_dummy.jl"], - testset_name="DryRun", - available_tests=("*.jl",), - filename_builder=identity, - funcname_builder=identity, - eval_mode=false, - verbose=false, - showtiming=false, - test_dir=temp_dir, - ) - finally - redirect_stdout(old_stdout) - close(wr) - end - s = read(rd, String) - - Test.@test occursin("Dry run", s) - Test.@test occursin("test_dummy.jl", s) - end - end - - # ============================================================================ - # UNIT TESTS - _select_tests - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_select_tests" begin - select_tests = TestRunner._select_tests - - # Mock filename builder for testing - test_builder_sym = name -> Symbol(:test_, name) - test_builder_str = name -> "test_" * String(name) - - # Mocking readdir requires a temporary directory or we mock the fs behavior? - # Easier: we test the filtering logic with a custom test_dir. - # Let's create a temporary directory structure for testing. - - mktempdir() do temp_dir - # Create some dummy test files - touch(joinpath(temp_dir, "test_utils.jl")) - touch(joinpath(temp_dir, "test_core.jl")) - touch(joinpath(temp_dir, "runtests.jl")) # Should be ignored - touch(joinpath(temp_dir, "readme.md")) # Should be ignored - - # ========================================================== - # Scenario 1: Auto-discovery (available_tests empty) - # ========================================================== - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Auto-discovery (empty available_tests)" begin - # Empty args -> run all .jl files (excluding runtests.jl) - # Names derived as basenames - sel = select_tests(String[], Symbol[], false, identity; test_dir=temp_dir) - Test.@test sort(sel) == ["test_core.jl", "test_utils.jl"] - - # Run all (-a) -> same result - sel = select_tests(String[], Symbol[], true, identity; test_dir=temp_dir) - Test.@test sort(sel) == ["test_core.jl", "test_utils.jl"] - - # Globbing: select only utils - sel = select_tests( - ["test_utils"], Symbol[], false, identity; test_dir=temp_dir - ) - Test.@test sel == ["test_utils.jl"] - - # Globbing: pattern matching - sel = select_tests( - ["test_c*"], Symbol[], false, identity; test_dir=temp_dir - ) - Test.@test sel == ["test_core.jl"] - - # Globbing: match filename? - sel = select_tests( - ["test_core_jl"], Symbol[], false, identity; test_dir=temp_dir - ) - # "^test_core_jl$" doesn't match "test_core.jl" or "test_core" - Test.@test isempty(sel) - - sel = select_tests( - ["test_core.jl"], Symbol[], false, identity; test_dir=temp_dir - ) - Test.@test sel == ["test_core.jl"] - end - - # ========================================================== - # Scenario 2: With available_tests - # ========================================================== - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "With available_tests" begin - available = [:utils, :core] - - # Note: TestRunner checks if file exists via builder - # test_utils.jl exists -> :utils is valid - # test_core.jl exists -> :core is valid - - # Empty args -> run all valid available tests - sel = select_tests( - String[], available, false, test_builder_sym; test_dir=temp_dir - ) - Test.@test sort(sel) == [:core, :utils] - - # Selection - sel = select_tests( - ["utils"], available, false, test_builder_sym; test_dir=temp_dir - ) - Test.@test sel == [:utils] - - # Globbing over available names - # available test :core, file: test_core.jl - sel = select_tests( - ["c*"], available, false, test_builder_sym; test_dir=temp_dir - ) - Test.@test sel == [:core] - - # Globbing over filename without extension - # available test :core, file: test_core.jl - sel = select_tests( - ["test_core"], available, false, test_builder_sym; test_dir=temp_dir - ) - Test.@test sel == [:core] - - # Builder may return String - sel = select_tests( - ["core"], available, false, test_builder_str; test_dir=temp_dir - ) - Test.@test sel == [:core] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol in subdirectory" begin - mkpath(joinpath(temp_dir, "suite_src")) - touch(joinpath(temp_dir, "suite_src", "test_utils.jl")) - - available = [:utils] - - sel = select_tests( - String[], available, false, test_builder_sym; test_dir=temp_dir - ) - Test.@test sel == [:utils] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests may be directories" begin - mkpath(joinpath(temp_dir, "suite")) - touch(joinpath(temp_dir, "suite", "test_a.jl")) - touch(joinpath(temp_dir, "suite", "test_b.jl")) - - available = TestRunner.TestSpec["suite"] - sel = select_tests(String[], available, false, identity; test_dir=temp_dir) - Test.@test sel == ["suite/test_a.jl", "suite/test_b.jl"] - - sel = select_tests( - ["suite/test_a"], available, false, identity; test_dir=temp_dir - ) - Test.@test sel == ["suite/test_a.jl"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "glob: match basename without test_ prefix" begin - mkpath(joinpath(temp_dir, "suite_src")) - touch(joinpath(temp_dir, "suite_src", "test_utils.jl")) - - available = TestRunner.TestSpec["suite_src/*"] - - sel = select_tests(["utils"], available, false, identity; test_dir=temp_dir) - Test.@test sel == ["suite_src/test_utils.jl"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "bare directory selection" begin - mkpath(joinpath(temp_dir, "suite_norm")) - touch(joinpath(temp_dir, "suite_norm", "test_x.jl")) - touch(joinpath(temp_dir, "suite_norm", "test_y.jl")) - - available = TestRunner.TestSpec["suite_norm/*"] - - # "suite_norm" (no wildcard) should behave like "suite_norm/*" - sel = select_tests( - ["suite_norm"], available, false, identity; test_dir=temp_dir - ) - Test.@test sort(sel) == ["suite_norm/test_x.jl", "suite_norm/test_y.jl"] - - # Trailing slash should also work - sel = select_tests( - ["suite_norm/"], available, false, identity; test_dir=temp_dir - ) - Test.@test sort(sel) == ["suite_norm/test_x.jl", "suite_norm/test_y.jl"] - - # Explicit wildcard still works - sel = select_tests( - ["suite_norm/*"], available, false, identity; test_dir=temp_dir - ) - Test.@test sort(sel) == ["suite_norm/test_x.jl", "suite_norm/test_y.jl"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "bare nested directory selection" begin - mkpath(joinpath(temp_dir, "suite_deep", "sub")) - touch(joinpath(temp_dir, "suite_deep", "sub", "test_z.jl")) - - available = TestRunner.TestSpec["suite_deep/sub/*"] - - # "suite_deep/sub" should match "suite_deep/sub/*" - sel = select_tests( - ["suite_deep/sub"], available, false, identity; test_dir=temp_dir - ) - Test.@test sel == ["suite_deep/sub/test_z.jl"] - end - end - end - - # ============================================================================ - # UNIT TESTS - _normalize_selections - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_normalize_selections" begin - normalize = TestRunner._normalize_selections - - candidates = TestRunner.TestSpec[ - "suite/exceptions/test_display.jl", - "suite/exceptions/test_types.jl", - "suite/core/test_core.jl", - ] - - Test.@testset "bare directory expands to dir/*" begin - result = normalize(["suite/exceptions"], candidates) - Test.@test "suite/exceptions" in result - Test.@test "suite/exceptions/*" in result - end - - Test.@testset "trailing slash is stripped and expanded" begin - result = normalize(["suite/exceptions/"], candidates) - Test.@test "suite/exceptions" in result - Test.@test "suite/exceptions/*" in result - end - - Test.@testset "pattern with wildcard is kept as-is" begin - result = normalize(["suite/exceptions/*"], candidates) - Test.@test result == ["suite/exceptions/*"] - end - - Test.@testset "non-matching directory is not expanded" begin - result = normalize(["nonexistent"], candidates) - Test.@test result == ["nonexistent"] - end - - Test.@testset "parent directory expands" begin - result = normalize(["suite"], candidates) - Test.@test "suite" in result - Test.@test "suite/*" in result - end - - Test.@testset "no duplicates" begin - result = normalize(["suite/exceptions", "suite/exceptions/"], candidates) - Test.@test count(==("suite/exceptions"), result) == 1 - Test.@test count(==("suite/exceptions/*"), result) == 1 - end - end - - # ============================================================================ - # UNIT TESTS - Progress display - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Progress display" begin - progress_bar = TestRunner._progress_bar - bar_width = TestRunner._bar_width - default_on_test_done = TestRunner._default_on_test_done - - Test.@testset "_bar_width adaptive" begin - Test.@test bar_width(1) == 1 - Test.@test bar_width(10) == 10 - Test.@test bar_width(20) == 20 - Test.@test bar_width(21) == 21 - Test.@test bar_width(30) == 30 - Test.@test bar_width(50) == 50 - Test.@test bar_width(100) == 100 - Test.@test bar_width(101) == 100 - Test.@test bar_width(500) == 100 - Test.@test bar_width(1000) == 100 - Test.@test bar_width(0) == 0 - end - - Test.@testset "_bar_width with custom progress_bar_threshold" begin - # Custom threshold of 30 - Test.@test bar_width(1, 30) == 1 - Test.@test bar_width(10, 30) == 10 - Test.@test bar_width(30, 30) == 30 - Test.@test bar_width(31, 30) == 30 - Test.@test bar_width(100, 30) == 30 - - # Custom threshold of 100 - Test.@test bar_width(1, 100) == 1 - Test.@test bar_width(50, 100) == 50 - Test.@test bar_width(100, 100) == 100 - Test.@test bar_width(101, 100) == 100 - Test.@test bar_width(500, 100) == 100 - - # Custom threshold of 10 - Test.@test bar_width(1, 10) == 1 - Test.@test bar_width(10, 10) == 10 - Test.@test bar_width(11, 10) == 10 - Test.@test bar_width(100, 10) == 10 - end - - Test.@testset "_progress_bar rendering" begin - # Full bar (explicit width) - bar = progress_bar(10, 10; width=10) - Test.@test bar == "[██████████]" - - # Empty bar - bar = progress_bar(0, 10; width=10) - Test.@test bar == "[░░░░░░░░░░]" - - # Half bar - bar = progress_bar(5, 10; width=10) - Test.@test bar == "[█████░░░░░]" - - # Auto width: 2 tests → width=2 - bar = progress_bar(1, 2) - Test.@test length(bar) == 4 # 2 chars + 2 brackets - - # Auto width: 50 tests → width=50 - bar = progress_bar(25, 50) - Test.@test length(bar) == 52 # 50 chars + 2 brackets - - # Auto width: 100 tests → width=100 - bar = progress_bar(50, 100) - Test.@test length(bar) == 102 # 100 chars + 2 brackets - - # Auto width: 500 tests → width=100 - bar = progress_bar(250, 500) - Test.@test length(bar) == 102 # 100 chars + 2 brackets - - # Edge case: total=0 - bar = progress_bar(0, 0; width=10) - Test.@test bar == "[░░░░░░░░░░]" - end - - Test.@testset "_format_progress_line output" begin - fmt = TestRunner._format_progress_line - - info = TestRunner.TestRunInfo( - :my_test, - "/tmp/test_my_test.jl", - :test_my_test, - 3, - 10, - :post_eval, - nothing, - 1.234, - ) - buf = IOBuffer() - fmt(buf, info) - output = String(take!(buf)) - Test.@test contains(output, "✓") - Test.@test contains(output, "[03/10]") - Test.@test contains(output, "my_test") - Test.@test contains(output, "1.2s") - Test.@test contains(output, "█") - - # Error status - info_err = TestRunner.TestRunInfo( - "suite/test_fail.jl", - "/tmp/test_fail.jl", - :test_fail, - 5, - 10, - :error, - ErrorException("boom"), - 0.5, - ) - buf_err = IOBuffer() - fmt(buf_err, info_err) - output_err = String(take!(buf_err)) - Test.@test contains(output_err, "✗") - Test.@test contains(output_err, "FAILED") - Test.@test contains(output_err, "[05/10]") - - # test_failed status (from Test.@test failures) - info_tf = TestRunner.TestRunInfo( - :my_test, - "/tmp/test_my_test.jl", - :test_my_test, - 7, - 10, - :test_failed, - nothing, - 2.0, - ) - buf_tf = IOBuffer() - fmt(buf_tf, info_tf) - output_tf = String(take!(buf_tf)) - Test.@test contains(output_tf, "✗") - Test.@test contains(output_tf, "FAILED") - - # Skipped status - info_skip = TestRunner.TestRunInfo( - :skipped_test, - "/tmp/test_skip.jl", - :test_skip, - 1, - 5, - :skipped, - nothing, - nothing, - ) - buf_skip = IOBuffer() - fmt(buf_skip, info_skip) - output_skip = String(take!(buf_skip)) - Test.@test contains(output_skip, "○") - Test.@test !contains(output_skip, "FAILED") - - # Fixed bar of 20 chars even for >400 tests (empty at start) - info_large = TestRunner.TestRunInfo( - :big_test, "/tmp/test_big.jl", :test_big, 1, 500, :post_eval, nothing, 0.1 - ) - buf_large = IOBuffer() - fmt(buf_large, info_large) - output_large = String(take!(buf_large)) - Test.@test contains(output_large, "✓") - Test.@test contains(output_large, "░") - - # Test with some progress (25%) - info_large_progress = TestRunner.TestRunInfo( - :big_test, "/tmp/test_big.jl", :test_big, 125, 500, :post_eval, nothing, 0.1 - ) - buf_large_progress = IOBuffer() - fmt(buf_large_progress, info_large_progress) - output_large_progress = String(take!(buf_large_progress)) - Test.@test contains(output_large_progress, "✓") - Test.@test contains(output_large_progress, "█") - Test.@test contains(output_large_progress, "░") - end - - Test.@testset "_format_progress_line cumulative coloring (full width)" begin - fmt = TestRunner._format_progress_line - history = [1, 1, 2, 3, 1] # green, green, yellow, red, green - info = TestRunner.TestRunInfo( - :final, "/tmp/final.jl", :final, 5, 5, :post_eval, nothing, 0.5 - ) - buf = IOBuffer() - fmt(buf, info; history=history, cumulative_severity=maximum(history)) - output = String(take!(buf)) - Test.@test contains(output, "[") - Test.@test occursin("\e[31m[", output) # brackets red because of failure - Test.@test occursin("\e[33m┆", output) # yellow block present (skip glyph) - end - - Test.@testset "_format_progress_line compressed coloring" begin - fmt = TestRunner._format_progress_line - info = TestRunner.TestRunInfo( - :compressed, - "/tmp/compressed.jl", - :compressed, - 10, - 50, - :post_eval, - nothing, - 0.2, - ) - buf = IOBuffer() - fmt(buf, info; cumulative_severity=2) - output = String(take!(buf)) - Test.@test occursin("\e[33m[", output) || - occursin("\e[33m[", replace(output, "\e[0m"=>"")) - end - - Test.@testset "_format_progress_line show_progress_bar=false" begin - fmt = TestRunner._format_progress_line - info = TestRunner.TestRunInfo( - :test_example, - "/tmp/test.jl", - :test_example, - 5, - 10, - :post_eval, - nothing, - 1.2, - ) - buf = IOBuffer() - fmt(buf, info; show_progress_bar=false) - output = String(take!(buf)) - # Bar glyphs should not be present - Test.@test !occursin("█", output) - Test.@test !occursin("░", output) - # No bar pattern like [█ or [░ - Test.@test !occursin("[█", output) - Test.@test !occursin("[░", output) - # Rest of line should be present - Test.@test contains(output, "✓") - Test.@test contains(output, "[05/10]") - Test.@test contains(output, "test_example") - Test.@test contains(output, "(1.2s)") - end - end - - # ============================================================================ - # EDGE CASES - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Edge cases" begin - parse_args = TestRunner._parse_test_args - select_tests = TestRunner._select_tests - normalize_available = TestRunner._normalize_available_tests - find_symbol_file = TestRunner._find_symbol_test_file_rel - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "mixed case & special chars" begin - # Test names are converted to Symbols, preserving case - (sel, _, _) = parse_args(["MyTest", "test_123"]) - Test.@test sel == ["MyTest", "test_123"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests may be a Tuple" begin - out = normalize_available((:a, "suite_ext/*")) - Test.@test out == TestRunner.TestSpec[:a, "suite_ext/*"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests may be Vector{Any}" begin - out = normalize_available(Any[:a, "suite_ext/*"]) - Test.@test out == TestRunner.TestSpec[:a, "suite_ext/*"] - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests input validation" begin - Test.@test_throws Exceptions.IncorrectArgument normalize_available(123) - Test.@test_throws Exceptions.IncorrectArgument normalize_available(Any[1]) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol resolution: shallowest match" begin - mktempdir() do temp_dir - mkpath(joinpath(temp_dir, "a")) - mkpath(joinpath(temp_dir, "a", "b")) - touch(joinpath(temp_dir, "a", "test_x.jl")) - touch(joinpath(temp_dir, "a", "b", "test_x.jl")) - - rel = find_symbol_file(:x, n -> "test_" * String(n); test_dir=temp_dir) - Test.@test rel == joinpath("a", "test_x.jl") - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "order preservation" begin - # Selections should preserve order in ARGS parsing - (sel, _, _) = parse_args(["z", "a", "m"]) - Test.@test sel == ["z", "a", "m"] - - # Using custom select_tests to verify preservation behavior - mktempdir() do temp_dir - touch(joinpath(temp_dir, "z.jl")) - touch(joinpath(temp_dir, "a.jl")) - touch(joinpath(temp_dir, "m.jl")) - touch(joinpath(temp_dir, "b.jl")) - - # Case 1: Available list order determines output order if provided - sel = select_tests( - ["z", "a"], [:a, :b, :z], false, identity; test_dir=temp_dir - ) - Test.@test sel == [:a, :z] # candidates order candidate list order - - # Case 2: Auto-discovery order (filesystem order, filtered) - # We can't guarantee FS order, so checking set equality - sel = select_tests(["z", "a"], Symbol[], false, identity; test_dir=temp_dir) - Test.@test Set(sel) == Set(["a.jl", "z.jl"]) - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "duplicate selections preserved" begin - # Duplicates are not filtered (caller's responsibility) - (sel, _, _) = parse_args(["utils", "utils"]) - Test.@test sel == ["utils", "utils"] - end - end - - # ============================================================================ - # UNIT TESTS - _run_single_test error branches - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_run_single_test" begin - run_single = TestRunner._run_single_test - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "error when file not found" begin - # L112: error branch when test file doesn't exist - err = try - run_single( - :nonexistent_file_xyz123; - filename_builder=identity, - funcname_builder=identity, - eval_mode=true, - test_dir=mktempdir(), - ) - nothing - catch e - e - end - Test.@test err isa Exceptions.IncorrectArgument - Test.@test occursin("not found", err.msg) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "string spec: file not found" begin - err = try - run_single( - "does_not_exist_abc123.jl"; - filename_builder=identity, - funcname_builder=identity, - eval_mode=true, - test_dir=mktempdir(), - ) - nothing - catch e - e - end - Test.@test err isa Exceptions.IncorrectArgument - Test.@test occursin("not found", err.msg) - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "string spec: eval_mode=false" begin - mktempdir() do temp_dir - stem = "testrunner_evalmode_false" - file = joinpath(temp_dir, stem * ".jl") - write( - file, - "const __ctbase_tr_flag__ = Ref(false)\n" * - "function $(stem)()\n" * - " __ctbase_tr_flag__[] = true\n" * - " return nothing\n" * - "end\n", - ) - - run_single( - stem; - filename_builder=identity, - funcname_builder=identity, - eval_mode=false, - test_dir=temp_dir, - ) - # Use invokelatest to avoid world age warning in Julia 1.12+ - flag_value = Base.invokelatest(getfield, Main, :__ctbase_tr_flag__) - Test.@test flag_value[] == false - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol spec: eval_mode=false" begin - mktempdir() do temp_dir - touch(joinpath(temp_dir, "test_sym_noeval.jl")) - write( - joinpath(temp_dir, "test_sym_noeval.jl"), - "const __ctbase_sym_tr_flag__ = Ref(false)\n" * - "function test_sym_noeval()\n" * - " __ctbase_sym_tr_flag__[] = true\n" * - " return nothing\n" * - "end\n", - ) - - run_single( - :sym_noeval; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> "test_" * String(n), - eval_mode=false, - test_dir=temp_dir, - ) - - flag_value = Base.invokelatest(getfield, Main, :__ctbase_sym_tr_flag__) - Test.@test flag_value[] == false - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "string spec: missing function" begin - mktempdir() do temp_dir - stem = "testrunner_missing_func" - file = joinpath(temp_dir, stem * ".jl") - write(file, "x = 1\n") - - err = try - run_single( - stem; - filename_builder=identity, - funcname_builder=identity, - eval_mode=true, - test_dir=temp_dir, - ) - nothing - catch e - e - end - - Test.@test err isa Exceptions.PreconditionError - Test.@test occursin("not found", err.msg) - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol spec: funcname_builder returns nothing" begin - mktempdir() do temp_dir - touch(joinpath(temp_dir, "test_foo.jl")) - - err = try - run_single( - :foo; - filename_builder=n -> "test_" * String(n), - funcname_builder=_ -> nothing, - eval_mode=true, - test_dir=temp_dir, - ) - nothing - catch e - e - end - - Test.@test err isa Exceptions.PreconditionError - Test.@test occursin("eval_mode=true", err.msg) - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol spec: built function missing" begin - mktempdir() do temp_dir - write(joinpath(temp_dir, "test_bar.jl"), "x = 1\n") - - err = try - run_single( - :bar; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> "test_" * String(n), - eval_mode=true, - test_dir=temp_dir, - ) - nothing - catch e - e - end - - Test.@test err isa Exceptions.PreconditionError - Test.@test occursin("not found", err.msg) - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "funcname_builder: String or Symbol" begin - mktempdir() do temp_dir - # Create two test files with different names to avoid redefinition warning - write( - joinpath(temp_dir, "test_baz_string.jl"), - "function test_baz_string()\n Test.@test true\nend\n", - ) - write( - joinpath(temp_dir, "test_baz_symbol.jl"), - "function test_baz_symbol()\n Test.@test true\nend\n", - ) - - # Test with funcname_builder returning String - run_single( - :baz_string; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> "test_" * String(n), # Returns String - eval_mode=true, - test_dir=temp_dir, - ) - # If no error, the test passed - - # Test with funcname_builder returning Symbol - run_single( - :baz_symbol; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), # Returns Symbol - eval_mode=true, - test_dir=temp_dir, - ) - # If no error, the test passed - - Test.@test true # Explicit pass if both succeeded - end - end - - # Note: Other error branches (L126, L134, L139) are not directly tested - # because they require Base.include(Main, ...) which causes side effects. - # They are tested indirectly through normal test execution. - end - - # ============================================================================ - # UNIT TESTS - TestRunInfo struct - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "TestRunInfo" begin - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "construction with all fields" begin - info = TestRunner.TestRunInfo( - :my_test, - "/tmp/test_my_test.jl", - :test_my_test, - 1, - 5, - :pre_eval, - nothing, - nothing, - ) - Test.@test info.spec === :my_test - Test.@test info.filename == "/tmp/test_my_test.jl" - Test.@test info.func_symbol === :test_my_test - Test.@test info.index == 1 - Test.@test info.total == 5 - Test.@test info.status === :pre_eval - Test.@test info.error === nothing - Test.@test info.elapsed === nothing - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "construction with String spec" begin - info = TestRunner.TestRunInfo( - "suite/test_a.jl", - "/tmp/suite/test_a.jl", - :test_a, - 3, - 10, - :post_eval, - nothing, - 1.5, - ) - Test.@test info.spec == "suite/test_a.jl" - Test.@test info.elapsed == 1.5 - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "construction with error" begin - ex = ErrorException("boom") - info = TestRunner.TestRunInfo( - :failing, "/tmp/test_failing.jl", :test_failing, 2, 3, :error, ex, 0.1 - ) - Test.@test info.status === :error - Test.@test info.error === ex - Test.@test info.elapsed == 0.1 - end - end - - # ============================================================================ - # UNIT TESTS - Callbacks via _run_single_test - # ============================================================================ - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Callbacks" begin - run_single = TestRunner._run_single_test - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "on_test_start receives :pre_eval" begin - mktempdir() do temp_dir - write( - joinpath(temp_dir, "test_cb_start.jl"), - "function test_cb_start()\n Test.@test true\nend\n", - ) - - captured = Ref{Any}(nothing) - run_single( - :cb_start; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), - eval_mode=true, - test_dir=temp_dir, - index=2, - total=7, - on_test_start=info -> (captured[]=info; true), - on_test_done=nothing, - ) - - info = captured[] - Test.@test info isa TestRunner.TestRunInfo - Test.@test info.spec === :cb_start - Test.@test info.func_symbol === :test_cb_start - Test.@test info.index == 2 - Test.@test info.total == 7 - Test.@test info.status === :pre_eval - Test.@test info.error === nothing - Test.@test info.elapsed === nothing - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "on_test_done receives :post_eval" begin - mktempdir() do temp_dir - write( - joinpath(temp_dir, "test_cb_done.jl"), - "function test_cb_done()\n Test.@test true\nend\n", - ) - - captured = Ref{Any}(nothing) - run_single( - :cb_done; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), - eval_mode=true, - test_dir=temp_dir, - index=1, - total=3, - on_test_start=nothing, - on_test_done=info -> (captured[] = info), - ) - - info = captured[] - Test.@test info isa TestRunner.TestRunInfo - Test.@test info.spec === :cb_done - Test.@test info.status === :post_eval - Test.@test info.error === nothing - Test.@test info.elapsed isa Float64 - Test.@test info.elapsed >= 0.0 - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "on_test_start false skips eval" begin - mktempdir() do temp_dir - write( - joinpath(temp_dir, "test_cb_skip.jl"), - "const __ctbase_cb_skip_flag__ = Ref(false)\n" * - "function test_cb_skip()\n" * - " __ctbase_cb_skip_flag__[] = true\n" * - " return nothing\n" * - "end\n", - ) - - done_captured = Ref{Any}(nothing) - run_single( - :cb_skip; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), - eval_mode=true, - test_dir=temp_dir, - index=1, - total=1, - on_test_start=_ -> false, - on_test_done=info -> (done_captured[] = info), - ) - - # Function was NOT called - flag_value = Base.invokelatest(getfield, Main, :__ctbase_cb_skip_flag__) - Test.@test flag_value[] == false - - # on_test_done was called with :skipped - info = done_captured[] - Test.@test info isa TestRunner.TestRunInfo - Test.@test info.status === :skipped - Test.@test info.elapsed === nothing - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "on_test_done receives :skipped" begin - mktempdir() do temp_dir - write(joinpath(temp_dir, "test_cb_noeval.jl"), "x = 1\n") - - done_captured = Ref{Any}(nothing) - run_single( - :cb_noeval; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), - eval_mode=false, - test_dir=temp_dir, - index=1, - total=1, - on_test_start=nothing, - on_test_done=info -> (done_captured[] = info), - ) - - info = done_captured[] - Test.@test info isa TestRunner.TestRunInfo - Test.@test info.status === :skipped - Test.@test info.func_symbol === nothing - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "on_test_done receives :error on eval failure" begin - mktempdir() do temp_dir - write( - joinpath(temp_dir, "test_cb_err.jl"), - "function test_cb_err()\n error(\"intentional failure\")\nend\n", - ) - - done_captured = Ref{Any}(nothing) - err = try - run_single( - :cb_err; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), - eval_mode=true, - test_dir=temp_dir, - index=1, - total=1, - on_test_start=_ -> true, - on_test_done=info -> (done_captured[] = info), - ) - nothing - catch e - e - end - - # Error is rethrown - Test.@test err isa ErrorException - Test.@test occursin("intentional failure", err.msg) - - # on_test_done was called with :error - info = done_captured[] - Test.@test info isa TestRunner.TestRunInfo - Test.@test info.status === :error - Test.@test info.error isa ErrorException - Test.@test info.elapsed isa Float64 - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "both callbacks work together" begin - mktempdir() do temp_dir - write( - joinpath(temp_dir, "test_cb_both.jl"), - "function test_cb_both()\n Test.@test true\nend\n", - ) - - start_captured = Ref{Any}(nothing) - done_captured = Ref{Any}(nothing) - run_single( - :cb_both; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), - eval_mode=true, - test_dir=temp_dir, - index=3, - total=5, - on_test_start=info -> (start_captured[]=info; true), - on_test_done=info -> (done_captured[] = info), - ) - - Test.@test start_captured[].status === :pre_eval - Test.@test start_captured[].index == 3 - Test.@test start_captured[].total == 5 - Test.@test done_captured[].status === :post_eval - Test.@test done_captured[].index == 3 - Test.@test done_captured[].total == 5 - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "callbacks with String spec" begin - mktempdir() do temp_dir - write( - joinpath(temp_dir, "test_cb_str.jl"), - "function test_cb_str()\n Test.@test true\nend\n", - ) - - start_captured = Ref{Any}(nothing) - done_captured = Ref{Any}(nothing) - run_single( - "test_cb_str.jl"; - filename_builder=identity, - funcname_builder=identity, - eval_mode=true, - test_dir=temp_dir, - index=1, - total=2, - on_test_start=info -> (start_captured[]=info; true), - on_test_done=info -> (done_captured[] = info), - ) - - Test.@test start_captured[].spec == "test_cb_str.jl" - Test.@test start_captured[].status === :pre_eval - Test.@test done_captured[].spec == "test_cb_str.jl" - Test.@test done_captured[].status === :post_eval - end - end - - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "no callbacks (nothing) preserves original behavior" begin - mktempdir() do temp_dir - write( - joinpath(temp_dir, "test_cb_none.jl"), - "function test_cb_none()\n Test.@test true\nend\n", - ) - - # Should not error when callbacks are nothing - run_single( - :cb_none; - filename_builder=n -> "test_" * String(n), - funcname_builder=n -> Symbol(:test_, n), - eval_mode=true, - test_dir=temp_dir, - on_test_start=nothing, - on_test_done=nothing, - ) - Test.@test true - end - end - end - # ============================================================================ # INTEGRATION TESTS - Callbacks via run_tests # ============================================================================ diff --git a/test/suite/extensions/test_testrunner_arg_parsing.jl b/test/suite/extensions/test_testrunner_arg_parsing.jl new file mode 100644 index 00000000..90f69713 --- /dev/null +++ b/test/suite/extensions/test_testrunner_arg_parsing.jl @@ -0,0 +1,164 @@ +module TestTestRunnerArgParsing + +import Test +import CTBase +import CTBase.Exceptions + +const TestRunner = Base.get_extension(CTBase, :TestRunner) + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_testrunner_arg_parsing() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_parse_test_args" begin + # Access the private function via the loaded extension module + parse_args = TestRunner._parse_test_args + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "empty args with defaults" begin + (sel, all_flag, dry_flag) = parse_args(String[]) + Test.@test sel == String[] + Test.@test all_flag == false + Test.@test dry_flag == false + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "single test name" begin + (sel, _, _) = parse_args(["utils"]) + Test.@test sel == ["utils"] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "multiple test names" begin + (sel, _, _) = parse_args(["utils", "default"]) + Test.@test sel == ["utils", "default"] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "coverage flags are filtered out" begin + (sel, _, _) = parse_args(["coverage=true"]) + Test.@test sel == String[] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "CLI flags -a / --all" begin + # -a should set run_all=true + (_, run_all, _) = parse_args(["-a"]) + Test.@test run_all == true + + (_, run_all, _) = parse_args(["--all"]) + Test.@test run_all == true + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "CLI flags -n / --dryrun" begin + # -n should set dry_run=true + (_, _, dry_run) = parse_args(["-n"]) + Test.@test dry_run == true + + (_, _, dry_run) = parse_args(["--dryrun"]) + Test.@test dry_run == true + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "mixed flags and tests" begin + (sel, run_all, dry_run) = parse_args(["utils", "-n", "--all"]) + Test.@test sel == ["utils"] + Test.@test run_all == true + Test.@test dry_run == true + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "test/ prefix is stripped" begin + (sel, _, _) = parse_args(["test/suite/foo"]) + Test.@test sel == ["suite/foo"] + + (sel, _, _) = parse_args(["test/suite"]) + Test.@test sel == ["suite"] + + # No prefix → unchanged + (sel, _, _) = parse_args(["suite/foo"]) + Test.@test sel == ["suite/foo"] + + # Exact "test" (no slash) → unchanged + (sel, _, _) = parse_args(["test"]) + Test.@test sel == ["test"] + + # Multiple args with mixed prefixes + (sel, _, _) = parse_args(["test/suite/a", "suite/b"]) + Test.@test sel == ["suite/a", "suite/b"] + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_strip_test_prefix" begin + strip_prefix = TestRunner._strip_test_prefix + + Test.@test strip_prefix("test/suite/foo") == "suite/foo" + Test.@test strip_prefix("test/a") == "a" + Test.@test strip_prefix("suite/foo") == "suite/foo" + Test.@test strip_prefix("test") == "test" + Test.@test strip_prefix("testing/foo") == "testing/foo" + Test.@test strip_prefix("test/") == "" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_normalize_selections" begin + normalize = TestRunner._normalize_selections + + candidates = TestRunner.TestSpec[ + "suite/exceptions/test_display.jl", + "suite/exceptions/test_types.jl", + "suite/core/test_core.jl", + ] + + Test.@testset "bare directory expands to dir/*" begin + result = normalize(["suite/exceptions"], candidates) + Test.@test "suite/exceptions" in result + Test.@test "suite/exceptions/*" in result + end + + Test.@testset "trailing slash is stripped and expanded" begin + result = normalize(["suite/exceptions/"], candidates) + Test.@test "suite/exceptions" in result + Test.@test "suite/exceptions/*" in result + end + + Test.@testset "pattern with wildcard is kept as-is" begin + result = normalize(["suite/exceptions/*"], candidates) + Test.@test result == ["suite/exceptions/*"] + end + + Test.@testset "non-matching directory is not expanded" begin + result = normalize(["nonexistent"], candidates) + Test.@test result == ["nonexistent"] + end + + Test.@testset "parent directory expands" begin + result = normalize(["suite"], candidates) + Test.@test "suite" in result + Test.@test "suite/*" in result + end + + Test.@testset "no duplicates" begin + result = normalize(["suite/exceptions", "suite/exceptions/"], candidates) + Test.@test count(==("suite/exceptions"), result) == 1 + Test.@test count(==("suite/exceptions/*"), result) == 1 + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_normalize_available_tests" begin + normalize_available = TestRunner._normalize_available_tests + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests may be a Tuple" begin + out = normalize_available((:a, "suite_ext/*")) + Test.@test out == TestRunner.TestSpec[:a, "suite_ext/*"] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests may be Vector{Any}" begin + out = normalize_available(Any[:a, "suite_ext/*"]) + Test.@test out == TestRunner.TestSpec[:a, "suite_ext/*"] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests input validation" begin + Test.@test_throws Exceptions.IncorrectArgument normalize_available(123) + Test.@test_throws Exceptions.IncorrectArgument normalize_available(Any[1]) + end + end + + return nothing +end + +end # module + +test_testrunner_arg_parsing() = TestTestRunnerArgParsing.test_testrunner_arg_parsing() diff --git a/test/suite/extensions/test_testrunner_execution.jl b/test/suite/extensions/test_testrunner_execution.jl new file mode 100644 index 00000000..11238244 --- /dev/null +++ b/test/suite/extensions/test_testrunner_execution.jl @@ -0,0 +1,244 @@ +module TestTestRunnerExecution + +import Test +import CTBase +import CTBase.Exceptions + +const TestRunner = Base.get_extension(CTBase, :TestRunner) + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_testrunner_execution() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_run_single_test" begin + run_single = TestRunner._run_single_test + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "error when file not found" begin + # L112: error branch when test file doesn't exist + err = try + run_single( + :nonexistent_file_xyz123; + filename_builder=identity, + funcname_builder=identity, + eval_mode=true, + test_dir=mktempdir(), + ) + nothing + catch e + e + end + Test.@test err isa Exceptions.IncorrectArgument + Test.@test occursin("not found", err.msg) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "string spec: file not found" begin + err = try + run_single( + "does_not_exist_abc123.jl"; + filename_builder=identity, + funcname_builder=identity, + eval_mode=true, + test_dir=mktempdir(), + ) + nothing + catch e + e + end + Test.@test err isa Exceptions.IncorrectArgument + Test.@test occursin("not found", err.msg) + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "string spec: eval_mode=false" begin + mktempdir() do temp_dir + stem = "testrunner_evalmode_false" + file = joinpath(temp_dir, stem * ".jl") + write( + file, + "const __ctbase_tr_flag__ = Ref(false)\n" * + "function $(stem)()\n" * + " __ctbase_tr_flag__[] = true\n" * + " return nothing\n" * + "end\n", + ) + + run_single( + stem; + filename_builder=identity, + funcname_builder=identity, + eval_mode=false, + test_dir=temp_dir, + ) + # Use invokelatest to avoid world age warning in Julia 1.12+ + flag_value = Base.invokelatest(getfield, Main, :__ctbase_tr_flag__) + Test.@test flag_value[] == false + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol spec: eval_mode=false" begin + mktempdir() do temp_dir + touch(joinpath(temp_dir, "test_sym_noeval.jl")) + write( + joinpath(temp_dir, "test_sym_noeval.jl"), + "const __ctbase_sym_tr_flag__ = Ref(false)\n" * + "function test_sym_noeval()\n" * + " __ctbase_sym_tr_flag__[] = true\n" * + " return nothing\n" * + "end\n", + ) + + run_single( + :sym_noeval; + filename_builder=n -> "test_" * String(n), + funcname_builder=n -> "test_" * String(n), + eval_mode=false, + test_dir=temp_dir, + ) + + flag_value = Base.invokelatest(getfield, Main, :__ctbase_sym_tr_flag__) + Test.@test flag_value[] == false + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "string spec: missing function" begin + mktempdir() do temp_dir + stem = "testrunner_missing_func" + file = joinpath(temp_dir, stem * ".jl") + write(file, "x = 1\n") + + err = try + run_single( + stem; + filename_builder=identity, + funcname_builder=identity, + eval_mode=true, + test_dir=temp_dir, + ) + nothing + catch e + e + end + + Test.@test err isa Exceptions.PreconditionError + Test.@test occursin("not found", err.msg) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol spec: funcname_builder returns nothing" begin + mktempdir() do temp_dir + touch(joinpath(temp_dir, "test_foo.jl")) + + err = try + run_single( + :foo; + filename_builder=n -> "test_" * String(n), + funcname_builder=_ -> nothing, + eval_mode=true, + test_dir=temp_dir, + ) + nothing + catch e + e + end + + Test.@test err isa Exceptions.PreconditionError + Test.@test occursin("eval_mode=true", err.msg) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol spec: built function missing" begin + mktempdir() do temp_dir + write(joinpath(temp_dir, "test_bar.jl"), "x = 1\n") + + err = try + run_single( + :bar; + filename_builder=n -> "test_" * String(n), + funcname_builder=n -> "test_" * String(n), + eval_mode=true, + test_dir=temp_dir, + ) + nothing + catch e + e + end + + Test.@test err isa Exceptions.PreconditionError + Test.@test occursin("not found", err.msg) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "funcname_builder: String or Symbol" begin + mktempdir() do temp_dir + # Create two test files with different names to avoid redefinition warning + write( + joinpath(temp_dir, "test_baz_string.jl"), + "function test_baz_string()\n Test.@test true\nend\n", + ) + write( + joinpath(temp_dir, "test_baz_symbol.jl"), + "function test_baz_symbol()\n Test.@test true\nend\n", + ) + + # Test with funcname_builder returning String + run_single( + :baz_string; + filename_builder=n -> "test_" * String(n), + funcname_builder=n -> "test_" * String(n), # Returns String + eval_mode=true, + test_dir=temp_dir, + ) + # If no error, the test passed + + # Test with funcname_builder returning Symbol + run_single( + :baz_symbol; + filename_builder=n -> "test_" * String(n), + funcname_builder=n -> Symbol(:test_, n), # Returns Symbol + eval_mode=true, + test_dir=temp_dir, + ) + # If no error, the test passed + + Test.@test true # Explicit pass if both succeeded + end + end + + # Note: Other error branches (L126, L134, L139) are not directly tested + # because they require Base.include(Main, ...) which causes side effects. + # They are tested indirectly through normal test execution. + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "TestRunner: file exists then disappears" begin + Test.@test TestRunner !== nothing + mktempdir() do tmp + # Create a test file that will be deleted before execution + test_file = joinpath(tmp, "phantom_test.jl") + touch(test_file) + + # Delete the file before calling _run_single_test + rm(test_file) + + err = try + TestRunner._run_single_test( + :phantom_test; + filename_builder=identity, + funcname_builder=identity, + eval_mode=false, + test_dir=tmp, + ) + nothing + catch e + e + end + + Test.@test err isa CTBase.Exceptions.IncorrectArgument + Test.@test occursin("not found", err.msg) + end + end + + return nothing +end + +end # module + +test_testrunner_execution() = TestTestRunnerExecution.test_testrunner_execution() diff --git a/test/suite/extensions/test_testrunner_progress.jl b/test/suite/extensions/test_testrunner_progress.jl new file mode 100644 index 00000000..394bb6e3 --- /dev/null +++ b/test/suite/extensions/test_testrunner_progress.jl @@ -0,0 +1,250 @@ +module TestTestRunnerProgress + +import Test +import CTBase + +const TestRunner = Base.get_extension(CTBase, :TestRunner) + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_testrunner_progress() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Progress display" begin + progress_bar = TestRunner._progress_bar + bar_width = TestRunner._bar_width + default_on_test_done = TestRunner._default_on_test_done + + Test.@testset "_bar_width adaptive" begin + Test.@test bar_width(1) == 1 + Test.@test bar_width(10) == 10 + Test.@test bar_width(20) == 20 + Test.@test bar_width(21) == 21 + Test.@test bar_width(30) == 30 + Test.@test bar_width(50) == 50 + Test.@test bar_width(100) == 100 + Test.@test bar_width(101) == 100 + Test.@test bar_width(500) == 100 + Test.@test bar_width(1000) == 100 + Test.@test bar_width(0) == 0 + end + + Test.@testset "_bar_width with custom progress_bar_threshold" begin + # Custom threshold of 30 + Test.@test bar_width(1, 30) == 1 + Test.@test bar_width(10, 30) == 10 + Test.@test bar_width(30, 30) == 30 + Test.@test bar_width(31, 30) == 30 + Test.@test bar_width(100, 30) == 30 + + # Custom threshold of 100 + Test.@test bar_width(1, 100) == 1 + Test.@test bar_width(50, 100) == 50 + Test.@test bar_width(100, 100) == 100 + Test.@test bar_width(101, 100) == 100 + Test.@test bar_width(500, 100) == 100 + + # Custom threshold of 10 + Test.@test bar_width(1, 10) == 1 + Test.@test bar_width(10, 10) == 10 + Test.@test bar_width(11, 10) == 10 + Test.@test bar_width(100, 10) == 10 + end + + Test.@testset "_progress_bar rendering" begin + # Full bar (explicit width) + bar = progress_bar(10, 10; width=10) + Test.@test bar == "[██████████]" + + # Empty bar + bar = progress_bar(0, 10; width=10) + Test.@test bar == "[░░░░░░░░░░]" + + # Half bar + bar = progress_bar(5, 10; width=10) + Test.@test bar == "[█████░░░░░]" + + # Auto width: 2 tests → width=2 + bar = progress_bar(1, 2) + Test.@test length(bar) == 4 # 2 chars + 2 brackets + + # Auto width: 50 tests → width=50 + bar = progress_bar(25, 50) + Test.@test length(bar) == 52 # 50 chars + 2 brackets + + # Auto width: 100 tests → width=100 + bar = progress_bar(50, 100) + Test.@test length(bar) == 102 # 100 chars + 2 brackets + + # Auto width: 500 tests → width=100 + bar = progress_bar(250, 500) + Test.@test length(bar) == 102 # 100 chars + 2 brackets + + # Edge case: total=0 + bar = progress_bar(0, 0; width=10) + Test.@test bar == "[░░░░░░░░░░]" + end + + Test.@testset "_format_progress_line output" begin + fmt = TestRunner._format_progress_line + + info = TestRunner.TestRunInfo( + :my_test, + "/tmp/test_my_test.jl", + :test_my_test, + 3, + 10, + :post_eval, + nothing, + 1.234, + ) + buf = IOBuffer() + fmt(buf, info) + output = String(take!(buf)) + Test.@test contains(output, "✓") + Test.@test contains(output, "[03/10]") + Test.@test contains(output, "my_test") + Test.@test contains(output, "1.2s") + Test.@test contains(output, "█") + + # Error status + info_err = TestRunner.TestRunInfo( + "suite/test_fail.jl", + "/tmp/test_fail.jl", + :test_fail, + 5, + 10, + :error, + ErrorException("boom"), + 0.5, + ) + buf_err = IOBuffer() + fmt(buf_err, info_err) + output_err = String(take!(buf_err)) + Test.@test contains(output_err, "✗") + Test.@test contains(output_err, "FAILED") + Test.@test contains(output_err, "[05/10]") + + # test_failed status (from Test.@test failures) + info_tf = TestRunner.TestRunInfo( + :my_test, + "/tmp/test_my_test.jl", + :test_my_test, + 7, + 10, + :test_failed, + nothing, + 2.0, + ) + buf_tf = IOBuffer() + fmt(buf_tf, info_tf) + output_tf = String(take!(buf_tf)) + Test.@test contains(output_tf, "✗") + Test.@test contains(output_tf, "FAILED") + + # Skipped status + info_skip = TestRunner.TestRunInfo( + :skipped_test, + "/tmp/test_skip.jl", + :test_skip, + 1, + 5, + :skipped, + nothing, + nothing, + ) + buf_skip = IOBuffer() + fmt(buf_skip, info_skip) + output_skip = String(take!(buf_skip)) + Test.@test contains(output_skip, "○") + Test.@test !contains(output_skip, "FAILED") + + # Fixed bar of 20 chars even for >400 tests (empty at start) + info_large = TestRunner.TestRunInfo( + :big_test, "/tmp/test_big.jl", :test_big, 1, 500, :post_eval, nothing, 0.1 + ) + buf_large = IOBuffer() + fmt(buf_large, info_large) + output_large = String(take!(buf_large)) + Test.@test contains(output_large, "✓") + Test.@test contains(output_large, "░") + + # Test with some progress (25%) + info_large_progress = TestRunner.TestRunInfo( + :big_test, "/tmp/test_big.jl", :test_big, 125, 500, :post_eval, nothing, 0.1 + ) + buf_large_progress = IOBuffer() + fmt(buf_large_progress, info_large_progress) + output_large_progress = String(take!(buf_large_progress)) + Test.@test contains(output_large_progress, "✓") + Test.@test contains(output_large_progress, "█") + Test.@test contains(output_large_progress, "░") + end + + Test.@testset "_format_progress_line cumulative coloring (full width)" begin + fmt = TestRunner._format_progress_line + history = [1, 1, 2, 3, 1] # green, green, yellow, red, green + info = TestRunner.TestRunInfo( + :final, "/tmp/final.jl", :final, 5, 5, :post_eval, nothing, 0.5 + ) + buf = IOBuffer() + fmt(buf, info; history=history, cumulative_severity=maximum(history)) + output = String(take!(buf)) + Test.@test contains(output, "[") + Test.@test occursin("\e[31m[", output) # brackets red because of failure + Test.@test occursin("\e[33m┆", output) # yellow block present (skip glyph) + end + + Test.@testset "_format_progress_line compressed coloring" begin + fmt = TestRunner._format_progress_line + info = TestRunner.TestRunInfo( + :compressed, + "/tmp/compressed.jl", + :compressed, + 10, + 50, + :post_eval, + nothing, + 0.2, + ) + buf = IOBuffer() + fmt(buf, info; cumulative_severity=2) + output = String(take!(buf)) + Test.@test occursin("\e[33m[", output) || + occursin("\e[33m[", replace(output, "\e[0m"=>"")) + end + + Test.@testset "_format_progress_line show_progress_bar=false" begin + fmt = TestRunner._format_progress_line + info = TestRunner.TestRunInfo( + :test_example, + "/tmp/test.jl", + :test_example, + 5, + 10, + :post_eval, + nothing, + 1.2, + ) + buf = IOBuffer() + fmt(buf, info; show_progress_bar=false) + output = String(take!(buf)) + # Bar glyphs should not be present + Test.@test !occursin("█", output) + Test.@test !occursin("░", output) + # No bar pattern like [█ or [░ + Test.@test !occursin("[█", output) + Test.@test !occursin("[░", output) + # Rest of line should be present + Test.@test contains(output, "✓") + Test.@test contains(output, "[05/10]") + Test.@test contains(output, "test_example") + Test.@test contains(output, "(1.2s)") + end + end + + return nothing +end + +end # module + +test_testrunner_progress() = TestTestRunnerProgress.test_testrunner_progress() diff --git a/test/suite/extensions/test_testrunner_selection.jl b/test/suite/extensions/test_testrunner_selection.jl new file mode 100644 index 00000000..a6b6d0a2 --- /dev/null +++ b/test/suite/extensions/test_testrunner_selection.jl @@ -0,0 +1,245 @@ +module TestTestRunnerSelection + +import Test +import CTBase + +const TestRunner = Base.get_extension(CTBase, :TestRunner) + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_testrunner_selection() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_select_tests" begin + select_tests = TestRunner._select_tests + + # Mock filename builder for testing + test_builder_sym = name -> Symbol(:test_, name) + test_builder_str = name -> "test_" * String(name) + + # Mocking readdir requires a temporary directory or we mock the fs behavior? + # Easier: we test the filtering logic with a custom test_dir. + # Let's create a temporary directory structure for testing. + + mktempdir() do temp_dir + # Create some dummy test files + touch(joinpath(temp_dir, "test_utils.jl")) + touch(joinpath(temp_dir, "test_core.jl")) + touch(joinpath(temp_dir, "runtests.jl")) # Should be ignored + touch(joinpath(temp_dir, "readme.md")) # Should be ignored + + # ========================================================== + # Scenario 1: Auto-discovery (available_tests empty) + # ========================================================== + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Auto-discovery (empty available_tests)" begin + # Empty args -> run all .jl files (excluding runtests.jl) + # Names derived as basenames + sel = select_tests(String[], Symbol[], false, identity; test_dir=temp_dir) + Test.@test sort(sel) == ["test_core.jl", "test_utils.jl"] + + # Run all (-a) -> same result + sel = select_tests(String[], Symbol[], true, identity; test_dir=temp_dir) + Test.@test sort(sel) == ["test_core.jl", "test_utils.jl"] + + # Globbing: select only utils + sel = select_tests( + ["test_utils"], Symbol[], false, identity; test_dir=temp_dir + ) + Test.@test sel == ["test_utils.jl"] + + # Globbing: pattern matching + sel = select_tests( + ["test_c*"], Symbol[], false, identity; test_dir=temp_dir + ) + Test.@test sel == ["test_core.jl"] + + # Globbing: match filename? + sel = select_tests( + ["test_core_jl"], Symbol[], false, identity; test_dir=temp_dir + ) + # "^test_core_jl$" doesn't match "test_core.jl" or "test_core" + Test.@test isempty(sel) + + sel = select_tests( + ["test_core.jl"], Symbol[], false, identity; test_dir=temp_dir + ) + Test.@test sel == ["test_core.jl"] + end + + # ========================================================== + # Scenario 2: With available_tests + # ========================================================== + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "With available_tests" begin + available = [:utils, :core] + + # Note: TestRunner checks if file exists via builder + # test_utils.jl exists -> :utils is valid + # test_core.jl exists -> :core is valid + + # Empty args -> run all valid available tests + sel = select_tests( + String[], available, false, test_builder_sym; test_dir=temp_dir + ) + Test.@test sort(sel) == [:core, :utils] + + # Selection + sel = select_tests( + ["utils"], available, false, test_builder_sym; test_dir=temp_dir + ) + Test.@test sel == [:utils] + + # Globbing over available names + # available test :core, file: test_core.jl + sel = select_tests( + ["c*"], available, false, test_builder_sym; test_dir=temp_dir + ) + Test.@test sel == [:core] + + # Globbing over filename without extension + # available test :core, file: test_core.jl + sel = select_tests( + ["test_core"], available, false, test_builder_sym; test_dir=temp_dir + ) + Test.@test sel == [:core] + + # Builder may return String + sel = select_tests( + ["core"], available, false, test_builder_str; test_dir=temp_dir + ) + Test.@test sel == [:core] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol in subdirectory" begin + mkpath(joinpath(temp_dir, "suite_src")) + touch(joinpath(temp_dir, "suite_src", "test_utils.jl")) + + available = [:utils] + + sel = select_tests( + String[], available, false, test_builder_sym; test_dir=temp_dir + ) + Test.@test sel == [:utils] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests may be directories" begin + mkpath(joinpath(temp_dir, "suite")) + touch(joinpath(temp_dir, "suite", "test_a.jl")) + touch(joinpath(temp_dir, "suite", "test_b.jl")) + + available = TestRunner.TestSpec["suite"] + sel = select_tests(String[], available, false, identity; test_dir=temp_dir) + Test.@test sel == ["suite/test_a.jl", "suite/test_b.jl"] + + sel = select_tests( + ["suite/test_a"], available, false, identity; test_dir=temp_dir + ) + Test.@test sel == ["suite/test_a.jl"] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "glob: match basename without test_ prefix" begin + mkpath(joinpath(temp_dir, "suite_src")) + touch(joinpath(temp_dir, "suite_src", "test_utils.jl")) + + available = TestRunner.TestSpec["suite_src/*"] + + sel = select_tests(["utils"], available, false, identity; test_dir=temp_dir) + Test.@test sel == ["suite_src/test_utils.jl"] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "bare directory selection" begin + mkpath(joinpath(temp_dir, "suite_norm")) + touch(joinpath(temp_dir, "suite_norm", "test_x.jl")) + touch(joinpath(temp_dir, "suite_norm", "test_y.jl")) + + available = TestRunner.TestSpec["suite_norm/*"] + + # "suite_norm" (no wildcard) should behave like "suite_norm/*" + sel = select_tests( + ["suite_norm"], available, false, identity; test_dir=temp_dir + ) + Test.@test sort(sel) == ["suite_norm/test_x.jl", "suite_norm/test_y.jl"] + + # Trailing slash should also work + sel = select_tests( + ["suite_norm/"], available, false, identity; test_dir=temp_dir + ) + Test.@test sort(sel) == ["suite_norm/test_x.jl", "suite_norm/test_y.jl"] + + # Explicit wildcard still works + sel = select_tests( + ["suite_norm/*"], available, false, identity; test_dir=temp_dir + ) + Test.@test sort(sel) == ["suite_norm/test_x.jl", "suite_norm/test_y.jl"] + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "bare nested directory selection" begin + mkpath(joinpath(temp_dir, "suite_deep", "sub")) + touch(joinpath(temp_dir, "suite_deep", "sub", "test_z.jl")) + + available = TestRunner.TestSpec["suite_deep/sub/*"] + + # "suite_deep/sub" should match "suite_deep/sub/*" + sel = select_tests( + ["suite_deep/sub"], available, false, identity; test_dir=temp_dir + ) + Test.@test sel == ["suite_deep/sub/test_z.jl"] + end + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_find_symbol_test_file_rel" begin + find_symbol_file = TestRunner._find_symbol_test_file_rel + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "symbol resolution: shallowest match" begin + mktempdir() do temp_dir + mkpath(joinpath(temp_dir, "a")) + mkpath(joinpath(temp_dir, "a", "b")) + touch(joinpath(temp_dir, "a", "test_x.jl")) + touch(joinpath(temp_dir, "a", "b", "test_x.jl")) + + rel = find_symbol_file(:x, n -> "test_" * String(n); test_dir=temp_dir) + Test.@test rel == joinpath("a", "test_x.jl") + end + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "order preservation" begin + parse_args = TestRunner._parse_test_args + select_tests = TestRunner._select_tests + + # Selections should preserve order in ARGS parsing + (sel, _, _) = parse_args(["z", "a", "m"]) + Test.@test sel == ["z", "a", "m"] + + # Using custom select_tests to verify preservation behavior + mktempdir() do temp_dir + touch(joinpath(temp_dir, "z.jl")) + touch(joinpath(temp_dir, "a.jl")) + touch(joinpath(temp_dir, "m.jl")) + touch(joinpath(temp_dir, "b.jl")) + + # Case 1: Available list order determines output order if provided + sel = select_tests( + ["z", "a"], [:a, :b, :z], false, identity; test_dir=temp_dir + ) + Test.@test sel == [:a, :z] # candidates order candidate list order + + # Case 2: Auto-discovery order (filesystem order, filtered) + # We can't guarantee FS order, so checking set equality + sel = select_tests(["z", "a"], Symbol[], false, identity; test_dir=temp_dir) + Test.@test Set(sel) == Set(["a.jl", "z.jl"]) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "duplicate selections preserved" begin + parse_args = TestRunner._parse_test_args + # Duplicates are not filtered (caller's responsibility) + (sel, _, _) = parse_args(["utils", "utils"]) + Test.@test sel == ["utils", "utils"] + end + + return nothing +end + +end # module + +test_testrunner_selection() = TestTestRunnerSelection.test_testrunner_selection() diff --git a/test/suite/extensions/test_testrunner_types.jl b/test/suite/extensions/test_testrunner_types.jl new file mode 100644 index 00000000..c504681b --- /dev/null +++ b/test/suite/extensions/test_testrunner_types.jl @@ -0,0 +1,74 @@ +module TestTestRunnerTypes + +import Test +import CTBase +import CTBase.Extensions + +const TestRunner = Base.get_extension(CTBase, :TestRunner) + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_testrunner_types() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "TestRunInfo" begin + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "construction with all fields" begin + info = TestRunner.TestRunInfo( + :my_test, + "/tmp/test_my_test.jl", + :test_my_test, + 1, + 5, + :pre_eval, + nothing, + nothing, + ) + Test.@test info.spec === :my_test + Test.@test info.filename == "/tmp/test_my_test.jl" + Test.@test info.func_symbol === :test_my_test + Test.@test info.index == 1 + Test.@test info.total == 5 + Test.@test info.status === :pre_eval + Test.@test info.error === nothing + Test.@test info.elapsed === nothing + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "construction with String spec" begin + info = TestRunner.TestRunInfo( + "suite/test_a.jl", + "/tmp/suite/test_a.jl", + :test_a, + 3, + 10, + :post_eval, + nothing, + 1.5, + ) + Test.@test info.spec == "suite/test_a.jl" + Test.@test info.elapsed == 1.5 + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "construction with error" begin + ex = ErrorException("boom") + info = TestRunner.TestRunInfo( + :failing, "/tmp/test_failing.jl", :test_failing, 2, 3, :error, ex, 0.1 + ) + Test.@test info.status === :error + Test.@test info.error === ex + Test.@test info.elapsed == 0.1 + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "TestSpec" begin + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "TestSpec is Vector{Union{String, Symbol}}" begin + spec = TestRunner.TestSpec[:a, "suite/*"] + Test.@test spec isa Vector{Union{String, Symbol}} + Test.@test spec == [:a, "suite/*"] + end + end + + return nothing +end + +end # module + +test_testrunner_types() = TestTestRunnerTypes.test_testrunner_types() diff --git a/test/suite/unicode/test_unicode_utils.jl b/test/suite/unicode/test_subscripts.jl similarity index 54% rename from test/suite/unicode/test_unicode_utils.jl rename to test/suite/unicode/test_subscripts.jl index 245e2713..8adae9b8 100644 --- a/test/suite/unicode/test_unicode_utils.jl +++ b/test/suite/unicode/test_subscripts.jl @@ -1,4 +1,4 @@ -module TestUnicodeUtils +module TestSubscripts import Test import CTBase.Exceptions: Exceptions @@ -7,7 +7,7 @@ import CTBase.Unicode: Unicode const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true -function test_unicode_utils() +function test_subscripts() Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctindice (single subscript char)" begin # Test exception thrown for out-of-range inputs Test.@test_throws Exceptions.IncorrectArgument Unicode.ctindice(-1) @@ -36,50 +36,62 @@ function test_unicode_utils() Test.@test Unicode.ctindices(314) == "₃₁₄" end - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscript (single superscript char)" begin - # Exception for out-of-range - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(-1) - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(10) - - # Test all valid superscripts [0..9] - Test.@test Unicode.ctupperscript(0) == '⁰' - Test.@test Unicode.ctupperscript(1) == '¹' - Test.@test Unicode.ctupperscript(2) == '²' - Test.@test Unicode.ctupperscript(3) == '³' - Test.@test Unicode.ctupperscript(4) == '⁴' - Test.@test Unicode.ctupperscript(5) == '⁵' - Test.@test Unicode.ctupperscript(6) == '⁶' - Test.@test Unicode.ctupperscript(7) == '⁷' - Test.@test Unicode.ctupperscript(8) == '⁸' - Test.@test Unicode.ctupperscript(9) == '⁹' - end + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctindice enriched errors" begin + # Test negative value + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctindice(-1) - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscripts (multi-digit superscript string)" begin - # Exception for negative input - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscripts(-1) + try + Unicode.ctindice(-1) + Test.@test false # Should not reach here + catch e + Test.@test e isa Exceptions.IncorrectArgument + Test.@test e.got == "-1" + Test.@test e.expected == "0-9" + Test.@test occursin("subscript must be between 0 and 9", e.msg) + Test.@test occursin("ctindices()", e.suggestion) + Test.@test e.context == "Unicode subscript generation" + end + + # Test value too large + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctindice(15) - # Test zero and multi-digit inputs - Test.@test Unicode.ctupperscripts(0) == "⁰" - Test.@test Unicode.ctupperscripts(19) == "¹⁹" # no leading zero - Test.@test Unicode.ctupperscripts(109) == "¹⁰⁹" + try + Unicode.ctindice(15) + Test.@test false # Should not reach here + catch e + Test.@test e isa Exceptions.IncorrectArgument + Test.@test e.got == "15" + Test.@test e.expected == "0-9" + Test.@test occursin("ctindices()", e.suggestion) + Test.@test e.context == "Unicode subscript generation" + end end - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctindices / ctindice consistency" begin - for d in 0:9 - Test.@test Unicode.ctindices(d) == string(Unicode.ctindice(d)) + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctindices enriched errors" begin + # Test negative value + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctindices(-5) + + try + Unicode.ctindices(-5) + Test.@test false # Should not reach here + catch e + Test.@test e isa Exceptions.IncorrectArgument + Test.@test e.got == "-5" + Test.@test e.expected == "≥ 0" + Test.@test occursin("subscript must be positive", e.msg) + Test.@test e.context == "Unicode subscript string generation" end end - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscripts / ctupperscript consistency" begin + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctindices / ctindice consistency" begin for d in 0:9 - Test.@test Unicode.ctupperscripts(d) == string(Unicode.ctupperscript(d)) + Test.@test Unicode.ctindices(d) == string(Unicode.ctindice(d)) end end return nothing end -end # module TestUnicodeUtils +end # module -# CRITICAL: redefine in outer scope so the test runner can call it -test_unicode_utils() = TestUnicodeUtils.test_unicode_utils() +test_subscripts() = TestSubscripts.test_subscripts() diff --git a/test/suite/unicode/test_superscripts.jl b/test/suite/unicode/test_superscripts.jl new file mode 100644 index 00000000..7bffedb6 --- /dev/null +++ b/test/suite/unicode/test_superscripts.jl @@ -0,0 +1,97 @@ +module TestSuperscripts + +import Test +import CTBase.Exceptions: Exceptions +import CTBase.Unicode: Unicode + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_superscripts() + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscript (single superscript char)" begin + # Exception for out-of-range + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(-1) + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(10) + + # Test all valid superscripts [0..9] + Test.@test Unicode.ctupperscript(0) == '⁰' + Test.@test Unicode.ctupperscript(1) == '¹' + Test.@test Unicode.ctupperscript(2) == '²' + Test.@test Unicode.ctupperscript(3) == '³' + Test.@test Unicode.ctupperscript(4) == '⁴' + Test.@test Unicode.ctupperscript(5) == '⁵' + Test.@test Unicode.ctupperscript(6) == '⁶' + Test.@test Unicode.ctupperscript(7) == '⁷' + Test.@test Unicode.ctupperscript(8) == '⁸' + Test.@test Unicode.ctupperscript(9) == '⁹' + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscripts (multi-digit superscript string)" begin + # Exception for negative input + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscripts(-1) + + # Test zero and multi-digit inputs + Test.@test Unicode.ctupperscripts(0) == "⁰" + Test.@test Unicode.ctupperscripts(19) == "¹⁹" # no leading zero + Test.@test Unicode.ctupperscripts(109) == "¹⁰⁹" + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscript enriched errors" begin + # Test negative value + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(-1) + + try + Unicode.ctupperscript(-1) + Test.@test false # Should not reach here + catch e + Test.@test e isa Exceptions.IncorrectArgument + Test.@test e.got == "-1" + Test.@test e.expected == "0-9" + Test.@test occursin("superscript must be between 0 and 9", e.msg) + Test.@test occursin("ctupperscripts()", e.suggestion) + Test.@test e.context == "Unicode superscript generation" + end + + # Test value too large + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(12) + + try + Unicode.ctupperscript(12) + Test.@test false # Should not reach here + catch e + Test.@test e isa Exceptions.IncorrectArgument + Test.@test e.got == "12" + Test.@test e.expected == "0-9" + Test.@test occursin("ctupperscripts()", e.suggestion) + Test.@test e.context == "Unicode superscript generation" + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscripts enriched errors" begin + # Test negative value + Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscripts(-3) + + try + Unicode.ctupperscripts(-3) + Test.@test false # Should not reach here + catch e + Test.@test e isa Exceptions.IncorrectArgument + Test.@test e.got == "-3" + Test.@test e.expected == "≥ 0" + Test.@test occursin("superscript must be positive", e.msg) + Test.@test e.context == "Unicode superscript string generation" + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "ctupperscripts / ctupperscript consistency" begin + for d in 0:9 + Test.@test Unicode.ctupperscripts(d) == string(Unicode.ctupperscript(d)) + end + end + + return nothing +end + +end # module + +test_superscripts() = TestSuperscripts.test_superscripts() diff --git a/test/suite/unicode/test_unicode_enriched.jl b/test/suite/unicode/test_unicode_enriched.jl deleted file mode 100644 index e1c7f1fa..00000000 --- a/test/suite/unicode/test_unicode_enriched.jl +++ /dev/null @@ -1,144 +0,0 @@ -module TestUnicodeEnriched - -import Test -import CTBase.Unicode -import CTBase.Exceptions - -const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true -const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true - -function test_unicode_enriched() - Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Enriched Unicode Errors" begin - - # ==================================================================== - # ERROR TESTS - Unicode Functions Exception Quality - # ==================================================================== - - Test.@testset "ctindice enriched errors" begin - # Test negative value - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctindice(-1) - - try - Unicode.ctindice(-1) - Test.@test false # Should not reach here - catch e - Test.@test e isa Exceptions.IncorrectArgument - Test.@test e.got == "-1" - Test.@test e.expected == "0-9" - Test.@test occursin("subscript must be between 0 and 9", e.msg) - Test.@test occursin("ctindices()", e.suggestion) - Test.@test e.context == "Unicode subscript generation" - end - - # Test value too large - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctindice(15) - - try - Unicode.ctindice(15) - Test.@test false # Should not reach here - catch e - Test.@test e isa Exceptions.IncorrectArgument - Test.@test e.got == "15" - Test.@test e.expected == "0-9" - Test.@test occursin("ctindices()", e.suggestion) - Test.@test e.context == "Unicode subscript generation" - end - end - - Test.@testset "ctindices enriched errors" begin - # Test negative value - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctindices(-5) - - try - Unicode.ctindices(-5) - Test.@test false # Should not reach here - catch e - Test.@test e isa Exceptions.IncorrectArgument - Test.@test e.got == "-5" - Test.@test e.expected == "≥ 0" - Test.@test occursin("subscript must be positive", e.msg) - Test.@test e.context == "Unicode subscript string generation" - end - end - - Test.@testset "ctupperscript enriched errors" begin - # Test negative value - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(-1) - - try - Unicode.ctupperscript(-1) - Test.@test false # Should not reach here - catch e - Test.@test e isa Exceptions.IncorrectArgument - Test.@test e.got == "-1" - Test.@test e.expected == "0-9" - Test.@test occursin("superscript must be between 0 and 9", e.msg) - Test.@test occursin("ctupperscripts()", e.suggestion) - Test.@test e.context == "Unicode superscript generation" - end - - # Test value too large - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscript(12) - - try - Unicode.ctupperscript(12) - Test.@test false # Should not reach here - catch e - Test.@test e isa Exceptions.IncorrectArgument - Test.@test e.got == "12" - Test.@test e.expected == "0-9" - Test.@test occursin("ctupperscripts()", e.suggestion) - Test.@test e.context == "Unicode superscript generation" - end - end - - Test.@testset "ctupperscripts enriched errors" begin - # Test negative value - Test.@test_throws Exceptions.IncorrectArgument Unicode.ctupperscripts(-3) - - try - Unicode.ctupperscripts(-3) - Test.@test false # Should not reach here - catch e - Test.@test e isa Exceptions.IncorrectArgument - Test.@test e.got == "-3" - Test.@test e.expected == "≥ 0" - Test.@test occursin("superscript must be positive", e.msg) - Test.@test e.context == "Unicode superscript string generation" - end - end - - # ==================================================================== - # UNIT TESTS - Successful Operations - # ==================================================================== - - Test.@testset "Successful Unicode operations" begin - # Test ctindice - Test.@test Unicode.ctindice(0) == '\u2080' - Test.@test Unicode.ctindice(5) == '\u2085' - Test.@test Unicode.ctindice(9) == '\u2089' - - # Test ctindices - Test.@test Unicode.ctindices(0) == "\u2080" - Test.@test Unicode.ctindices(123) == "\u2081\u2082\u2083" - - # Test ctupperscript - Test.@test Unicode.ctupperscript(0) == '\u2070' - Test.@test Unicode.ctupperscript(1) == '\u00B9' - Test.@test Unicode.ctupperscript(2) == '\u00B2' - Test.@test Unicode.ctupperscript(3) == '\u00B3' - Test.@test Unicode.ctupperscript(5) == '\u2075' - - # Test ctupperscripts - Test.@test Unicode.ctupperscripts(0) == "\u2070" - Test.@test Unicode.ctupperscripts(123) == "\u00B9\u00B2\u00B3" - end - end - - return nothing -end - -end # module - -# Export to outer scope for TestRunner -test_unicode_enriched() = TestUnicodeEnriched.test_unicode_enriched()