diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 55f14e5e..bed9d2fb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -6,7 +6,8 @@ on: - main tags: '*' pull_request: - + types: [labeled, synchronize, opened, reopened] + jobs: call: if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run ci') diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 8d3837d4..0353aec8 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -6,7 +6,8 @@ on: - main tags: '*' pull_request: - + types: [labeled, synchronize, opened, reopened] + jobs: call: if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run documentation') diff --git a/BREAKINGS.md b/BREAKINGS.md index 78490b94..15dde7d0 100644 --- a/BREAKINGS.md +++ b/BREAKINGS.md @@ -2,6 +2,18 @@ This document outlines all breaking changes introduced in CTBase v0.18.0-beta compared to v0.17.4. Use this guide to migrate your code and understand the impact of these changes. +## Non-breaking note (0.18.15-beta) + +- **Philosophy documentation**: Added comprehensive code philosophy documentation in `dev/philosophy/` covering modules, types/traits, exceptions, docstrings, testing, and documentation standards. No API changes; purely documentation additions. +- **Agent guides**: Added `AGENTS.md` and `CLAUDE.md` for agent navigation and project context. No API changes; purely documentation additions. +- **Documentation build improvements**: Changed `docs/make.jl` build method and fixed cross-references. No API changes; purely documentation improvements. +- **Typed exceptions**: Replaced untyped `error()` and `ArgumentError` with structured CTBase exceptions in `ext/` files. No API changes; internal error handling improvement. +- **Import qualification**: Qualified imports in submodules (`using DocStringExtensions` β†’ `import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES`). No API changes; internal code quality improvement. +- **Code cleanup**: Removed dead ternary branches, fixed byte-indexing, removed circular imports. No API changes; internal code quality improvement. +- **TestRunner auto-discovery fix**: Fixed non-recursive test discovery in auto-discovery mode. No API changes; bug fix. +- **Docstring refactoring**: Rewrote ExtensionError and SolverFailure docstrings with `$(TYPEDEF)` and standardized sections. No API changes; documentation improvement. +- **No migration required**: All changes are internal or documentation-only. No breaking changes. + ## Non-breaking note (0.18.14-beta) - **TestRunner progress display refactoring**: Renamed `progress` parameter to `show_progress_line` for clarity, and added new `show_progress_bar` parameter for granular control. Users with `progress=false` should change to `show_progress_line=false`. Users with `progress=true` (default) can keep using defaults or set `show_progress_line=true, show_progress_bar=false` for minimal display without the graphical bar. No breaking changes; purely parameter rename with backward-compatible defaults. Migration: replace `progress=` with `show_progress_line=`. diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 3eb08b1d..76517fb5 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -5,6 +5,71 @@ All notable changes to CTBase will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.18.15-beta] - 2026-06-06 + +### πŸ“š Documentation + +#### **Philosophy Documentation** + +- **Added philosophy documentation**: Added comprehensive code philosophy documentation in `dev/philosophy/` + - `modules.md`: Submodule organization, imports/qualification, DAG, exports + - `types-traits-interfaces.md`: Types vs traits, interfaces/contracts, SOLID/DRY/YAGNI, type stability + - `exceptions.md`: The 7 exceptions and the choice rule + - `docstrings.md`: Docstring templates, cross-references, example safety + - `testing.md`: Categories, fakes/stubs, module + callable function template + - `documentation.md`: API generation, guides, draft workflow +- **Added agent guides**: Added `AGENTS.md` (agent navigation guide) and `CLAUDE.md` (Claude project context) +- **Added planning template**: Added `dev/planning.md` for implementation plans +- **Added operational rules**: Added `dev/RULES.md` for MCP, doc build, git, and output capture procedures + +#### **Documentation Build Improvements** + +- **Faster builds**: Changed `docs/make.jl` from `Pkg.activate/instantiate` to `pushfirst!(LOAD_PATH, ...)` for faster builds +- **Fixed cross-references**: Fixed unresolved `@ref` links for extension tags (AbstractCoveragePostprocessingTag, TestRunnerTag, AbstractTestRunnerTag) +- **Added examples**: Added example code blocks for extension tags in docstrings +- **Refactored docstrings**: Rewrote ExtensionError and SolverFailure docstrings with `$(TYPEDEF)`, standardized sections, and cross-references + +### πŸ› Bug Fixes + +#### **TestRunner Auto-Discovery** + +- **Fixed non-recursive discovery**: Fixed test discovery in auto-discovery mode to use `_collect_test_files_recursive` instead of flat `readdir` +- **Impact**: Tests in subdirectories were silently ignored in auto-discovery mode; now properly collected recursively + +#### **Typed Exceptions (Tenet 6)** + +- **Replaced untyped errors**: Replaced all `error()` and `ArgumentError` with structured CTBase exceptions across `ext/` files + - `ext/TestRunner.jl`: 7 replacements (IncorrectArgument, PreconditionError) + - `ext/CoveragePostprocessing.jl`: 4 replacements (PreconditionError) + - `ext/DocumenterReference.jl`: 2 replacements (IncorrectArgument) +- **Fixed invalid Julia syntax**: Fixed 2 occurrences of `catch e::CTBase.Exceptions.CTException` β†’ `catch e` + `e isa ... || rethrow()` +- **Fixed docstring output**: Corrected incorrect output in `_progress_bar` docstring (width=20 β†’ 10 blocks) +- **Updated tests**: Updated 7 test files to reflect new exception types + +### 🧹 Code Quality + +#### **Import Qualification** + +- **Qualified imports**: Changed `using DocStringExtensions` to `import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES` in all submodules +- **Qualified Coverage import**: Changed `using Coverage` to `using Coverage: Coverage` in CoveragePostprocessing +- **Removed circular import**: Removed `using CTBase` from src/Exceptions/Exceptions.jl +- **Removed redundant imports**: Removed redundant `using DocStringExtensions` from src/Descriptions/types.jl + +#### **Code Cleanup** + +- **Removed dead ternary branches**: Simplified `_normalize_selections` and `_builder_to_string` in TestRunner +- **Fixed byte-indexing**: Changed path slicing from byte-indexing to `relpath()` in CoveragePostprocessing +- **Fixed collision risk**: Normalized flat names in cov file flattening to prevent collisions + +### πŸ§ͺ Testing + +- **All tests pass**: 1161/1161 tests pass +- **Documentation builds**: Documentation builds successfully with no extension errors + +### 🧹 Maintenance + +- **Version bump**: Bumped to 0.18.15-beta for development. + ## [0.18.14-beta] - 2026-05-30 ### ✨ New Features diff --git a/Project.toml b/Project.toml index d855d53a..671baba5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTBase" uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" -version = "0.18.14-beta" +version = "0.18.15-beta" authors = ["Olivier Cots ", "Jean-Baptiste Caillau "] [deps] diff --git a/docs/Project.toml b/docs/Project.toml index 9539b6b4..79b107af 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,5 @@ [deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" diff --git a/docs/README.md b/docs/README.md index e558b411..ab3c0018 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,38 +8,29 @@ The documentation is built using [Documenter.jl](https://github.com/JuliaDocs/Do ## Building the Documentation -There are several ways to build the documentation locally. - -### 1. Terminal One-Liner (Recommended) - -From the root of the `CTBase.jl` repository, run: +From the root of the repository: ```bash -julia --project=docs/ -e 'using Pkg; Pkg.develop(path=pwd()); include("docs/make.jl"); Pkg.rm("CTBase")' +julia --project=. docs/make.jl ``` -This command: -- Activates the `docs` project environment. -- Temporarily "develops" the current package so changes are reflected in the build. -- Executes `make.jl` to build the site. -- Cleans up the `docs` environment by removing the temporary link to `CTBase`. - -### 2. Manual REPL Build +`make.jl` prepends both `docs/` and the package root to `LOAD_PATH`, so the package +project is picked up automatically β€” no `Pkg.develop` step needed. -If you prefer working inside the Julia REPL: +### Draft mode (fast link validation) -```julia -# 1. Activate the docs project -using Pkg -Pkg.activate("docs/") +Set `draft = true` at the top of `docs/make.jl` to skip `@repl`/`@example` block +execution. This is much faster when iterating on cross-references and page structure: -# 2. Add CTBase in development mode (if not already done) -Pkg.develop(path=pwd()) - -# 3. Build the documentation -include("docs/make.jl") +```bash +# edit docs/make.jl: draft = true (line 16) +julia --project=. docs/make.jl ``` +!!! note "warnonly" + `make.jl` uses `warnonly=[:cross_references]`, so cross-reference warnings do not + abort the build. Other errors (missing pages, broken `@repl` blocks) still fail. + ## Viewing the Documentation After a successful build, the generated HTML files are located in `docs/build/`. You can open `index.html` in your browser: @@ -54,7 +45,7 @@ xdg-open docs/build/index.html ## Directory Structure -- `src/`: Contains the manual markdown files (Introduction, Tutorials, etc.). +- `src/`: Contains the manual markdown files (Introduction, Getting Started, Guides, etc.). - `make.jl`: The main build script for Documenter. - `api_reference.jl`: Contains the logic for automatic API reference generation. It extracts docstrings from the source code and creates temporary markdown files. - `build/`: The directory where the static website is generated (ignored by git). diff --git a/docs/make.jl b/docs/make.jl index 1b5f0854..3c3efdb5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,7 +1,7 @@ -using Pkg -Pkg.activate(@__DIR__) -Pkg.develop(PackageSpec(; path=joinpath(@__DIR__, ".."))) -Pkg.instantiate() +# to run the documentation generation: +# julia --project=. docs/make.jl +pushfirst!(LOAD_PATH, joinpath(@__DIR__)) +pushfirst!(LOAD_PATH, joinpath(@__DIR__, "..")) using Documenter using CTBase @@ -54,7 +54,7 @@ with_api_reference(src_dir) do api_pages makedocs(; draft=draft, remotes=nothing, # Disable remote links. Needed for DocumenterReference - warnonly=true, + warnonly=[:cross_references], sitename="CTBase.jl", format=Documenter.HTML(; repolink="https://" * repo_url, @@ -67,6 +67,7 @@ with_api_reference(src_dir) do api_pages ), pages=[ "Introduction" => "index.md", + "Getting Started" => "getting-started.md", "User Guides" => [ "Descriptions" => joinpath("guide", "descriptions.md"), "Exceptions" => joinpath("guide", "exceptions.md"), diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md new file mode 100644 index 00000000..b78f06a6 --- /dev/null +++ b/docs/src/getting-started.md @@ -0,0 +1,114 @@ +```@meta +CurrentModule = CTBase +``` + +# Getting Started + +## Installation + +CTBase.jl is typically installed as a dependency of another package in the ecosystem +(e.g. [OptimalControl.jl](https://github.com/control-toolbox/OptimalControl.jl)). +To install it directly: + +```julia +import Pkg +Pkg.add("CTBase") +``` + +**Requires Julia β‰₯ 1.10.** + +## Mental Model + +CTBase is the **base layer** of the control-toolbox ecosystem. +It provides infrastructure shared by every package above it. + +Three things to keep in mind: + +1. **No top-level exports.** `using CTBase` loads the package but brings no symbols + into scope. Every symbol is accessed via its qualified path: + ```julia + CTBase.Descriptions.add # βœ“ always works + CTBase.Exceptions.NotImplemented + ``` +2. **Submodule-first API.** The public API lives in named submodules + (`Descriptions`, `Exceptions`, `Extensions`, `Core`, `Unicode`). + You can bring a submodule's exports into scope explicitly: + ```julia + using CTBase.Exceptions # brings IncorrectArgument, NotImplemented, … into scope + ``` +3. **Extension-backed features.** `run_tests`, `postprocess_coverage`, and + `automatic_reference_documentation` require loading the matching weak dependency + (`Test`, `Coverage`, `Documenter` respectively) before they become active. + +## 5-Minute Walkthrough + +### Working with Descriptions + +A *description* is a `Tuple` of `Symbol`s that declaratively identifies an algorithm +or configuration. Catalogues collect known descriptions; `complete` resolves a partial +description to an exact one. + +```@repl walkthrough +using CTBase + +# Build a catalogue +descs = CTBase.Descriptions.add((), (:euler, :explicit)) +descs = CTBase.Descriptions.add(descs, (:euler, :implicit)) +descs = CTBase.Descriptions.add(descs, (:runge_kutta, :explicit)) + +# Partial completion: find the unique entry containing :implicit +CTBase.Descriptions.complete(:implicit; descriptions=descs) + +# Ambiguous completion raises AmbiguousDescription +try + CTBase.Descriptions.complete(:euler; descriptions=descs) +catch e + println(typeof(e)) +end +``` + +For more, see the **[Descriptions guide](guide/descriptions.md)**. + +### Working with Exceptions + +CTBase defines a typed exception hierarchy rooted at +[`CTBase.Exceptions.CTException`](@ref). +Each type carries structured context fields for actionable error messages. + +```@repl walkthrough +# IncorrectArgument β€” invalid input value +try + throw(CTBase.Exceptions.IncorrectArgument( + "state dimension must be positive"; + got="0", + expected="n > 0", + suggestion="Pass a positive integer for the state dimension", + )) +catch e + println(e) +end + +# NotImplemented β€” interface stub +try + throw(CTBase.Exceptions.NotImplemented( + "solve! is not implemented"; + required_method="solve!(::MyStrategy, ocp)", + suggestion="Import the package that provides this strategy", + )) +catch e + println(typeof(e)) +end +``` + +For more, see the **[Exceptions guide](guide/exceptions.md)**. + +## Next Steps + +| Topic | Guide | +|:---|:---| +| Descriptions catalogue and completion | [Descriptions](guide/descriptions.md) | +| Exception hierarchy and best practices | [Exceptions](guide/exceptions.md) | +| Modular test runner setup | [Test Runner](guide/test-runner.md) | +| Coverage report generation | [Coverage](guide/coverage.md) | +| Auto-generated API reference | [API Documentation](guide/api-documentation.md) | +| Full API reference | API Reference (left sidebar) | diff --git a/docs/src/guide/api-documentation.md b/docs/src/guide/api-documentation.md index ca37f359..03bb8aa3 100644 --- a/docs/src/guide/api-documentation.md +++ b/docs/src/guide/api-documentation.md @@ -1,10 +1,14 @@ +```@meta +CurrentModule = CTBase +``` + # API Documentation Guide This guide explains how to set up automated API reference documentation generation using the **DocumenterReference** extension of `CTBase.jl`. This is particularly useful for maintaining comprehensive and up-to-date API documentation as your codebase evolves. ## Overview -The `DocumenterReference` extension provides the `CTBase.automatic_reference_documentation()` function, which automatically generates API reference pages from your Julia source code. It: +The `DocumenterReference` extension provides the [`CTBase.automatic_reference_documentation`](@ref) function, which automatically generates API reference pages from your Julia source code. It: - Extracts docstrings from your modules - Separates public and private APIs diff --git a/docs/src/guide/coverage.md b/docs/src/guide/coverage.md index 2d571998..4e4ec5e5 100644 --- a/docs/src/guide/coverage.md +++ b/docs/src/guide/coverage.md @@ -1,3 +1,7 @@ +```@meta +CurrentModule = CTBase +``` + # Coverage Post-processing Guide This guide explains how to generate human-readable and machine-parseable coverage reports using the **CoveragePostprocessing** extension of `CTBase.jl`. @@ -25,7 +29,7 @@ To generate actionable coverage reports, we use a dedicated `coverage.jl` script pushfirst!(LOAD_PATH, @__DIR__) using Pkg -using CTBase # Provides postprocess_coverage +using CTBase # Provides CTBase.postprocess_coverage using Coverage # This function: @@ -33,6 +37,7 @@ using Coverage # 2. Generates an LCOV file (coverage/lcov.info). # 3. Generates a markdown summary (coverage/cov_report.md). # 4. Archives used .cov files to keep the directory clean. +# See: CTBase.postprocess_coverage CTBase.postprocess_coverage(; root_dir=dirname(@__DIR__) # Point to the package root ) diff --git a/docs/src/guide/descriptions.md b/docs/src/guide/descriptions.md index 12d1c230..1eb21809 100644 --- a/docs/src/guide/descriptions.md +++ b/docs/src/guide/descriptions.md @@ -1,137 +1,85 @@ -# Descriptions: encoding algorithms - -One of the central ideas in `CTBase.jl` is the notion of a **description**. -A description is simply a tuple of `Symbol`s that encodes an algorithm or -configuration in a declarative way. - -Formally, CTBase defines: - -```julia -const DescVarArg = Vararg{Symbol} -const Description = Tuple{DescVarArg} +```@meta +CurrentModule = CTBase ``` -For example, the tuple +# Descriptions: encoding algorithms -```julia-repl -julia> using CTBase +A *description* is a [`CTBase.Descriptions.Description`](@ref) β€” a `Tuple` of `Symbol`s +that declaratively encodes an algorithm or configuration. -julia> d = (:descent, :bfgs, :bisection) -(:descent, :bfgs, :bisection) +```@repl desc +using CTBase -julia> typeof(d) <: CTBase.Description -true +d = (:descent, :bfgs, :bisection) +typeof(d) <: CTBase.Descriptions.Description ``` -can be read as β€œa descent algorithm, with BFGS directions and a bisection -line search”. Higher-level packages in the control-toolbox ecosystem use -descriptions to catalogue algorithms in a uniform way. - -## Building a library of descriptions +Higher-level packages use descriptions to catalogue algorithms in a uniform, +string-free, and composable way. -CTBase provides a few small functions to manage collections of descriptions: +## Building a catalogue -- `CTBase.add(x, y)` adds the description `y` to the tuple of descriptions `x`, - rejecting duplicates with an `IncorrectArgument` exception. -- `CTBase.complete(list; descriptions=D)` picks a complete description from a - set `D` based on a partial list of symbols. -- `CTBase.remove(x, y)` returns the set difference of two descriptions. +[`CTBase.Descriptions.add`](@ref) adds a description to a catalogue (a +`Tuple{Vararg{Description}}`). Start with an empty tuple `()`: -Here is a complete example of a small β€œalgorithm library”: - -```julia-repl -julia> algorithms = () -() - -julia> algorithms = CTBase.add(algorithms, (:descent, :bfgs, :bisection)) -((:descent, :bfgs, :bisection),) - -julia> algorithms = CTBase.add(algorithms, (:descent, :gradient, :fixedstep)) -((:descent, :bfgs, :bisection), (:descent, :gradient, :fixedstep)) - -julia> display(algorithms) -(:descent, :bfgs, :bisection) -(:descent, :gradient, :fixedstep) +```@repl desc +algorithms = () +algorithms = CTBase.Descriptions.add(algorithms, (:descent, :bfgs, :bisection)) +algorithms = CTBase.Descriptions.add(algorithms, (:descent, :gradient, :fixedstep)) +display(algorithms) ``` -Given this library, we can **complete** a partial description: - -```julia-repl -julia> CTBase.complete((:descent,); descriptions=algorithms) -(:descent, :bfgs, :bisection) +Attempting to add a duplicate raises [`CTBase.Exceptions.IncorrectArgument`](@ref): -julia> CTBase.complete((:gradient, :fixedstep); descriptions=algorithms) -(:descent, :gradient, :fixedstep) +```@repl desc +try + CTBase.Descriptions.add(algorithms, (:descent, :bfgs, :bisection)) +catch e + println(e) +end ``` -Internally, `CTBase.complete` scans the `descriptions` tuple from top to -bottom. For each candidate description it computes: - -- how many symbols it shares with the partial list, and -- whether the partial list is a subset of the full description. +## Completing a partial description -If no description contains all the symbols from the partial list, -`AmbiguousDescription` is thrown. Otherwise, among the descriptions that do -contain the partial list, CTBase selects the one with the largest -intersection; if several have the same score, the **first** one in the -`descriptions` tuple wins. In other words, the order of `descriptions` -encodes a priority from top to bottom. +[`CTBase.Descriptions.complete`](@ref) picks the unique catalogue entry that contains +all provided symbols: -With this mechanism in place, we can then analyse the *remainder* of a -description by removing a prefix: - -```julia-repl -julia> full = CTBase.complete((:descent,); descriptions=algorithms) -(:descent, :bfgs, :bisection) - -julia> CTBase.remove(full, (:descent, :bfgs)) -(:bisection,) +```@repl desc +CTBase.Descriptions.complete(:bisection; descriptions=algorithms) +CTBase.Descriptions.complete(:gradient, :fixedstep; descriptions=algorithms) ``` -This β€œdescription language” lets higher-level packages refer to algorithms in a -structured, composable way, while CTBase takes care of the low-level -operations (adding, completing, and comparing descriptions). - -## Function Reference - -| Function | Purpose | Returns | Throws | -|----------|---------|---------|--------| -| `add(x, y)` | Add description `y` to collection `x` | Updated tuple | `IncorrectArgument` if duplicate | -| `complete(list; descriptions=D)` | Find complete description matching partial list | Complete description | `AmbiguousDescription` if no match | -| `remove(x, y)` | Remove symbols in `y` from description `x` | Remaining symbols | - | - -## Error Handling - -The description system uses CTBase exceptions to signal problems: - -### Duplicate Descriptions - -```@repl -using CTBase -algorithms = CTBase.add((), (:a, :b)) -CTBase.add(algorithms, (:a, :b)) # Error: duplicate +Among all entries that are a superset of the requested symbols, the one with the +largest overlap is returned (first wins on tie). If no entry matches, +[`CTBase.Exceptions.AmbiguousDescription`](@ref) is raised: + +```@repl desc +try + CTBase.Descriptions.complete(:euler; descriptions=algorithms) +catch e + println(typeof(e)) + println(e) +end ``` -This throws `IncorrectArgument` because adding a duplicate would create ambiguity. +## Removing symbols from a description -### Ambiguous or Invalid Descriptions +[`CTBase.Descriptions.remove`](@ref) returns a new description with specified symbols +removed β€” useful for extracting the remainder after stripping a known prefix: -```@repl -using CTBase -D = ((:a, :b), (:c, :d)) -CTBase.complete((:x,); descriptions=D) # Error: no match +```@repl desc +full = CTBase.Descriptions.complete(:bisection; descriptions=algorithms) +CTBase.Descriptions.remove(full, (:descent, :bfgs)) ``` -This throws `AmbiguousDescription` when no description in the library contains all the requested symbols. - -## Best Practices +## Function Reference -1. **Order matters**: Place more specific descriptions first in your library -2. **Use meaningful symbols**: Choose symbols that clearly describe the algorithm -3. **Keep it simple**: Descriptions should be short and focused -4. **Handle errors**: Always catch `AmbiguousDescription` when using `complete` -5. **Document your descriptions**: Maintain a list of valid descriptions for your package +| Function | Purpose | Throws | +|:---|:---|:---| +| [`CTBase.Descriptions.add`](@ref) | Add a description to a catalogue | [`CTBase.Exceptions.IncorrectArgument`](@ref) on duplicate | +| [`CTBase.Descriptions.complete`](@ref) | Complete a partial description | [`CTBase.Exceptions.AmbiguousDescription`](@ref) on no/ambiguous match | +| [`CTBase.Descriptions.remove`](@ref) | Remove symbols from a description | β€” | ## See Also -- [Exception Handling](exceptions.md): Understanding CTBase exceptions +- [Exceptions guide](exceptions.md) β€” understanding `IncorrectArgument` and `AmbiguousDescription`. diff --git a/docs/src/guide/exceptions.md b/docs/src/guide/exceptions.md index 15ff3463..d2a3b91c 100644 --- a/docs/src/guide/exceptions.md +++ b/docs/src/guide/exceptions.md @@ -1,18 +1,22 @@ +```@meta +CurrentModule = CTBase +``` + # Error Handling and CTBase Exceptions CTBase defines a small hierarchy of domain-specific exceptions to make error handling explicit and consistent across the control-toolbox ecosystem. -All custom exceptions inherit from `CTBase.CTException`: +All custom exceptions inherit from [`CTBase.Exceptions.CTException`](@ref): ```julia -abstract type CTBase.CTException <: Exception end +abstract type CTBase.Exceptions.CTException <: Exception end ``` ## Exception Hierarchy ```text -CTException (abstract) +CTBase.Exceptions.CTException (abstract) β”œβ”€β”€ IncorrectArgument # Input validation errors β”œβ”€β”€ PreconditionError # Order of operations, state validation β”œβ”€β”€ NotImplemented # Unimplemented interface methods @@ -30,7 +34,7 @@ You should generally catch exceptions like this: try # call into CTBase or a package built on top of it catch e - if e isa CTBase.CTException + if e isa CTBase.Exceptions.CTException # handle CTBase domain errors in a uniform way @warn "CTBase error" exception=(e, catch_backtrace()) else @@ -48,7 +52,7 @@ giving you a single place to handle all CTBase-specific problems. ### [IncorrectArgument](@id incorrect-argument-tutorial) ```julia -CTBase.IncorrectArgument <: CTBase.CTException +CTBase.Exceptions.IncorrectArgument <: CTBase.Exceptions.CTException ``` **When to use**: Thrown when an individual argument is invalid or violates a constraint. @@ -67,15 +71,15 @@ Adding a duplicate description: ```@repl using CTBase -algorithms = CTBase.add((), (:a, :b)) -CTBase.add(algorithms, (:a, :b)) # Error: duplicate +algorithms = CTBase.Descriptions.add((), (:a, :b)) +CTBase.Descriptions.add(algorithms, (:a, :b)) # Error: duplicate ``` Using invalid indices for the Unicode helpers: ```@repl using CTBase -CTBase.ctindice(-1) # Error: must be between 0 and 9 +CTBase.Unicode.ctindice(-1) # Error: must be between 0 and 9 ``` **Use this exception** whenever *one input value* is outside the allowed domain @@ -84,7 +88,7 @@ CTBase.ctindice(-1) # Error: must be between 0 and 9 ### [AmbiguousDescription](@id ambiguous-description-tutorial) ```julia -CTBase.AmbiguousDescription <: CTBase.CTException +CTBase.Exceptions.AmbiguousDescription <: CTBase.Exceptions.CTException ``` **When to use**: Thrown when a description (a tuple of `Symbol`s) cannot be matched to any known @@ -102,7 +106,7 @@ valid description. ```@repl using CTBase D = ((:a, :b), (:a, :b, :c), (:b, :c)) -CTBase.complete(:f; descriptions=D) # Error: no match found +CTBase.Descriptions.complete(:f; descriptions=D) # Error: no match found ``` **Use this exception** when *the high-level choice of description itself* is wrong @@ -113,7 +117,7 @@ or ambiguous and there is no sensible default. ### [PreconditionError](@id precondition-error-tutorial) ```julia -CTBase.PreconditionError <: CTBase.CTException +CTBase.Exceptions.PreconditionError <: CTBase.Exceptions.CTException ``` **When to use**: Thrown when a function is called in the wrong order or when the system is in an invalid state. @@ -132,7 +136,7 @@ System initialization order: ```julia function configure!(state::SystemState, config::Dict) if !state.initialized - throw(CTBase.PreconditionError( + throw(CTBase.Exceptions.PreconditionError( "System must be initialized before configuration", reason="initialize! not called yet", suggestion="Call initialize!(state) before configure!", @@ -148,7 +152,7 @@ State validation: ```julia function dynamics!(ocp::PreModel, f::Function) if !__is_state_set(ocp) - throw(CTBase.PreconditionError( + throw(CTBase.Exceptions.PreconditionError( "State must be set before defining dynamics", reason="state has not been defined yet", suggestion="Call state!(ocp, dimension) before dynamics!", @@ -168,15 +172,15 @@ end **Distinction from `IncorrectArgument`**: -- `IncorrectArgument`: The *value* of an argument is wrong -- `PreconditionError`: The *timing* or *state* is wrong +- [`CTBase.Exceptions.IncorrectArgument`](@ref): The *value* of an argument is wrong +- [`CTBase.Exceptions.PreconditionError`](@ref): The *timing* or *state* is wrong ## Implementation Exceptions ### [NotImplemented](@id not-implemented-tutorial) ```julia -CTBase.NotImplemented <: CTBase.CTException +CTBase.Exceptions.NotImplemented <: CTBase.Exceptions.CTException ``` **When to use**: Used to mark interface points that must be implemented by concrete subtypes. @@ -197,7 +201,7 @@ The typical pattern is to provide a method on an abstract type that throws abstract type MyAbstractAlgorithm end function run!(algo::MyAbstractAlgorithm, state) - throw(CTBase.NotImplemented( + throw(CTBase.Exceptions.NotImplemented( "run! is not implemented for $(typeof(algo))", required_method="run!(::$(typeof(algo)), state)", suggestion="Implement run! for your algorithm type", @@ -221,7 +225,7 @@ typed error rather than a generic `error("TODO")`. ### [ParsingError](@id parsing-error-tutorial) ```julia -CTBase.ParsingError <: CTBase.CTException +CTBase.Exceptions.ParsingError <: CTBase.Exceptions.CTException ``` **When to use**: Intended for errors detected during parsing of input structures or DSLs @@ -238,7 +242,7 @@ CTBase.ParsingError <: CTBase.CTException ```@repl using CTBase -throw(CTBase.ParsingError( +throw(CTBase.Exceptions.ParsingError( "unexpected token 'end'", location="line 42, column 10", suggestion="Check for unmatched 'begin' or remove extra 'end'", @@ -251,7 +255,7 @@ throw(CTBase.ParsingError( ### [ExtensionError](@id extension-error-tutorial) ```julia -CTBase.ExtensionError <: CTBase.CTException +CTBase.Exceptions.ExtensionError <: CTBase.Exceptions.CTException ``` **When to use**: Thrown when a feature requires optional dependencies (weak dependencies) that are not loaded. @@ -267,7 +271,7 @@ CTBase.ExtensionError <: CTBase.CTException ```julia function plot_results(data) - throw(CTBase.ExtensionError( + throw(CTBase.Exceptions.ExtensionError( :Plots, feature="result visualization", context="plot_results function" @@ -294,7 +298,7 @@ The enriched display automatically suggests: ### [SolverFailure](@id solver-failure-tutorial) ```julia -CTBase.SolverFailure <: CTBase.CTException +CTBase.Exceptions.SolverFailure <: CTBase.Exceptions.CTException ``` **When to use**: Thrown when a solver (ODE integrator, optimization solver, linear solver, etc.) @@ -313,7 +317,7 @@ fails to complete successfully or returns an error code. function integrate_ode(system, integrator) result = solve(system, integrator) if result.retcode != :Success - throw(CTBase.SolverFailure( + throw(CTBase.Exceptions.SolverFailure( "ODE integration failed", retcode=string(result.retcode), suggestion="Reduce time step or check initial conditions", @@ -348,21 +352,21 @@ The enriched display shows the solver-specific return code: **Distinction from other exceptions**: -- `IncorrectArgument`: The *input* is invalid -- `PreconditionError`: The *state* or *timing* is wrong -- `SolverFailure`: The *numerical computation* itself failed +- [`CTBase.Exceptions.IncorrectArgument`](@ref): The *input* is invalid +- [`CTBase.Exceptions.PreconditionError`](@ref): The *state* or *timing* is wrong +- [`CTBase.Exceptions.SolverFailure`](@ref): The *numerical computation* itself failed ## Quick Reference: Which Exception to Use? | Situation | Exception | Example | |-----------|-----------|---------| -| Invalid argument value | `IncorrectArgument` | `throw(IncorrectArgument("x must be > 0", got="-5", expected="> 0"))` | -| Wrong function call order | `PreconditionError` | `throw(PreconditionError("Must initialize before configure"))` | -| Unimplemented interface | `NotImplemented` | `throw(NotImplemented("run! not implemented for MyType"))` | -| Parsing error | `ParsingError` | `throw(ParsingError("unexpected token", location="line 10"))` | -| Ambiguous description | `AmbiguousDescription` | `throw(AmbiguousDescription((:x,), candidates=["(:a,:b)", "(:c,:d)"]))` | -| Missing optional dependency | `ExtensionError` | `throw(ExtensionError(:Plots, feature="plotting"))` | -| Solver/integrator failure | `SolverFailure` | `throw(SolverFailure("ODE failed", retcode=":Unstable"))` | +| Invalid argument value | [`CTBase.Exceptions.IncorrectArgument`](@ref) | `throw(IncorrectArgument("x must be > 0", got="-5", expected="> 0"))` | +| Wrong function call order | [`CTBase.Exceptions.PreconditionError`](@ref) | `throw(PreconditionError("Must initialize before configure"))` | +| Unimplemented interface | [`CTBase.Exceptions.NotImplemented`](@ref) | `throw(NotImplemented("run! not implemented for MyType"))` | +| Parsing error | [`CTBase.Exceptions.ParsingError`](@ref) | `throw(ParsingError("unexpected token", location="line 10"))` | +| Ambiguous description | [`CTBase.Exceptions.AmbiguousDescription`](@ref) | `throw(AmbiguousDescription((:x,), candidates=["(:a,:b)", "(:c,:d)"]))` | +| Missing optional dependency | [`CTBase.Exceptions.ExtensionError`](@ref) | `throw(ExtensionError(:Plots, feature="plotting"))` | +| Solver/integrator failure | [`CTBase.Exceptions.SolverFailure`](@ref) | `throw(SolverFailure("ODE failed", retcode=":Unstable"))` | ## Enriched Error Display diff --git a/docs/src/guide/test-runner.md b/docs/src/guide/test-runner.md index 05e8da1f..c2fd9e23 100644 --- a/docs/src/guide/test-runner.md +++ b/docs/src/guide/test-runner.md @@ -1,6 +1,11 @@ +```@meta +CurrentModule = CTBase +``` + # Test Runner Guide -This guide explains how to set up a modular testing infrastructure for Julia packages using the **TestRunner** extension of `CTBase.jl`. This setup enables granular test execution and is friendly both for human developers and AI agents. +This guide explains how to set up a modular testing infrastructure for Julia packages using the **TestRunner** extension of `CTBase.jl`. +The entry point is [`CTBase.run_tests`](@ref), activated by loading the `Test` weak dependency. This setup enables granular test execution and is friendly both for human developers and AI agents. ## Architecture Overview @@ -32,7 +37,7 @@ MyPackage.jl/ ## Setting up `runtests.jl` -The `runtests.jl` file is the entry point for your test suite. By using `CTBase.run_tests`, you enable a powerful mechanism to filter and execute specific tests using command-line arguments. This is crucial for fast iteration cycles. +The `runtests.jl` file is the entry point for your test suite. By using [`CTBase.run_tests`](@ref), you enable a powerful mechanism to filter and execute specific tests using command-line arguments. This is crucial for fast iteration cycles. ### Example `test/runtests.jl` @@ -222,7 +227,7 @@ The progress line is also automatically disabled when a custom `on_test_done` ca ## Callbacks -The `on_test_start` and `on_test_done` callbacks allow custom actions during the test lifecycle. Both receive a `TestRunInfo` struct. +The `on_test_start` and `on_test_done` callbacks allow custom actions during the test lifecycle. Both receive a [`TestRunner.TestRunInfo`](@ref) struct. ### `TestRunInfo` @@ -421,5 +426,5 @@ jobs: ## See Also -- [Exception Handling](exceptions.md): Understanding test failures and exceptions -- [Coverage Guide](coverage.md): Measuring test coverage +- [Exceptions guide](exceptions.md) β€” understanding test failures and exceptions. +- [Coverage guide](coverage.md) β€” measuring test coverage. diff --git a/docs/src/index.md b/docs/src/index.md index d9beeb71..ec36d39b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,59 +1,56 @@ -# CTBase.jl +# CTBase.jl β€” Ecosystem Foundation ```@meta CurrentModule = CTBase ``` -The `CTBase.jl` package is part of the [control-toolbox ecosystem](https://github.com/control-toolbox). +CTBase.jl is the foundational package of the [control-toolbox](https://github.com/control-toolbox) ecosystem. +It provides the **base layer** shared by all packages: common types, structured exceptions, description management, extension infrastructure, and developer tools. -It provides the core types, utilities, and infrastructure used by other packages in the ecosystem, such as [OptimalControl.jl](https://github.com/control-toolbox/OptimalControl.jl). +!!! note "Qualified access" + CTBase exports **no symbols** at the package level. Every public symbol is accessed + via its full qualified path, e.g. `CTBase.Exceptions.IncorrectArgument` or + `CTBase.Descriptions.add`. This makes the origin of every symbol explicit at every + call site and prevents namespace collisions between packages. -!!! note + Downstream packages (e.g. [OptimalControl.jl](https://github.com/control-toolbox/OptimalControl.jl)) + may re-export selected symbols for convenience. - The root package is [OptimalControl.jl](https://github.com/control-toolbox/OptimalControl.jl) which aims to provide tools to model and solve optimal control problems with ordinary differential equations by direct and indirect methods, both on CPU and GPU. +## Submodule overview -## Features and User Guides +| Submodule | Role | +|:---|:---| +| [`CTBase.Core`](@ref) | Fundamental numeric type alias (`ctNumber`) and internal display helpers | +| [`CTBase.Descriptions`](@ref) | Symbolic description tuples: catalogue management, pattern completion, similarity search | +| [`CTBase.Exceptions`](@ref) | Typed exception hierarchy with rich context fields | +| [`CTBase.Extensions`](@ref) | Tag-based extension dispatch for `run_tests`, `postprocess_coverage`, and `automatic_reference_documentation` | +| [`CTBase.Unicode`](@ref) | Unicode subscript/superscript helpers for display | -CTBase provides several key features to build robust control-toolbox packages: +## Quick Start -- **[Descriptions: encoding algorithms](guide/descriptions.md)**: A declarative way to encode algorithms or configurations using tuples of symbols. -- **[Error handling and Exceptions](guide/exceptions.md)**: A domain-specific exception hierarchy for consistent error reporting. -- **[Test Runner](guide/test-runner.md)**: A modular test runner for granular test execution. -- **[Coverage Post-processing](guide/coverage.md)**: Tools to generate readable coverage reports. -- **[API Documentation Generation](guide/api-documentation.md)**: Automated API reference generation from docstrings. +```@repl +using CTBase -## Note on Private Methods +# --- Descriptions --- +descs = CTBase.Descriptions.add((), (:euler, :explicit)) +descs = CTBase.Descriptions.add(descs, (:euler, :implicit)) +CTBase.Descriptions.complete(:euler, :explicit; descriptions=descs) -In some examples in the documentation, private methods are shown without the module -prefix. This is done for the sake of clarity and readability. - -```julia-repl -julia> using CTBase -julia> x = 1 -julia> private_fun(x) # throws an error -``` - -This should instead be written as: - -```julia-repl -julia> using CTBase -julia> x = 1 -julia> CTBase.private_fun(x) -``` - -If the method is re-exported by another package, - -```julia -module OptimalControl - import CTBase: private_fun - export private_fun +# --- Exceptions --- +try + throw(CTBase.Exceptions.IncorrectArgument("n must be positive"; got="-1")) +catch e + println(e) end ``` -then there is no need to prefix it with the original module name: +## User Guides -```julia-repl -julia> using OptimalControl -julia> x = 1 -julia> private_fun(x) -``` +- **[Getting Started](getting-started.md)** β€” installation, mental model, 5-minute walkthrough. +- **[Descriptions](guide/descriptions.md)** β€” catalogue API, pattern matching, error handling. +- **[Exceptions](guide/exceptions.md)** β€” exception hierarchy, choosing the right type, best practices. +- **[Test Runner](guide/test-runner.md)** β€” modular test infrastructure with `CTBase.run_tests`. +- **[Coverage](guide/coverage.md)** β€” post-processing coverage artifacts with `CTBase.postprocess_coverage`. +- **[API Documentation](guide/api-documentation.md)** β€” auto-generating per-module API pages. + +To browse the complete API, see the **API Reference** section in the left sidebar. diff --git a/ext/CoveragePostprocessing.jl b/ext/CoveragePostprocessing.jl index 3e2a2cca..10f6f860 100644 --- a/ext/CoveragePostprocessing.jl +++ b/ext/CoveragePostprocessing.jl @@ -10,11 +10,12 @@ Most functions in this module have filesystem side effects. module CoveragePostprocessing using CTBase: CTBase -using Coverage +using Coverage: Coverage +import DocStringExtensions: TYPEDSIGNATURES # Main entry point for coverage post-processing """ - CTBase.postprocess_coverage(::CTBase.Extensions.CoveragePostprocessingTag; generate_report::Bool=true, root_dir::String=pwd()) +$(TYPEDSIGNATURES) Post-process coverage artifacts produced by `Pkg.test(; coverage=true)`. @@ -42,7 +43,7 @@ This implementation: This function **creates/removes/moves files and directories** under `root_dir`. -# Usage sketch (non-executed) +# Example ```julia using CTBase @@ -88,31 +89,28 @@ function CTBase.postprocess_coverage( n_cov = _count_cov_files(source_dirs) if n_cov == 0 - error(""" - Coverage requested but no .cov files were produced. - - Expected locations: - - src/*.cov - - test/*.cov - - ext/*.cov - - Did you run: - Pkg.test(...; coverage=true) ? - """) + throw(CTBase.Exceptions.PreconditionError( + "Coverage requested but no .cov files were produced", + reason="no .cov files found in src/, test/, or ext/", + suggestion="run Pkg.test(...; coverage=true) to generate coverage data", + )) end _clean_stale_cov_files!(source_dirs) n_cov = _count_cov_files(source_dirs) - n_cov == 0 && - error("Coverage requested but no usable .cov files were found after cleanup.") + n_cov == 0 && throw(CTBase.Exceptions.PreconditionError( + "Coverage requested but no usable .cov files were found after cleanup", + )) generate_report && _generate_coverage_reports!( source_dirs, coverage_dir, root_dir, worst_n_files, max_uncovered_lines ) moved = _collect_and_move_cov_files!(source_dirs, cov_storage_dir) - isempty(moved) && error("Coverage requested but no .cov files were found to move.") + isempty(moved) && throw(CTBase.Exceptions.PreconditionError( + "Coverage requested but no .cov files were found to move", + )) println("βœ“ Moved $(length(moved)) .cov files to $cov_storage_dir") println("βœ“ Coverage post-processing completed successfully") @@ -221,14 +219,8 @@ function _collect_and_move_cov_files!(source_dirs, dest_dir) # But let's stick to the plan: just recursive collection. src = joinpath(root, f) - dest = joinpath(dest_dir, f) # Wait, this might fail if f is just filename. - - # The issue with joinpath(dest_dir, f) if f is just a filename is that it puts everything in root of dest_dir. - # If f comes from walkdir's `files`, it is just the filename. - # So `src` is correct. `dest` puts it in `coverage/cov/filename.cov`. - # This matches previous behavior, just expanded to subdirs. - - dest = joinpath(dest_dir, f) + flat = replace(relpath(src, dir), r"[/\\]" => "_") + dest = joinpath(dest_dir, flat) mv(src, dest; force=true) push!(moved, dest) end @@ -295,7 +287,9 @@ function _generate_coverage_reports!( append!(cov_all, Coverage.process_folder(dir)) end - isempty(cov_all) && error("No coverage data found in source directories") + isempty(cov_all) && throw(CTBase.Exceptions.PreconditionError( + "No coverage data found in source directories", + )) lcov_file = joinpath(coverage_dir, "lcov.info") Coverage.LCOV.writefile(lcov_file, cov_all) @@ -348,12 +342,8 @@ function _generate_coverage_reports!( # Helper to make paths relative to root_dir function relative_path(path, root) - if startswith(path, root) - relpath = path[(length(root) + 1):end] - # Remove leading slash if present - return startswith(relpath, "/") ? relpath[2:end] : relpath - end - return path + startswith(path, root) || return path + return relpath(path, root) end function write_report(io) diff --git a/ext/DocumenterReference.jl b/ext/DocumenterReference.jl index 25488d31..575999cc 100644 --- a/ext/DocumenterReference.jl +++ b/ext/DocumenterReference.jl @@ -233,9 +233,10 @@ function CTBase.automatic_reference_documentation( ) # Validate arguments if !public && !private - error( - "automatic_reference_documentation: both `public` and `private` cannot be false.", - ) + throw(CTBase.Exceptions.IncorrectArgument( + "both `public` and `private` cannot be false", + context="automatic_reference_documentation", + )) end # Parse primary_modules into a Dict{Module, Vector{String}} @@ -387,9 +388,11 @@ function _parse_primary_modules(primary_modules::Vector) files = last(m) result[mod] = _normalize_paths(files isa Vector ? files : [files]) else - error( + throw(CTBase.Exceptions.IncorrectArgument( "Invalid element in primary_modules: expected Module or Module => files pair", - ) + got=string(typeof(m)), + context="_parse_primary_modules", + )) end end return result diff --git a/ext/TestRunner.jl b/ext/TestRunner.jl index 5c689163..146f652c 100644 --- a/ext/TestRunner.jl +++ b/ext/TestRunner.jl @@ -10,7 +10,7 @@ running testsets). module TestRunner using CTBase: CTBase -using DocStringExtensions +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES using Test: Test, @testset """ @@ -30,7 +30,7 @@ and internal test identifiers. - String specs are treated as relative paths from `test_dir` - Glob patterns are supported for String specs -See also: `CTBase.run_tests`, `TestRunner._select_tests` +See also: [`CTBase.run_tests`](@ref) """ const TestSpec = Union{Symbol,String} @@ -132,7 +132,7 @@ julia> CTBase.run_tests(; ) ``` -See also: `TestRunner.TestRunInfo`, `TestRunner._parse_test_args`, `TestRunner._select_tests` +See also: [`CTBase.run_tests`](@ref), [`TestRunner.TestRunInfo`](@ref) """ function CTBase.run_tests( ::CTBase.Extensions.TestRunnerTag; @@ -154,12 +154,11 @@ function CTBase.run_tests( # Guard: a subdirectory named "test" inside test_dir would conflict with # the automatic `test/` prefix stripping in _parse_test_args. if isdir(joinpath(test_dir, "test")) - error( - "A subdirectory \"test\" exists inside the test directory " * - "\"$(test_dir)\". This is not supported because selection " * - "arguments starting with \"test/\" are automatically stripped " * - "(e.g. \"test/suite\" β†’ \"suite\"). Please rename the subdirectory.", - ) + throw(CTBase.Exceptions.PreconditionError( + "A subdirectory \"test\" exists inside the test directory \"$(test_dir)\"", + reason="selection arguments starting with \"test/\" are automatically stripped (e.g. \"test/suite\" β†’ \"suite\")", + suggestion="rename the subdirectory to avoid the conflict", + )) end # Parse command-line arguments @@ -194,7 +193,6 @@ function CTBase.run_tests( @testset "$(spec)" begin _run_single_test( spec; - available_tests=available_tests_vec, filename_builder, funcname_builder, eval_mode, @@ -329,7 +327,7 @@ julia> TestRunner._normalize_selections( ``` """ function _normalize_selections(selections::Vector{String}, candidates::Vector{<:TestSpec}) - candidate_strs = [c isa Symbol ? String(c) : String(c) for c in candidates] + candidate_strs = [String(c) for c in candidates] normalized = String[] for sel in selections # Strip trailing slash(es) @@ -394,7 +392,7 @@ function _glob_to_regex(pattern::AbstractString) end """ - _ensure_jl(filename::AbstractString) -> String +$(TYPEDSIGNATURES) Ensure that a filename ends with `.jl` extension. @@ -421,7 +419,7 @@ function _ensure_jl(filename::AbstractString) end """ - _builder_to_string(x) -> String +$(TYPEDSIGNATURES) Convert a Symbol or String to String. @@ -444,11 +442,11 @@ julia> TestRunner._builder_to_string("utils") ``` """ function _builder_to_string(x) - x isa Symbol ? String(x) : String(x) + String(x) end """ - _normalize_available_tests(available_tests) -> Vector{TestSpec} +$(TYPEDSIGNATURES) Normalize and validate the `available_tests` argument. @@ -480,7 +478,10 @@ function _normalize_available_tests(available_tests) available_tests === nothing && return TestSpec[] if !(available_tests isa AbstractVector || available_tests isa Tuple) - throw(ArgumentError("available_tests must be a Vector or Tuple of Symbol/String")) + throw(CTBase.Exceptions.IncorrectArgument( + "available_tests must be a Vector or Tuple of Symbol/String", + got=string(typeof(available_tests)), + )) end out = TestSpec[] @@ -488,14 +489,17 @@ function _normalize_available_tests(available_tests) if entry isa Symbol || entry isa String push!(out, entry) else - throw(ArgumentError("available_tests entries must be Symbol or String")) + throw(CTBase.Exceptions.IncorrectArgument( + "available_tests entries must be Symbol or String", + got=string(typeof(entry)), + )) end end return out end """ - _collect_test_files_recursive(test_dir::AbstractString) -> Vector{String} +$(TYPEDSIGNATURES) Recursively collect all `.jl` files in `test_dir` (excluding `runtests.jl`). @@ -535,7 +539,7 @@ function _collect_test_files_recursive(test_dir::AbstractString) end """ - _find_symbol_test_file_rel(name::Symbol, filename_builder::Function; test_dir::AbstractString) -> Union{String,Nothing} +$(TYPEDSIGNATURES) Find the relative path to a test file for a given symbol name. @@ -558,7 +562,7 @@ different subdirectories), prefers the shallowest path. - Prefers shallower paths when multiple matches exist - Returns the exact relative path if found -See also: `TestRunner._collect_test_files_recursive`, `TestRunner._ensure_jl` +See also: [`TestRunner._collect_test_files_recursive`](@ref), [`TestRunner._ensure_jl`](@ref) """ function _find_symbol_test_file_rel( name::Symbol, filename_builder::Function; test_dir::AbstractString @@ -609,47 +613,11 @@ function _select_tests( filename_builder::Function; test_dir::String=joinpath(pwd(), "test"), # Default assumption ) - # 1. Identify all potential test files - # We look for .jl files in test_dir, excluding runtests.jl - all_files = filter(f -> endswith(f, ".jl") && f != "runtests.jl", readdir(test_dir)) - - # Map filenames back to "test names" is tricky without reverse builder. - # Strategy: - # We assume `available_tests` defines the canonical list of "names". - # If `available_tests` is empty, we derive names from files? - # -> User said: "list all .jl files... but only keep available if provided" - candidates = TestSpec[] if isempty(available_tests) - # If no available_tests provided, every .jl file is a candidate! - # This effectively AUTO-DISCOVERS tests. - # We need to guess the "name" from the filename. - # This is hard because filename_builder is name -> filename. - # utils -> test_utils.jl - # test_utils.jl -> utils ?? - # For now, let's keep the user's current behavior: - # If available_tests is empty, the logic relies on what the user passes. - # BUT the new logic says "if args empty -> run all". - # So we MUST perform auto-discovery if we want "run all" to work without explicit available_tests. - - # Heuristic: if file starts with "test_", strip it? - # Or just use the basename as the symbol? - # Let's assume the "name" is the filename without extension for auto-discovery? - # Or better: don't guess names yet. Just work with filenames? - # But `run_tests` iterates over `names`. - - # Let's look at existing files: test_utils.jl -> name=:utils (via filename_builder) - # We cannot invert `filename_builder` (F -> S). - # So we are stuck if we want to infer names from files. - - # COMPROMISE: If available_tests is empty, we cannot guarantee auto-discovery compatibility with arbitrary filename_builders. - # We will assume a default mapping for auto-discovery: name = file_basename_without_extension. - for f in all_files - name_str = replace(f, ".jl" => "") - # If name starts with "test_", removing it is common convention, but maybe risky? - # Let's keep the full basename as the name if we are auto-discovering. - push!(candidates, Symbol(name_str)) + for f in _collect_test_files_recursive(test_dir) + push!(candidates, f) end else # If available_tests IS provided, we only consider these. @@ -764,7 +732,6 @@ This helper: # Arguments - `spec::TestSpec`: Test specification to run -- `available_tests::AbstractVector{<:TestSpec}`: Available tests for validation - `filename_builder::Function`: Function to map test names to filenames - `funcname_builder::Function`: Function to map test names to function names - `eval_mode::Bool`: Whether to evaluate the function after include @@ -780,7 +747,6 @@ This helper: """ function _run_single_test( spec::TestSpec; - available_tests::AbstractVector{<:TestSpec}, filename_builder::Function, funcname_builder::Function, eval_mode::Bool, @@ -792,7 +758,7 @@ function _run_single_test( ) # --- Resolve filename and func_symbol --- filename, func_symbol = _resolve_test( - spec; available_tests, filename_builder, funcname_builder, eval_mode, test_dir + spec; filename_builder, funcname_builder, eval_mode, test_dir ) # --- Include the file --- @@ -800,10 +766,11 @@ function _run_single_test( # --- Check function exists after include --- if eval_mode && func_symbol !== nothing && !isdefined(Main, func_symbol) - error(""" - Function "$(func_symbol)" not found after including "$(filename)". - Make sure the file defines a function with this name. - """) + throw(CTBase.Exceptions.PreconditionError( + "Function \"$(func_symbol)\" not found after including \"$(filename)\"", + reason="the file does not define a function with this name", + suggestion="make sure the file defines a top-level function named $(func_symbol)", + )) end # --- on_test_start callback --- @@ -879,7 +846,6 @@ Raises errors if the file is not found or if `eval_mode=true` but no function ca # Arguments - `spec::TestSpec`: Test specification to resolve -- `available_tests::AbstractVector{<:TestSpec}`: Available tests for validation - `filename_builder::Function`: Function to map test names to filenames - `funcname_builder::Function`: Function to map test names to function names - `eval_mode::Bool`: Whether to resolve a function name @@ -900,7 +866,6 @@ Raises errors if the file is not found or if `eval_mode=true` but no function ca """ function _resolve_test( spec::TestSpec; - available_tests::AbstractVector{<:TestSpec}, filename_builder::Function, funcname_builder::Function, eval_mode::Bool, @@ -910,10 +875,10 @@ function _resolve_test( rel = _ensure_jl(spec) filename = joinpath(test_dir, rel) if !isfile(filename) - error(""" - Test file "$(filename)" not found. - Current directory: $(pwd()) - """) + throw(CTBase.Exceptions.IncorrectArgument( + "Test file \"$(filename)\" not found", + context="current directory: $(pwd())", + )) end func_symbol = if eval_mode @@ -929,30 +894,29 @@ function _resolve_test( # Build filename rel = _find_symbol_test_file_rel(name, filename_builder; test_dir=test_dir) - rel === nothing && error(""" - Test file not found for test "$(name)". - Current directory: $(pwd()) - """) + rel === nothing && throw(CTBase.Exceptions.IncorrectArgument( + "Test file not found for test \"$(name)\"", + context="current directory: $(pwd())", + )) filename = joinpath(test_dir, rel) # Check file exists - !isfile(filename) && error(""" - Test file "$(filename)" not found for test "$(name)". - Current directory: $(pwd()) - """) + !isfile(filename) && throw(CTBase.Exceptions.IncorrectArgument( + "Test file \"$(filename)\" not found for test \"$(name)\"", + context="current directory: $(pwd())", + )) # Determine function name func_symbol = funcname_builder(name) # Check consistency: eval_mode=true but funcname_builder returns nothing if eval_mode && func_symbol === nothing - error( - """ - Inconsistency: eval_mode=true but funcname_builder returned nothing for test "$(name)". - Either set eval_mode=false, or make funcname_builder return a Symbol. - """, - ) + throw(CTBase.Exceptions.PreconditionError( + "eval_mode=true but funcname_builder returned nothing for test \"$(name)\"", + reason="funcname_builder must return a Symbol when eval_mode=true", + suggestion="set eval_mode=false, or make funcname_builder return a Symbol", + )) end if !eval_mode || func_symbol === nothing @@ -1086,7 +1050,7 @@ julia> TestRunner._progress_bar(5, 10) "[β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘]" julia> TestRunner._progress_bar(5, 10; width=20) -"[β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘]" +"[β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘]" julia> TestRunner._progress_bar(0, 10; width=5) "[β–‘β–‘β–‘β–‘β–‘]" diff --git a/src/CTBase.jl b/src/CTBase.jl index 88f03288..e1ddd1b7 100644 --- a/src/CTBase.jl +++ b/src/CTBase.jl @@ -6,8 +6,7 @@ packages such as OptimalControl.jl. """ module CTBase -using Base: Base -using DocStringExtensions +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES # ============================================================================ # # MODULAR ORGANIZATION diff --git a/src/Core/Core.jl b/src/Core/Core.jl index 6daf7110..827b782a 100644 --- a/src/Core/Core.jl +++ b/src/Core/Core.jl @@ -8,7 +8,7 @@ ecosystem, including type aliases and internal utilities. """ module Core -using DocStringExtensions +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES # -------------------------------------------------------------------------------------------------- # Type aliases and constants diff --git a/src/Descriptions/Descriptions.jl b/src/Descriptions/Descriptions.jl index 2f8a9d8a..2bca86a5 100644 --- a/src/Descriptions/Descriptions.jl +++ b/src/Descriptions/Descriptions.jl @@ -29,7 +29,7 @@ The Descriptions module is organized into thematic submodules: """ module Descriptions -using DocStringExtensions +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES using ..Exceptions # Include submodules diff --git a/src/Descriptions/catalog.jl b/src/Descriptions/catalog.jl index 1e6c5c96..1393b297 100644 --- a/src/Descriptions/catalog.jl +++ b/src/Descriptions/catalog.jl @@ -4,12 +4,16 @@ $(TYPEDSIGNATURES) Initialize a new description catalog with a single description `y`. # Arguments -- `y::Description`: The initial description to add +- `y::Description`: The initial description to add. # Returns -- `Tuple{Vararg{Description}}`: A tuple containing only the description `y` +- `Tuple{Vararg{Description}}`: A one-element tuple containing `y`. + +# Throws +- (none) # Example + ```julia-repl julia> using CTBase @@ -22,7 +26,7 @@ julia> descriptions[1] (:a,) ``` -See also: `Description` +See also: [`CTBase.Descriptions.Description`](@ref), [`CTBase.Descriptions.complete`](@ref), [`CTBase.Descriptions.remove`](@ref) """ add(::Tuple{}, y::Description)::Tuple{Vararg{Description}} = (y,) @@ -59,7 +63,7 @@ ERROR: IncorrectArgument: the description (:b,) is already in ((:a,), (:b,)) Context: description catalog management ``` -See also: `complete`, `remove` +See also: [`CTBase.Descriptions.complete`](@ref), [`CTBase.Descriptions.remove`](@ref) """ function add(x::Tuple{Vararg{Description}}, y::Description)::Tuple{Vararg{Description}} if y ∈ x diff --git a/src/Descriptions/complete.jl b/src/Descriptions/complete.jl index 13967ed3..ec0719e6 100644 --- a/src/Descriptions/complete.jl +++ b/src/Descriptions/complete.jl @@ -45,11 +45,7 @@ ERROR: AmbiguousDescription: the description (:f,) is ambiguous / incorrect Context: description completion ``` -# Enhanced Error Features -When no matching description is found, the function provides suggestions based on -similarity and lists existing candidates. - -See also: `compute_similarity`, `find_similar_descriptions`, `format_description_candidates` +See also: [`CTBase.Descriptions.compute_similarity`](@ref), [`CTBase.Descriptions.find_similar_descriptions`](@ref), [`CTBase.Descriptions.format_description_candidates`](@ref), [`CTBase.Exceptions.AmbiguousDescription`](@ref) """ function complete(list::Symbol...; descriptions::Tuple{Vararg{Description}})::Description n = length(descriptions) @@ -130,8 +126,10 @@ This method is equivalent to `complete(list...; descriptions=descriptions)`. # Throws -- ``AmbiguousDescription``: If `descriptions` is empty, or if `list` is not contained +- [`CTBase.Exceptions.AmbiguousDescription`](@ref): If `descriptions` is empty, or if `list` is not contained in any candidate description. + +See also: [`CTBase.Descriptions.complete`](@ref), [`CTBase.Exceptions.AmbiguousDescription`](@ref) """ function complete( list::Tuple{DescVarArg}; descriptions::Tuple{Vararg{Description}} diff --git a/src/Descriptions/display.jl b/src/Descriptions/display.jl index 1112d61b..4c527366 100644 --- a/src/Descriptions/display.jl +++ b/src/Descriptions/display.jl @@ -1,7 +1,14 @@ """ $(TYPEDSIGNATURES) -Print a tuple of descriptions, one per line. +Print a tuple of descriptions, one per line, to `io`. + +# Arguments +- `io::IO`: The output stream. +- `descriptions::Tuple{Vararg{Description}}`: The tuple of descriptions to display. + +# Returns +- `Nothing` # Example @@ -12,6 +19,8 @@ julia> display(((:a, :b), (:b, :c))) (:a, :b) (:b, :c) ``` + +See also: [`CTBase.Descriptions.Description`](@ref) """ function Base.show(io::IO, ::MIME"text/plain", descriptions::Tuple{Vararg{Description}}) N = length(descriptions) # use length instead of size for 1D tuple diff --git a/src/Descriptions/remove.jl b/src/Descriptions/remove.jl index 4d057d27..3f4f0e51 100644 --- a/src/Descriptions/remove.jl +++ b/src/Descriptions/remove.jl @@ -3,6 +3,13 @@ $(TYPEDSIGNATURES) Remove symbols from a description tuple. +# Arguments +- `x::Description`: The source description. +- `y::Description`: The symbols to remove from `x`. + +# Returns +- `Tuple{Vararg{Symbol}}`: The set difference of `x` and `y` as a tuple (symbols in `x` not in `y`). + # Example ```julia-repl @@ -11,6 +18,8 @@ julia> using CTBase julia> CTBase.remove((:a, :b, :c), (:a,)) (:b, :c) ``` + +See also: [`CTBase.Descriptions.add`](@ref), [`CTBase.Descriptions.complete`](@ref) """ function remove(x::Description, y::Description)::Tuple{Vararg{Symbol}} return tuple(setdiff(x, y)...) diff --git a/src/Descriptions/types.jl b/src/Descriptions/types.jl index 894e2015..57bd2b38 100644 --- a/src/Descriptions/types.jl +++ b/src/Descriptions/types.jl @@ -1,7 +1,5 @@ # Type definitions for Descriptions module -using DocStringExtensions - """ $(TYPEDEF) @@ -15,14 +13,14 @@ julia> CTBase.DescVarArg Vararg{Symbol} ``` -See also: `Description` +See also: [`CTBase.Descriptions.Description`](@ref) """ const DescVarArg = Vararg{Symbol} """ $(TYPEDEF) -A description is a tuple of symbols, used to declarative encode algorithms or configurations. +A description is a tuple of symbols, used to declaratively encode algorithms or configurations. # Example `Base.show` is overloaded for descriptions, so tuples of descriptions are @@ -36,6 +34,6 @@ julia> display(((:a, :b), (:b, :c))) (:b, :c) ``` -See also: `DescVarArg` +See also: [`CTBase.Descriptions.DescVarArg`](@ref) """ const Description = Tuple{DescVarArg} diff --git a/src/Exceptions/Exceptions.jl b/src/Exceptions/Exceptions.jl index 22ba4367..b14de2d8 100644 --- a/src/Exceptions/Exceptions.jl +++ b/src/Exceptions/Exceptions.jl @@ -36,7 +36,7 @@ The Exceptions module is organized into thematic files: """ module Exceptions -using CTBase +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES # Type definitions include("types.jl") diff --git a/src/Exceptions/types.jl b/src/Exceptions/types.jl index 05fb49db..bc7c72a6 100644 --- a/src/Exceptions/types.jl +++ b/src/Exceptions/types.jl @@ -2,12 +2,12 @@ # Based on CTBase.jl but with enriched error handling """ - CTException +$(TYPEDEF) Abstract supertype for all CTBase exceptions. -Compatible with CTBase.CTException for future migration. -All exceptions inherit from this type to allow uniform error handling. +All exceptions in the CTBase ecosystem inherit from this type, enabling +uniform error handling via a single `catch` clause. # Example @@ -16,51 +16,33 @@ julia> using CTBase julia> try throw(CTBase.Exceptions.IncorrectArgument("invalid input")) - catch e::CTBase.Exceptions.CTException - println("Caught a domain-specific exception: ", e) + catch e + e isa CTBase.Exceptions.CTException || rethrow() + println("Caught: ", e) end -Caught a domain-specific exception: IncorrectArgument: invalid input +Caught: IncorrectArgument: invalid input ``` -# Usage Pattern - -Use this as the common ancestor for all domain-specific errors to allow -catching all exceptions of this family via `catch e::CTException`. - -```julia -try - # code that may throw CTBase exceptions - risky_operation() -catch e::CTBase.Exceptions.CTException - # handle all CTBase domain errors uniformly - handle_error(e) -end -``` +See also: [`CTBase.Exceptions.IncorrectArgument`](@ref), [`CTBase.Exceptions.NotImplemented`](@ref) """ abstract type CTException <: Exception end """ - IncorrectArgument <: CTException - -Exception thrown when an individual argument is invalid or violates a precondition. +$(TYPEDEF) -This exception is raised when **one input value** is outside the allowed domain, such as: -- Wrong range or bounds (e.g., negative when positive is required) -- Duplicate values when uniqueness is required -- Empty collections when non-empty is required -- Type mismatches or invalid combinations +Exception thrown when an individual argument is invalid or violates a constraint. -Use this exception to signal that the problem is with the **input data itself**, not with -the state of the system or the calling context. +Use when the problem is with the **input data itself** (wrong range, duplicate, +empty collection, type mismatch) rather than the calling context or system state. # Fields -- `msg::String`: Main error message describing the problem -- `got::Union{String, Nothing}`: What value was received (optional) -- `expected::Union{String, Nothing}`: What value was expected (optional) -- `suggestion::Union{String, Nothing}`: How to fix the problem (optional) -- `context::Union{String, Nothing}`: Where the error occurred (optional) +- `msg::String`: Main error message describing the problem. +- `got::Union{String, Nothing}`: The invalid value received (optional). +- `expected::Union{String, Nothing}`: What was expected (optional). +- `suggestion::Union{String, Nothing}`: How to fix the problem (optional). +- `context::Union{String, Nothing}`: Where the error occurred (optional). -# Examples +# Example ```julia-repl julia> using CTBase @@ -69,22 +51,7 @@ julia> throw(CTBase.Exceptions.IncorrectArgument("the argument must be a non-emp ERROR: IncorrectArgument: the argument must be a non-empty tuple ``` -Adding a duplicate description to a catalogue: - -```julia-repl -julia> algorithms = CTBase.add((), (:a, :b)) -julia> CTBase.add(algorithms, (:a, :b)) -ERROR: IncorrectArgument: the description (:a, :b) is already in ((:a, :b),) -``` - -Invalid indices for Unicode helpers: - -```julia-repl -julia> CTBase.ctindice(-1) -ERROR: IncorrectArgument: the subscript must be between 0 and 9 -``` - -Enhanced version with detailed context: +With optional fields: ```julia throw(CTBase.Exceptions.IncorrectArgument( @@ -92,12 +59,11 @@ throw(CTBase.Exceptions.IncorrectArgument( got="vector of length 3", expected="vector of length 2", suggestion="Provide a vector matching the state dimension", - context="initial_guess for state" + context="initial_guess for state", )) ``` -# See Also -- `AmbiguousDescription`: For high-level description matching errors +See also: [`CTBase.Exceptions.AmbiguousDescription`](@ref), [`CTBase.Exceptions.PreconditionError`](@ref) """ struct IncorrectArgument <: CTException msg::String @@ -119,29 +85,23 @@ struct IncorrectArgument <: CTException end """ - PreconditionError <: CTException +$(TYPEDEF) -Exception thrown when a function call violates a **precondition** or is not allowed in the -**current state** of the object or system. +Exception thrown when a function call violates a precondition or is not allowed in the +current state of the system. -This exception signals that the arguments may be valid, but the call is forbidden because -of **when** or **how** it is made. This is distinct from `IncorrectArgument`, which -indicates a problem with the input values themselves. - -Common use cases: -- A method that is meant to be called only once -- State already closed or finalized -- Required setup not completed (e.g., state must be set before dynamics) -- Illegal order of operations -- Wrong phase of a computation +Use when the **arguments are valid** but the call is forbidden because of when or how it +is made (e.g., calling a method twice, missing a required prior setup step). +Distinct from [`CTBase.Exceptions.IncorrectArgument`](@ref), which signals a problem +with the input values themselves. # Fields -- `msg::String`: Main error message -- `reason::Union{String, Nothing}`: Why the precondition failed (optional) -- `suggestion::Union{String, Nothing}`: How to fix the problem (optional) -- `context::Union{String, Nothing}`: Where the error occurred (optional) +- `msg::String`: Main error message. +- `reason::Union{String, Nothing}`: Why the precondition failed (optional). +- `suggestion::Union{String, Nothing}`: How to fix the problem (optional). +- `context::Union{String, Nothing}`: Where the error occurred (optional). -# Examples +# Example ```julia-repl julia> using CTBase @@ -150,36 +110,18 @@ julia> throw(CTBase.Exceptions.PreconditionError("state must be set before dynam ERROR: PreconditionError: state must be set before dynamics ``` -Typical pattern for checking preconditions (as used in CTModels.jl): - -```julia -function dynamics!(ocp::PreModel, f::Function) - if !__is_state_set(ocp) - throw(CTBase.Exceptions.PreconditionError( - "State must be set before defining dynamics", - reason="state has not been defined yet", - suggestion="Call state!(ocp, dimension) before dynamics!", - context="dynamics! function - state validation" - )) - end - # ... set dynamics ... -end -``` - -Enhanced version with detailed context: +With optional fields: ```julia throw(CTBase.Exceptions.PreconditionError( "Cannot call state! twice", reason="state has already been defined for this OCP", - suggestion="Create a new OCP instance or use a different component name", - context="state definition" + suggestion="Create a new OCP instance", + context="state definition", )) ``` -# See Also -- `IncorrectArgument`: For input validation errors -- `NotImplemented`: For unimplemented interface methods +See also: [`CTBase.Exceptions.IncorrectArgument`](@ref), [`CTBase.Exceptions.NotImplemented`](@ref) """ struct PreconditionError <: CTException msg::String @@ -198,22 +140,19 @@ struct PreconditionError <: CTException end """ - NotImplemented <: CTException +$(TYPEDEF) Exception thrown to mark interface points that must be implemented by concrete subtypes. -This exception is used to define abstract interfaces where a default method on an abstract -type throws `NotImplemented`, and each concrete implementation must override it. This makes -it easy to detect missing implementations during testing and development. - -Use `NotImplemented` when defining **interfaces** and you want an explicit, typed error -rather than a generic `error("TODO")`. +Use when a default method on an abstract type should explicitly signal that a concrete +subtype has not provided the required implementation. Prefer this over a generic +`error("not implemented")` to give users a typed, catchable error. # Fields -- `msg::String`: Description of what is not implemented -- `required_method::Union{String, Nothing}`: Method signature or requirement (optional) -- `suggestion::Union{String, Nothing}`: How to fix the problem (optional) -- `context::Union{String, Nothing}`: Where the error occurred (optional) +- `msg::String`: Description of what is not implemented. +- `required_method::Union{String, Nothing}`: The missing method signature (optional). +- `suggestion::Union{String, Nothing}`: How to fix the problem (optional). +- `context::Union{String, Nothing}`: Where the error occurred (optional). # Example @@ -224,7 +163,7 @@ julia> throw(CTBase.Exceptions.NotImplemented("feature X is not implemented")) ERROR: NotImplemented: feature X is not implemented ``` -A typical pattern for defining an interface: +Typical interface stub pattern: ```julia abstract type MyAbstractAlgorithm end @@ -233,27 +172,13 @@ function run!(algo::MyAbstractAlgorithm, state) throw(CTBase.Exceptions.NotImplemented( "run! is not implemented for \$(typeof(algo))", required_method="run!(::MyAbstractAlgorithm, state)", + suggestion="Implement run! for your concrete algorithm type", context="algorithm execution", - suggestion="Implement run! for your concrete algorithm type" )) end ``` -Concrete algorithms then provide their own `run!` method instead of raising this exception. - -Enhanced version with full context: - -```julia -throw(CTBase.Exceptions.NotImplemented( - "Method solve! not implemented", - required_method="solve!(::MyStrategy, ...)", - context="solve call", - suggestion="Import the relevant package (e.g. CTDirect) or implement solve!(::MyStrategy, ...)" -)) -``` - -# See Also -- `IncorrectArgument`: For input validation errors +See also: [`CTBase.Exceptions.IncorrectArgument`](@ref), [`CTBase.Exceptions.PreconditionError`](@ref) """ struct NotImplemented <: CTException msg::String @@ -272,21 +197,18 @@ struct NotImplemented <: CTException end """ - ParsingError <: CTException +$(TYPEDEF) Exception thrown during parsing when a syntax error or invalid structure is detected. -This exception is intended for errors detected during parsing of input structures or -domain-specific languages (DSLs). Use this when processing user input that follows a -specific grammar or format, and the input violates the expected syntax. - -This exception is raised when **the structure or syntax** of the input is invalid, -rather than the semantic meaning. For semantic errors, use `IncorrectArgument` instead. +Use when the **structure or syntax** of the input is invalid (e.g., DSL grammar +violation). For semantic errors on a valid-syntax input, prefer +[`CTBase.Exceptions.IncorrectArgument`](@ref) instead. # Fields -- `msg::String`: Description of the parsing error -- `location::Union{String, Nothing}`: Where in the input the error occurred (optional) -- `suggestion::Union{String, Nothing}`: How to fix the problem (optional) +- `msg::String`: Description of the parsing error. +- `location::Union{String, Nothing}`: Where in the input the error occurred (optional). +- `suggestion::Union{String, Nothing}`: How to fix the problem (optional). # Example @@ -303,26 +225,11 @@ With optional fields: throw(CTBase.Exceptions.ParsingError( "Unexpected token 'end'", location="line 42, column 15", - suggestion="Check syntax balance or remove extra 'end'" + suggestion="Check syntax balance or remove extra 'end'", )) ``` -As used in CTParser.jl (message only): - -```julia -info = string("Line ", 42, ": x = 1\n", "Unexpected token 'end'") -throw(CTBase.Exceptions.ParsingError(info)) -``` - -Common use cases: -- Parsing mathematical expressions or formulas -- Reading configuration files or DSL syntax -- Processing structured input with specific grammar rules -- Validating syntax of domain-specific languages - -# See Also -- `IncorrectArgument`: For general input validation errors -- `AmbiguousDescription`: For description matching errors +See also: [`CTBase.Exceptions.IncorrectArgument`](@ref), [`CTBase.Exceptions.AmbiguousDescription`](@ref) """ struct ParsingError <: CTException msg::String @@ -339,24 +246,21 @@ struct ParsingError <: CTException end """ - AmbiguousDescription <: CTException - -Exception thrown when a description (a tuple of `Symbol`s) cannot be matched to any known -valid descriptions. +$(TYPEDEF) -This exception is raised by `CTBase.complete()` when the user provides an incomplete or -inconsistent description that doesn't match any of the available descriptions in the -catalogue. Use this exception when **the high-level choice of description itself** is wrong -or ambiguous and there is no sensible default. +Exception thrown when a description (a tuple of `Symbol`s) cannot be matched to any +known valid description in a catalogue. -Enhanced version with additional context for better error reporting. +Raised by [`CTBase.Descriptions.complete`](@ref) when the partial description provided +by the user is not a subset of any catalogue entry. # Fields -- `msg::String`: Main error message (auto-generated if not provided) -- `description::Tuple{Vararg{Symbol}}`: The ambiguous or incorrect description tuple -- `candidates::Union{Vector{String}, Nothing}`: Suggested valid descriptions (optional) -- `suggestion::Union{String, Nothing}`: How to fix the problem (optional) -- `context::Union{String, Nothing}`: Where the error occurred (optional) +- `msg::String`: Main error message. +- `description::Tuple{Vararg{Symbol}}`: The ambiguous or unrecognised description tuple. +- `candidates::Union{Vector{String}, Nothing}`: Suggested valid descriptions (optional). +- `suggestion::Union{String, Nothing}`: How to fix the problem (optional). +- `context::Union{String, Nothing}`: Where the error occurred (optional). +- `diagnostic::Union{String, Nothing}`: Diagnostic tag, e.g. `"unknown symbols"` (optional). # Example @@ -368,30 +272,18 @@ julia> CTBase.complete(:f; descriptions=D) ERROR: AmbiguousDescription: the description (:f,) is ambiguous / incorrect ``` -In this example, the symbol `:f` does not appear in any of the known descriptions, -so `complete()` cannot determine which description to return. - -Enhanced version with full context: +With optional fields: ```julia throw(CTBase.Exceptions.AmbiguousDescription( (:f,), candidates=["(:descent, :bfgs, :bisection)", "(:descent, :gradient, :fixedstep)"], suggestion="Use a complete description like (:descent, :bfgs, :bisection)", - context="algorithm selection" + context="algorithm selection", )) ``` -# Common Use Cases -- Algorithm selection in optimization libraries -- Configuration matching in DSL systems -- Pattern matching in description-based APIs -- Validation of symbolic descriptions in mathematical modeling - -# See Also -- `complete`: Matches a partial description to a complete one -- `add`: Adds descriptions to a catalogue (throws `IncorrectArgument` for duplicates) -- `IncorrectArgument`: For input validation errors +See also: [`CTBase.Descriptions.complete`](@ref), [`CTBase.Descriptions.add`](@ref), [`CTBase.Exceptions.IncorrectArgument`](@ref) """ struct AmbiguousDescription <: CTException msg::String @@ -414,40 +306,25 @@ struct AmbiguousDescription <: CTException end """ - ExtensionError <: CTException +$(TYPEDEF) -Exception thrown when an extension or optional dependency is not loaded but -a function requiring it is called. +Exception thrown when an optional dependency (weak dependency) is required by a feature +but has not been loaded. -This exception is used to signal that a feature requires one or more optional dependencies -(weak dependencies) to be loaded. When a user tries to use a feature without loading the -required extensions, this exception provides a helpful message indicating exactly which -packages need to be loaded. - -It is also used internally by `ExtensionError()` when called without any weak dependencies, -in which case it throws `PreconditionError` instead. - -Enhanced version with additional context for better error reporting. +Calling the zero-argument constructor `ExtensionError()` is forbidden and throws a +[`CTBase.Exceptions.PreconditionError`](@ref) instead β€” at least one dependency symbol +must be supplied. # Fields -- `msg::String`: Main error message (auto-generated from message parameter) -- `weakdeps::Tuple{Vararg{Symbol}}`: The tuple of symbols representing the missing dependencies -- `feature::Union{String, Nothing}`: Which functionality requires these dependencies (optional) -- `context::Union{String, Nothing}`: Where the error occurred (optional) +- `msg::String`: Auto-generated error message listing the missing packages. +- `weakdeps::Tuple{Vararg{Symbol}}`: The missing dependency symbols. +- `feature::Union{String, Nothing}`: The functionality that requires these dependencies (optional). +- `context::Union{String, Nothing}`: Where the error occurred (optional). -# Constructor +# Throws +- [`CTBase.Exceptions.PreconditionError`](@ref): If called with no arguments. -```julia -ExtensionError(weakdeps::Symbol...; message::String="", feature::Union{String, Nothing}=nothing, context::Union{String, Nothing}=nothing) -``` - -Throws `PreconditionError` if no weak dependencies are provided: - -```julia -ExtensionError() # Throws PreconditionError -``` - -# Examples +# Example ```julia-repl julia> using CTBase @@ -456,32 +333,25 @@ julia> throw(CTBase.Exceptions.ExtensionError(:MyExtension)) ERROR: ExtensionError. Please make: julia> using MyExtension ``` -With multiple dependencies and a custom message: +With multiple dependencies: ```julia-repl julia> throw(CTBase.Exceptions.ExtensionError(:MyExtension, :AnotherDep; message="to use this feature")) ERROR: ExtensionError. Please make: julia> using MyExtension, AnotherDep to use this feature ``` -Enhanced version with full context: +With full context: ```julia throw(CTBase.Exceptions.ExtensionError( - (:Plots, :PlotlyJS), + :Plots; message="to plot optimization results", feature="plotting functionality", - context="solve! call" + context="solve! call", )) ``` -# Common Use Cases -- Optional plotting functionality in optimization packages -- Specialized solvers that require additional packages -- Export/import features with format-specific dependencies -- Advanced algorithms that depend on external libraries - -# See Also -- `PreconditionError`: Thrown when `ExtensionError()` is called without arguments +See also: [`CTBase.Exceptions.PreconditionError`](@ref) """ struct ExtensionError <: CTException msg::String @@ -506,26 +376,20 @@ struct ExtensionError <: CTException end """ - SolverFailure <: CTException - -Exception thrown when a solver (ODE integrator, optimization solver, linear solver, etc.) -fails to complete successfully or returns an error code. +$(TYPEDEF) -This exception is used across the Control Toolbox to report solver failures in a uniform way. -The `retcode` field is generic and can accommodate different solver types: -- SciML integrators: `:Unstable`, `:DtLessThanMin`, `:MaxIters`, `:Success` -- NLP solvers: `:Infeasible`, `:MaxIterations`, `:Stalled`, `:FirstOrder` -- Linear solvers: condition number indicators, singular matrix flags, etc. +Exception thrown when a numerical solver (ODE integrator, NLP solver, linear solver, etc.) +fails to complete successfully. -This exception signals that the numerical computation itself failed, not that the input -was invalid (use `IncorrectArgument` for that) or that a precondition was violated (use -`PreconditionError` for that). +Use this when the **numerical computation itself** fails, not when the input is invalid +([`CTBase.Exceptions.IncorrectArgument`](@ref)) or a precondition is violated +([`CTBase.Exceptions.PreconditionError`](@ref)). # Fields -- `msg::String`: Main error message describing the failure -- `retcode::Union{String, Nothing}`: Solver-specific return code (optional) -- `suggestion::Union{String, Nothing}`: How to fix the problem (optional) -- `context::Union{String, Nothing}`: Where the error occurred (optional) +- `msg::String`: Main error message describing the failure. +- `retcode::Union{String, Nothing}`: Solver-specific return code, e.g. `":Unstable"` (optional). +- `suggestion::Union{String, Nothing}`: How to fix the problem (optional). +- `context::Union{String, Nothing}`: Where the error occurred (optional). # Example @@ -536,26 +400,18 @@ julia> throw(CTBase.Exceptions.SolverFailure("ODE integration failed", retcode=" ERROR: SolverFailure: ODE integration failed ``` -Enhanced version with full context: +With full context: ```julia throw(CTBase.Exceptions.SolverFailure( "Optimization solver did not converge", retcode=":MaxIterations", suggestion="Increase max iterations or adjust tolerance settings", - context="IPOPT solver in CTDirect" + context="IPOPT solver in CTDirect", )) ``` -# Common Use Cases -- ODE integration failures in CTFlows (SciML integrators) -- Non-convergence of optimization solvers in CTDirect -- Ill-conditioned linear systems in numerical algorithms -- Any numerical solver that returns a status code indicating failure - -# See Also -- `IncorrectArgument`: For input validation errors -- `PreconditionError`: For precondition violations +See also: [`CTBase.Exceptions.IncorrectArgument`](@ref), [`CTBase.Exceptions.PreconditionError`](@ref) """ struct SolverFailure <: CTException msg::String diff --git a/src/Extensions/Extensions.jl b/src/Extensions/Extensions.jl index 980b26bb..cecb19a8 100644 --- a/src/Extensions/Extensions.jl +++ b/src/Extensions/Extensions.jl @@ -9,7 +9,7 @@ and extension functions. """ module Extensions -using DocStringExtensions +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES using ..Exceptions # -------------------------------------------------------------------------------------------------- @@ -149,6 +149,17 @@ Concrete tag type used to dispatch to the `CoveragePostprocessing` extension. Instances of this type are passed to `postprocess_coverage` to enable coverage post-processing when the extension is available. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.CoveragePostprocessingTag() isa CTBase.AbstractCoveragePostprocessingTag +true +``` + +See also: [`CTBase.Extensions.AbstractCoveragePostprocessingTag`](@ref) """ struct CoveragePostprocessingTag <: AbstractCoveragePostprocessingTag end @@ -231,6 +242,17 @@ Abstract supertype for tags used to select a particular implementation of Concrete subtypes identify a specific backend that provides the actual test runner logic. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.TestRunnerTag() isa CTBase.AbstractTestRunnerTag +true +``` + +See also: [`CTBase.Extensions.TestRunnerTag`](@ref) """ abstract type AbstractTestRunnerTag end @@ -241,6 +263,17 @@ Concrete tag type used to dispatch to the `TestRunner` extension. Instances of this type are passed to `run_tests` to enable the extension-based test runner when the extension is available. + +# Example + +```julia-repl +julia> using CTBase + +julia> tag = CTBase.TestRunnerTag() +CTBase.TestRunnerTag() +``` + +See also: [`CTBase.Extensions.AbstractTestRunnerTag`](@ref) """ struct TestRunnerTag <: AbstractTestRunnerTag end diff --git a/src/Unicode/Unicode.jl b/src/Unicode/Unicode.jl index dbd635b9..4ea50cad 100644 --- a/src/Unicode/Unicode.jl +++ b/src/Unicode/Unicode.jl @@ -8,7 +8,7 @@ and superscript characters, useful for mathematical notation and display. """ module Unicode -using DocStringExtensions +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES using ..Exceptions """ diff --git a/test/suite/exceptions/test_exceptions.jl b/test/suite/exceptions/test_exceptions.jl index 0f00b487..e866f7ba 100644 --- a/test/suite/exceptions/test_exceptions.jl +++ b/test/suite/exceptions/test_exceptions.jl @@ -1,12 +1,20 @@ +module TestExceptions + +using Test +import CTBase.Exceptions + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + function test_exceptions() # Test suite for CTException subtypes and their error printing # Test AmbiguousDescription @testset verbose = VERBOSE showtiming = SHOWTIMING "AmbiguousDescription" begin - e = CTBase.AmbiguousDescription((:e,)) + e = Exceptions.AmbiguousDescription((:e,)) # Check that throwing error(e) produces an ErrorException - @test_throws CTBase.AmbiguousDescription throw(e) + @test_throws Exceptions.AmbiguousDescription throw(e) # Check that showerror produces a string output output = sprint(showerror, e) @test typeof(output) == String @@ -15,7 +23,7 @@ function test_exceptions() @test occursin("(:e,)", output) # Test enriched version with candidates and suggestions - e_enriched = CTBase.AmbiguousDescription( + e_enriched = Exceptions.AmbiguousDescription( (:x,), candidates=["(:a, :b)", "(:c, :d)"], suggestion="Try one of the available descriptions", @@ -33,15 +41,15 @@ function test_exceptions() # Test IncorrectArgument @testset verbose = VERBOSE showtiming = SHOWTIMING "IncorrectArgument" begin - e = CTBase.IncorrectArgument("invalid argument") - @test_throws CTBase.IncorrectArgument throw(e) + e = Exceptions.IncorrectArgument("invalid argument") + @test_throws Exceptions.IncorrectArgument throw(e) output = sprint(showerror, e) @test typeof(output) == String @test occursin("IncorrectArgument", output) @test occursin("invalid argument", output) # Test enriched version with all fields - e_enriched = CTBase.IncorrectArgument( + e_enriched = Exceptions.IncorrectArgument( "dimension mismatch", got="vector of length 3", expected="vector of length 2", @@ -61,15 +69,15 @@ function test_exceptions() # Test NotImplemented @testset verbose = VERBOSE showtiming = SHOWTIMING "NotImplemented" begin - e = CTBase.NotImplemented("feature not ready") - @test_throws CTBase.NotImplemented throw(e) + e = Exceptions.NotImplemented("feature not ready") + @test_throws Exceptions.NotImplemented throw(e) output = sprint(showerror, e) @test typeof(output) == String @test occursin("NotImplemented", output) @test occursin("feature not ready", output) # Test enriched version - e_enriched = CTBase.NotImplemented( + e_enriched = Exceptions.NotImplemented( "method not implemented", required_method="MyAbstractType", suggestion="Implement this method for your concrete type", @@ -86,15 +94,15 @@ function test_exceptions() # Test PreconditionError @testset verbose = VERBOSE showtiming = SHOWTIMING "PreconditionError" begin - e = CTBase.PreconditionError("state must be set before dynamics") - @test_throws CTBase.PreconditionError throw(e) + e = Exceptions.PreconditionError("state must be set before dynamics") + @test_throws Exceptions.PreconditionError throw(e) output = sprint(showerror, e) @test typeof(output) == String @test occursin("PreconditionError", output) @test occursin("state must be set before dynamics", output) # Test enriched version - e_enriched = CTBase.PreconditionError( + e_enriched = Exceptions.PreconditionError( "Cannot call state! twice", reason="state has already been defined for this OCP", suggestion="Create a new OCP instance", @@ -111,15 +119,15 @@ function test_exceptions() # Test ParsingError @testset verbose = VERBOSE showtiming = SHOWTIMING "ParsingError" begin - e = CTBase.ParsingError("syntax error") - @test_throws CTBase.ParsingError throw(e) + e = Exceptions.ParsingError("syntax error") + @test_throws Exceptions.ParsingError throw(e) output = sprint(showerror, e) @test typeof(output) == String @test occursin("ParsingError", output) @test occursin("syntax error", output) # Test enriched version - e_enriched = CTBase.ParsingError( + e_enriched = Exceptions.ParsingError( "unexpected token", location="line 42, column 15", suggestion="Check syntax balance", @@ -134,30 +142,30 @@ function test_exceptions() # Test ExtensionError @testset verbose = VERBOSE showtiming = SHOWTIMING "ExtensionError" begin # Test constructor throws if no dependencies provided - @test_throws CTBase.PreconditionError CTBase.ExtensionError() + @test_throws Exceptions.PreconditionError Exceptions.ExtensionError() # Create with one weak dependency - e = CTBase.ExtensionError(:MyExt) - @test_throws CTBase.ExtensionError throw(e) + e = Exceptions.ExtensionError(:MyExt) + @test_throws Exceptions.ExtensionError throw(e) output = sprint(showerror, e) @test typeof(output) == String @test occursin("ExtensionError", output) @test occursin("MyExt", output) @test occursin("using", output) # Create with multiple weak dependencies - e2 = CTBase.ExtensionError(:Ext1, :Ext2) + e2 = Exceptions.ExtensionError(:Ext1, :Ext2) output2 = sprint(showerror, e2) @test occursin("Ext1", output2) @test occursin("Ext2", output2) # Test with optional message - e_msg = CTBase.ExtensionError(:MyExt; message="to enable feature X") + e_msg = Exceptions.ExtensionError(:MyExt; message="to enable feature X") output_msg = sprint(showerror, e_msg) @test occursin("ExtensionError", output_msg) @test occursin("MyExt", output_msg) @test occursin("to enable feature X", output_msg) # Test enriched version with feature and context - e_enriched = CTBase.ExtensionError( + e_enriched = Exceptions.ExtensionError( :Documenter, :Markdown; message="to generate documentation", @@ -173,15 +181,15 @@ function test_exceptions() # Test SolverFailure @testset verbose = VERBOSE showtiming = SHOWTIMING "SolverFailure" begin - e = CTBase.SolverFailure("solver failed") - @test_throws CTBase.SolverFailure throw(e) + e = Exceptions.SolverFailure("solver failed") + @test_throws Exceptions.SolverFailure throw(e) output = sprint(showerror, e) @test typeof(output) == String @test occursin("SolverFailure", output) @test occursin("solver failed", output) # Test enriched version with retcode - e_enriched = CTBase.SolverFailure( + e_enriched = Exceptions.SolverFailure( "ODE integration failed", retcode=":Unstable", suggestion="Reduce time step", @@ -197,9 +205,13 @@ function test_exceptions() end @testset verbose = VERBOSE showtiming = SHOWTIMING "CTException supertype catch" begin - e = CTBase.IncorrectArgument("msg") - @test_throws CTBase.IncorrectArgument throw(e) + e = Exceptions.IncorrectArgument("msg") + @test_throws Exceptions.IncorrectArgument throw(e) end return nothing end + +end # module + +test_exceptions() = TestExceptions.test_exceptions() diff --git a/test/suite/extensions/test_coverage_edge_cases.jl b/test/suite/extensions/test_coverage_edge_cases.jl index 9908e14a..59cc8c52 100644 --- a/test/suite/extensions/test_coverage_edge_cases.jl +++ b/test/suite/extensions/test_coverage_edge_cases.jl @@ -103,7 +103,6 @@ function test_coverage_edge_cases() err = try TR._run_single_test( :phantom_test; - available_tests=Symbol[], filename_builder=identity, funcname_builder=identity, eval_mode=false, @@ -114,8 +113,7 @@ function test_coverage_edge_cases() e end - @test err isa ErrorException - @test occursin("Test file", err.msg) + @test err isa CTBase.Exceptions.IncorrectArgument @test occursin("not found", err.msg) finally diff --git a/test/suite/extensions/test_coverage_post_process.jl b/test/suite/extensions/test_coverage_post_process.jl index cc225410..0f72b692 100644 --- a/test/suite/extensions/test_coverage_post_process.jl +++ b/test/suite/extensions/test_coverage_post_process.jl @@ -22,7 +22,7 @@ function test_coverage_post_process() e end - @test err isa ErrorException + @test err isa CTBase.Exceptions.PreconditionError @test occursin("no .cov files", lowercase(err.msg)) end end diff --git a/test/suite/extensions/test_documenter_reference.jl b/test/suite/extensions/test_documenter_reference.jl index 9d0c1bc9..6570f0d5 100644 --- a/test/suite/extensions/test_documenter_reference.jl +++ b/test/suite/extensions/test_documenter_reference.jl @@ -70,7 +70,7 @@ function test_documenter_reference() DR = DocumenterReference @testset verbose = VERBOSE showtiming = SHOWTIMING "Invalid primary_modules input" begin - @test_throws ErrorException CTBase.automatic_reference_documentation( + @test_throws CTBase.Exceptions.IncorrectArgument CTBase.automatic_reference_documentation( CTBase.Extensions.DocumenterReferenceTag(); subdirectory="ref", primary_modules=["invalid_string"], # String is not Module or Pair @@ -386,7 +386,7 @@ function test_documenter_reference() ) # public=false, private=false should error - @test_throws ErrorException CTBase.automatic_reference_documentation( + @test_throws CTBase.Exceptions.IncorrectArgument CTBase.automatic_reference_documentation( CTBase.Extensions.DocumenterReferenceTag(); subdirectory="ref", primary_modules=[DocumenterReferenceTestMod], diff --git a/test/suite/extensions/test_testrunner.jl b/test/suite/extensions/test_testrunner.jl index 20454737..c9fce312 100644 --- a/test/suite/extensions/test_testrunner.jl +++ b/test/suite/extensions/test_testrunner.jl @@ -178,36 +178,35 @@ function test_testrunner() # 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 sort(sel) == [:test_core, :test_utils] + @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 sort(sel) == [:test_core, :test_utils] + @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 sel == [:test_utils] + @test sel == ["test_utils.jl"] # Globbing: pattern matching sel = select_tests( ["test_c*"], Symbol[], false, identity; test_dir=temp_dir ) - @test sel == [:test_core] + @test sel == ["test_core.jl"] - # Globbing: match filename? - # candidate=:test_core -> filename="test_core.jl" + # Globbing: match filename? sel = select_tests( ["test_core_jl"], Symbol[], false, identity; test_dir=temp_dir ) - # This won't match regex "^test_core_jl$" against "test_core" or "test_core.jl" + # "^test_core_jl$" doesn't match "test_core.jl" or "test_core" @test isempty(sel) sel = select_tests( ["test_core.jl"], Symbol[], false, identity; test_dir=temp_dir ) - @test sel == [:test_core] + @test sel == ["test_core.jl"] end # ========================================================== @@ -643,8 +642,8 @@ function test_testrunner() end @testset verbose = VERBOSE showtiming = SHOWTIMING "available_tests input validation" begin - @test_throws ArgumentError normalize_available(123) - @test_throws ArgumentError normalize_available(Any[1]) + @test_throws CTBase.Exceptions.IncorrectArgument normalize_available(123) + @test_throws CTBase.Exceptions.IncorrectArgument normalize_available(Any[1]) end @testset verbose = VERBOSE showtiming = SHOWTIMING "symbol resolution: shallowest match" begin @@ -680,7 +679,7 @@ function test_testrunner() # 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 Set(sel) == Set([:z, :a]) + @test Set(sel) == Set(["a.jl", "z.jl"]) end end @@ -703,7 +702,6 @@ function test_testrunner() err = try run_single( :nonexistent_file_xyz123; - available_tests=Symbol[], filename_builder=identity, funcname_builder=identity, eval_mode=true, @@ -713,7 +711,7 @@ function test_testrunner() catch e e end - @test err isa ErrorException + @test err isa CTBase.Exceptions.IncorrectArgument @test occursin("not found", err.msg) end @@ -721,7 +719,6 @@ function test_testrunner() err = try run_single( "does_not_exist_abc123.jl"; - available_tests=Symbol[], filename_builder=identity, funcname_builder=identity, eval_mode=true, @@ -731,7 +728,7 @@ function test_testrunner() catch e e end - @test err isa ErrorException + @test err isa CTBase.Exceptions.IncorrectArgument @test occursin("not found", err.msg) end @@ -750,7 +747,6 @@ function test_testrunner() run_single( stem; - available_tests=Symbol[], filename_builder=identity, funcname_builder=identity, eval_mode=false, @@ -776,7 +772,6 @@ function test_testrunner() run_single( :sym_noeval; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> "test_" * String(n), eval_mode=false, @@ -797,7 +792,6 @@ function test_testrunner() err = try run_single( stem; - available_tests=Symbol[], filename_builder=identity, funcname_builder=identity, eval_mode=true, @@ -808,8 +802,7 @@ function test_testrunner() e end - @test err isa ErrorException - @test occursin("Function", err.msg) + @test err isa CTBase.Exceptions.PreconditionError @test occursin("not found", err.msg) end end @@ -821,7 +814,6 @@ function test_testrunner() err = try run_single( :foo; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=_ -> nothing, eval_mode=true, @@ -832,8 +824,8 @@ function test_testrunner() e end - @test err isa ErrorException - @test occursin("Inconsistency", err.msg) + @test err isa CTBase.Exceptions.PreconditionError + @test occursin("eval_mode=true", err.msg) end end @@ -844,7 +836,6 @@ function test_testrunner() err = try run_single( :bar; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> "test_" * String(n), eval_mode=true, @@ -855,8 +846,7 @@ function test_testrunner() e end - @test err isa ErrorException - @test occursin("Function", err.msg) + @test err isa CTBase.Exceptions.PreconditionError @test occursin("not found", err.msg) end end @@ -872,7 +862,6 @@ function test_testrunner() # Test with funcname_builder returning String run_single( :baz; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> "test_" * String(n), # Returns String eval_mode=true, @@ -883,7 +872,6 @@ function test_testrunner() # Test with funcname_builder returning Symbol run_single( :baz; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), # Returns Symbol eval_mode=true, @@ -969,7 +957,6 @@ function test_testrunner() captured = Ref{Any}(nothing) run_single( :cb_start; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), eval_mode=true, @@ -1002,7 +989,6 @@ function test_testrunner() captured = Ref{Any}(nothing) run_single( :cb_done; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), eval_mode=true, @@ -1037,7 +1023,6 @@ function test_testrunner() done_captured = Ref{Any}(nothing) run_single( :cb_skip; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), eval_mode=true, @@ -1067,7 +1052,6 @@ function test_testrunner() done_captured = Ref{Any}(nothing) run_single( :cb_noeval; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), eval_mode=false, @@ -1096,7 +1080,6 @@ function test_testrunner() err = try run_single( :cb_err; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), eval_mode=true, @@ -1135,7 +1118,6 @@ function test_testrunner() done_captured = Ref{Any}(nothing) run_single( :cb_both; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), eval_mode=true, @@ -1166,7 +1148,6 @@ function test_testrunner() done_captured = Ref{Any}(nothing) run_single( "test_cb_str.jl"; - available_tests=Symbol[], filename_builder=identity, funcname_builder=identity, eval_mode=true, @@ -1194,7 +1175,6 @@ function test_testrunner() # Should not error when callbacks are nothing run_single( :cb_none; - available_tests=Symbol[], filename_builder=n -> "test_" * String(n), funcname_builder=n -> Symbol(:test_, n), eval_mode=true,