From 3e201205e9dfb4733474455ea9be4752ff899895 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 9 Jun 2026 12:20:57 +0200 Subject: [PATCH 1/4] Add placeholder for exception presentation review work --- .exception-presentation-review-placeholder | 2 ++ src/Unicode/subscripts.jl | 15 ++++++++++++--- src/Unicode/superscripts.jl | 15 ++++++++++++--- test/suite/unicode/test_subscripts.jl | 6 +++--- test/suite/unicode/test_superscripts.jl | 6 +++--- 5 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 .exception-presentation-review-placeholder diff --git a/.exception-presentation-review-placeholder b/.exception-presentation-review-placeholder new file mode 100644 index 00000000..d9a2815d --- /dev/null +++ b/.exception-presentation-review-placeholder @@ -0,0 +1,2 @@ +# Placeholder for exception presentation review work +# This file will be removed once the actual work is done diff --git a/src/Unicode/subscripts.jl b/src/Unicode/subscripts.jl index d08b7a71..878e2786 100644 --- a/src/Unicode/subscripts.jl +++ b/src/Unicode/subscripts.jl @@ -17,13 +17,22 @@ julia> CTBase.ctindice(3) ``` """ function ctindice(i::Int)::Char - if i < 0 || i > 9 + if i < 0 + throw( + Exceptions.IncorrectArgument( + "the subscript must be positive"; + got=string(i), + expected="≥ 0", + context="Unicode subscript generation", + ), + ) + elseif i > 9 throw( Exceptions.IncorrectArgument( - "the subscript must be between 0 and 9"; + "the subscript must be a single digit"; got=string(i), expected="0-9", - suggestion="Use ctindices() for numbers larger than 9, or check your input value", + suggestion="Use ctindices() for numbers with multiple digits", context="Unicode subscript generation", ), ) diff --git a/src/Unicode/superscripts.jl b/src/Unicode/superscripts.jl index 3ef40683..711ae2bb 100644 --- a/src/Unicode/superscripts.jl +++ b/src/Unicode/superscripts.jl @@ -18,13 +18,22 @@ julia> CTBase.ctupperscript(2) ``` """ function ctupperscript(i::Int)::Char - if i < 0 || i > 9 + if i < 0 + throw( + Exceptions.IncorrectArgument( + "the superscript must be positive"; + got=string(i), + expected="≥ 0", + context="Unicode superscript generation", + ), + ) + elseif i > 9 throw( Exceptions.IncorrectArgument( - "the superscript must be between 0 and 9"; + "the superscript must be a single digit"; got=string(i), expected="0-9", - suggestion="Use ctupperscripts() for numbers larger than 9, or check your input value", + suggestion="Use ctupperscripts() for numbers with multiple digits", context="Unicode superscript generation", ), ) diff --git a/test/suite/unicode/test_subscripts.jl b/test/suite/unicode/test_subscripts.jl index 8adae9b8..ba712e44 100644 --- a/test/suite/unicode/test_subscripts.jl +++ b/test/suite/unicode/test_subscripts.jl @@ -46,9 +46,8 @@ function test_subscripts() catch e Test.@test e isa Exceptions.IncorrectArgument Test.@test e.got == "-1" - Test.@test e.expected == "0-9" - Test.@test occursin("subscript must be between 0 and 9", e.msg) - Test.@test occursin("ctindices()", e.suggestion) + Test.@test e.expected == "≥ 0" + Test.@test occursin("subscript must be positive", e.msg) Test.@test e.context == "Unicode subscript generation" end @@ -62,6 +61,7 @@ function test_subscripts() Test.@test e isa Exceptions.IncorrectArgument Test.@test e.got == "15" Test.@test e.expected == "0-9" + Test.@test occursin("subscript must be a single digit", e.msg) Test.@test occursin("ctindices()", e.suggestion) Test.@test e.context == "Unicode subscript generation" end diff --git a/test/suite/unicode/test_superscripts.jl b/test/suite/unicode/test_superscripts.jl index 7bffedb6..eb36e1b0 100644 --- a/test/suite/unicode/test_superscripts.jl +++ b/test/suite/unicode/test_superscripts.jl @@ -46,9 +46,8 @@ function test_superscripts() catch e Test.@test e isa Exceptions.IncorrectArgument Test.@test e.got == "-1" - Test.@test e.expected == "0-9" - Test.@test occursin("superscript must be between 0 and 9", e.msg) - Test.@test occursin("ctupperscripts()", e.suggestion) + Test.@test e.expected == "≥ 0" + Test.@test occursin("superscript must be positive", e.msg) Test.@test e.context == "Unicode superscript generation" end @@ -62,6 +61,7 @@ function test_superscripts() Test.@test e isa Exceptions.IncorrectArgument Test.@test e.got == "12" Test.@test e.expected == "0-9" + Test.@test occursin("superscript must be a single digit", e.msg) Test.@test occursin("ctupperscripts()", e.suggestion) Test.@test e.context == "Unicode superscript generation" end From 992656f4fdf5049d006f0380868eeb2804b04eb4 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 9 Jun 2026 12:20:57 +0200 Subject: [PATCH 2/4] Differentiate error messages in ctindice() and ctupperscript() for negative vs multi-digit values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split error condition in ctindice(): i < 0 now shows "must be positive" (expected: ≥ 0), i > 9 shows "must be a single digit" with suggestion to use ctindices() - Split error condition in ctupperscript(): same pattern with ctupperscripts() suggestion - Updated tests to verify new error messages - All 90 tests in test/suite/unicode/ pass From 43ccfb6c029efe265c78c329858be3ec7059cbee Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 9 Jun 2026 12:21:40 +0200 Subject: [PATCH 3/4] Remove placeholder file --- .exception-presentation-review-placeholder | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .exception-presentation-review-placeholder diff --git a/.exception-presentation-review-placeholder b/.exception-presentation-review-placeholder deleted file mode 100644 index d9a2815d..00000000 --- a/.exception-presentation-review-placeholder +++ /dev/null @@ -1,2 +0,0 @@ -# Placeholder for exception presentation review work -# This file will be removed once the actual work is done From 40fbfcc9fec5d27966450242ac62eb8dbfd102cd Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 9 Jun 2026 13:30:14 +0200 Subject: [PATCH 4/4] Redesign exception display with improved formatting and structure - Refactored display.jl to use modular rendering functions - Added structured exception context display with sections - Improved error message formatting with clear hierarchy - Updated tests to cover new display behavior - Updated documentation in exceptions.md and index.md --- docs/src/guide/exceptions.md | 55 +- docs/src/index.md | 2 +- src/Exceptions/display.jl | 546 ++++++++++++------ .../exceptions/test_exception_display.jl | 112 ++-- test/suite/exceptions/test_exceptions.jl | 18 +- 5 files changed, 461 insertions(+), 272 deletions(-) diff --git a/docs/src/guide/exceptions.md b/docs/src/guide/exceptions.md index d2a3b91c..62d26d51 100644 --- a/docs/src/guide/exceptions.md +++ b/docs/src/guide/exceptions.md @@ -236,7 +236,6 @@ CTBase.Exceptions.ParsingError <: CTBase.Exceptions.CTException - `msg::String`: Error message - `location::Union{String,Nothing}`: Where in the input the error occurred (optional) - `suggestion::Union{String,Nothing}`: How to fix the syntax (optional) -- `context::Union{String,Nothing}`: What was being parsed (optional) **Example**: @@ -245,8 +244,7 @@ using CTBase throw(CTBase.Exceptions.ParsingError( "unexpected token 'end'", location="line 42, column 10", - suggestion="Check for unmatched 'begin' or remove extra 'end'", - context="control flow parsing" + suggestion="Check for unmatched 'begin' or remove extra 'end'" )) ``` @@ -282,9 +280,14 @@ end The enriched display automatically suggests: ```text -❌ Error: ExtensionError, missing dependencies -📦 Missing dependencies: Plots -💡 Suggestion: julia> using Plots +ExtensionError +│ +│ missing dependencies +│ +│ Missing Plots +│ +│ Hint Run: using Plots +└─ ``` **Use this exception** when: @@ -331,10 +334,15 @@ end The enriched display shows the solver-specific return code: ```text -❌ Error: SolverFailure, ODE integration failed -🔧 Return code: :Unstable -📂 Context: SciML integrator -💡 Suggestion: Reduce time step or check initial conditions +SolverFailure +│ +│ ODE integration failed +│ +│ Retcode :Unstable +│ +│ Context SciML integrator +│ Hint Reduce time step or check initial conditions +└─ ``` **Common return codes**: @@ -372,23 +380,24 @@ The enriched display shows the solver-specific return code: All CTBase exceptions provide an enriched, user-friendly display with: -- **🎯 Clear error type and message** -- **📋 Contextual information** (got/expected, reason, location) -- **💡 Actionable suggestions** for fixing the problem -- **📍 User code location** tracking -- **🎨 Emoji-based visual hierarchy** +- **Type name** on line 1, with caller location when available +- **Pipe-box layout** using `│` separators for visual structure +- **Aligned labels** (Got, Expected, Reason, Hint, Context, …) padded to a common width +- **Color coding**: red for type name, yellow for warnings, green for hints +- **Dynamic Hint** for `ExtensionError` generated from `weakdeps` Example of enriched display: ```text -Control Toolbox Error - -❌ Error: PreconditionError, System must be initialized before configuration -❓ Reason: initialize! not called yet -📂 Context: system configuration -💡 Suggestion: Call initialize!(state) before configure! -📍 In your code: - configure! at MyModule.jl:42 +PreconditionError → configure! MyModule.jl:42 +│ +│ System must be initialized before configuration +│ +│ Reason initialize! not called yet +│ +│ Context system configuration +│ Hint Call initialize!(state) before configure! +└─ ``` This makes debugging faster by providing all the information needed to understand and fix the problem. diff --git a/docs/src/index.md b/docs/src/index.md index 0e71f964..a92bc260 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -44,7 +44,7 @@ CTBase.Descriptions.complete(:euler, :explicit; descriptions=descs) try throw(CTBase.Exceptions.IncorrectArgument("n must be positive"; got="-1")) catch e - println(e) + showerror(stdout, e) end ``` diff --git a/src/Exceptions/display.jl b/src/Exceptions/display.jl index dc7d21ae..98c3fedf 100644 --- a/src/Exceptions/display.jl +++ b/src/Exceptions/display.jl @@ -1,36 +1,70 @@ # Custom display functions for user-friendly error messages +# ANSI formatting primitives """ -Generate ANSI escape sequence for the specified color and formatting. -""" -function _ansi_color(color::Symbol, bold::Bool=false) - color_codes = Dict( - :black => 30, - :red => 31, - :green => 32, - :yellow => 33, - :blue => 34, - :magenta => 35, - :cyan => 36, - :white => 37, - :default => 39, - ) - - code = get(color_codes, color, 39) - return bold ? "\033[1;$(code)m" : "\033[$(code)m" -end +$(TYPEDSIGNATURES) + +Apply dimmed (faint) ANSI styling to a string. -"""Generate ANSI reset sequence to clear formatting.""" -_ansi_reset() = "\033[0m" +# Arguments +- `s::AbstractString`: The string to style. +# Returns +- `String`: The string wrapped in dim ANSI escape codes. """ -Print text with ANSI color formatting for Documenter compatibility. +_dim(s) = "\033[2m$(s)\033[0m" + """ -function _print_ansi_styled( - io, text::Union{String,Symbol,Type}, color::Symbol, bold::Bool=false -) - return print(io, _ansi_color(color, bold), string(text), _ansi_reset()) -end +$(TYPEDSIGNATURES) + +Apply bold ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. + +# Returns +- `String`: The string wrapped in bold ANSI escape codes. +""" +_bold(s) = "\033[1m$(s)\033[0m" + +""" +$(TYPEDSIGNATURES) + +Apply red ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. + +# Returns +- `String`: The string wrapped in red ANSI escape codes. +""" +_red(s) = "\033[1;31m$(s)\033[0m" + +""" +$(TYPEDSIGNATURES) + +Apply yellow ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. + +# Returns +- `String`: The string wrapped in yellow ANSI escape codes. +""" +_yellow(s) = "\033[33m$(s)\033[0m" + +""" +$(TYPEDSIGNATURES) + +Apply green ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. + +# Returns +- `String`: The string wrapped in green ANSI escape codes. +""" +_green(s) = "\033[32m$(s)\033[0m" """ extract_user_frames(st::Vector) @@ -60,199 +94,345 @@ function _extract_user_frames(st::Vector) end """ - format_user_friendly_error(io::IO, e::CTException) +$(TYPEDSIGNATURES) -Display an error in a user-friendly format with clear sections and user code location. +Format a diagnostic tag for `AmbiguousDescription` display, expanding shorthand tags into human-readable messages. # Arguments -- `io::IO`: Output stream -- `e::CTException`: The exception to display +- `diagnostic::String`: The diagnostic tag (e.g., "empty catalog", "unknown symbols", "no complete match"). + +# Returns +- `String`: The expanded human-readable message. + +# Notes +- Unknown tags are returned unchanged. """ -function _format_user_friendly_error(io::IO, e::CTException) - #println(io, "\n" * "━"^70) - _print_ansi_styled(io, "Control Toolbox Error\n", :red, true) - #println(io, "─"^28) - - # Main problem - print(io, "\n❌ Error: ") - _print_ansi_styled(io, typeof(e), :red, true) - println(io, ", ", e.msg) - - # Type-specific details - if e isa IncorrectArgument - if !isnothing(e.got) - print(io, "🔍 Got: ", e.got) - if !isnothing(e.expected) - print(io, ", Expected: ", e.expected) - end - println(io) - end +function _format_diagnostic(diagnostic::String) + if diagnostic == "empty catalog" + return "Empty catalog — no descriptions available" + elseif diagnostic == "unknown symbols" + return "Unknown symbols — none of the requested symbols appear in any available description" + elseif diagnostic == "no complete match" + return "No complete match — no description contains all symbols" + else + return diagnostic + end +end - if !isnothing(e.context) - println(io, "📂 Context: ", e.context) - end +""" +$(TYPEDSIGNATURES) - if !isnothing(e.suggestion) - println(io, "💡 Suggestion: ", e.suggestion) - end +Build primary field `(label, value, color)` tuples for `IncorrectArgument` display. - elseif e isa PreconditionError - if !isnothing(e.reason) - println(io, "❓ Reason: ", e.reason) - end +# Arguments +- `e::IncorrectArgument`: The exception instance. - if !isnothing(e.context) - println(io, "📂 Context: ", e.context) - end +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples. + Colors are `:yellow` for warnings, `:green` for expected values, `:default` otherwise. - if !isnothing(e.suggestion) - println(io, "💡 Suggestion: ", e.suggestion) - end +# Notes +- Only includes fields that are not `nothing`. +""" +function _build_primary_pairs(e::IncorrectArgument) + pairs = [] + !isnothing(e.got) && push!(pairs, ("Got", string(e.got), :yellow)) + !isnothing(e.expected) && push!(pairs, ("Expected", string(e.expected), :green)) + return pairs +end - elseif e isa NotImplemented - if !isnothing(e.required_method) - println(io, "🔧 Required method: ", e.required_method) - end +""" +$(TYPEDSIGNATURES) - if !isnothing(e.context) - println(io, "📂 Context: ", e.context) - end +Build primary field `(label, value, color)` tuples for `PreconditionError` display. - if !isnothing(e.suggestion) - println(io, "💡 Suggestion: ", e.suggestion) - end +# Arguments +- `e::PreconditionError`: The exception instance. - elseif e isa ParsingError - if !isnothing(e.location) - println(io, "📍 Location: ", e.location) - end +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples. - if !isnothing(e.suggestion) - println(io, "💡 Suggestion: ", e.suggestion) - end +# Notes +- Only includes fields that are not `nothing`. +""" +function _build_primary_pairs(e::PreconditionError) + pairs = [] + !isnothing(e.reason) && push!(pairs, ("Reason", string(e.reason), :default)) + return pairs +end - elseif e isa AmbiguousDescription - # Show diagnostic first for clarity - on one line - if !isnothing(e.diagnostic) - print(io, "⚠️ Diagnostic: ") - if e.diagnostic == "empty catalog" - _print_ansi_styled(io, "Empty catalog", :yellow, true) - print(io, " - no descriptions available") - elseif e.diagnostic == "unknown symbols" - _print_ansi_styled(io, "Unknown symbols", :yellow, true) - print( - io, - " - none of the requested symbols appear in any available description", - ) - elseif e.diagnostic == "no complete match" - _print_ansi_styled(io, "No complete match", :yellow, true) - print(io, " - no available description contains all the requested symbols") - else - print(io, e.diagnostic) - end - println(io) - end +""" +$(TYPEDSIGNATURES) - # Requested description on one line - println(io, "🎯 Requested description: ", e.description) +Build primary field `(label, value, color)` tuples for `NotImplemented` display. - if !isnothing(e.candidates) && !isempty(e.candidates) - println(io, "📋 Available descriptions:") - for candidate in e.candidates - println(io, " - ", candidate) - end - end +# Arguments +- `e::NotImplemented`: The exception instance. - if !isnothing(e.context) - println(io, "📂 Context: ", e.context) - end +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples. - # Suggestion on one line - if !isnothing(e.suggestion) - print(io, "💡 Suggestion: ", e.suggestion) - - # Show closest matches directly in the suggestion if it ends with ":" - if endswith(strip(e.suggestion), ":") && - contains(e.suggestion, "closest matches") - if !isnothing(e.candidates) && !isempty(e.candidates) - # Show up to 3 candidates as closest matches - max_show = min(3, length(e.candidates)) - for i in 1:max_show - if i == 1 - print(io, " ", e.candidates[i]) - else - print(io, ", ", e.candidates[i]) - end - end - end - end - println(io) - end +# Notes +- Only includes fields that are not `nothing`. +""" +function _build_primary_pairs(e::NotImplemented) + pairs = [] + !isnothing(e.required_method) && push!(pairs, ("Method", string(e.required_method), :default)) + return pairs +end - elseif e isa ExtensionError - # Missing dependencies on one line - print(io, "📦 Missing dependencies: ") - for (i, dep) in enumerate(e.weakdeps) - if i == 1 - print(io, dep) - else - print(io, ", ", dep) - end - end - println(io) +""" +$(TYPEDSIGNATURES) + +Build primary field `(label, value, color)` tuples for `ParsingError` display. + +# Arguments +- `e::ParsingError`: The exception instance. + +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples. + +# Notes +- Only includes fields that are not `nothing`. +""" +function _build_primary_pairs(e::ParsingError) + pairs = [] + !isnothing(e.location) && push!(pairs, ("Location", string(e.location), :default)) + return pairs +end + +""" +$(TYPEDSIGNATURES) + +Build primary field `(label, value, color)` tuples for `AmbiguousDescription` display. + +# Arguments +- `e::AmbiguousDescription`: The exception instance. + +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples. + +# Notes +- The `candidates` field is passed as a `Vector{String}` for multi-line rendering. +- Only includes fields that are not `nothing` or empty. +""" +function _build_primary_pairs(e::AmbiguousDescription) + pairs = [] + !isnothing(e.diagnostic) && push!(pairs, ("Diagnostic", _format_diagnostic(e.diagnostic), :yellow)) + push!(pairs, ("Requested", string(e.description), :default)) + if !isnothing(e.candidates) && !isempty(e.candidates) + push!(pairs, ("Available", e.candidates, :default)) + end + return pairs +end + +""" +$(TYPEDSIGNATURES) + +Build primary field `(label, value, color)` tuples for `ExtensionError` display. + +# Arguments +- `e::ExtensionError`: The exception instance. - # Suggestion on one line - print(io, "💡 Suggestion: ") - _print_ansi_styled(io, "julia>", :green, true) - _print_ansi_styled(io, " using ", :magenta) - for (i, dep) in enumerate(e.weakdeps) +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Single tuple with the missing dependencies. + +# Notes +- Always includes the `Missing` field with joined `weakdeps`. +""" +function _build_primary_pairs(e::ExtensionError) + dep_str = join(string.(e.weakdeps), ", ") + return [("Missing", dep_str, :yellow)] +end + +""" +$(TYPEDSIGNATURES) + +Build primary field `(label, value, color)` tuples for `SolverFailure` display. + +# Arguments +- `e::SolverFailure`: The exception instance. + +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples. + +# Notes +- Only includes fields that are not `nothing`. +""" +function _build_primary_pairs(e::SolverFailure) + pairs = [] + !isnothing(e.retcode) && push!(pairs, ("Retcode", string(e.retcode), :yellow)) + return pairs +end + +""" +$(TYPEDSIGNATURES) + +Build secondary field `(label, value, color)` tuples for generic `CTException` display. + +# Arguments +- `e::CTException`: The exception instance. + +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples for + `Context` and `Hint` (from `suggestion`) fields. + +# Notes +- Only includes fields that exist and are not `nothing`. +- `Hint` is colored green for visibility. +""" +function _build_secondary_pairs(e::CTException) + pairs = [] + hasfield(typeof(e), :context) && !isnothing(e.context) && push!(pairs, ("Context", e.context, :default)) + hasfield(typeof(e), :suggestion) && !isnothing(e.suggestion) && push!(pairs, ("Hint", e.suggestion, :green)) + return pairs +end + +""" +$(TYPEDSIGNATURES) + +Build secondary field `(label, value, color)` tuples for `ExtensionError` display. + +# Arguments +- `e::ExtensionError`: The exception instance. + +# Returns +- `Vector{Tuple{String, Any, Symbol}}`: Vector of `(label, value, color)` tuples. + The `Hint` is dynamically generated from `weakdeps`. + +# Notes +- The `Hint` field is generated as "Run: using " followed by the joined dependency names. +- This override is necessary because `ExtensionError` does not have a `suggestion` field. +""" +function _build_secondary_pairs(e::ExtensionError) + pairs = [] + !isnothing(e.context) && push!(pairs, ("Context", e.context, :default)) + hint = "Run: using " * join(string.(e.weakdeps), ", ") + push!(pairs, ("Hint", hint, :green)) + return pairs +end + +""" +$(TYPEDSIGNATURES) + +Print a pipe-formatted field with dynamic label alignment. + +# Arguments +- `io::IO`: Output stream. +- `label::String`: The field label. +- `value`: The field value (can be a `Vector` for multi-line rendering). +- `max_len::Int`: The maximum label length for alignment. +- `color::Symbol`: The color symbol (`:yellow`, `:green`, or `:default`). + +# Notes +- For `Vector` values, the first line includes the label, subsequent lines are indented. +- The pipe character `│` is dimmed for visual hierarchy. +""" +function _print_pipe_field(io, label::String, value, max_len::Int, color::Symbol) + if value isa Vector + # Multi-line case: AmbiguousDescription.candidates + for (i, v) in enumerate(value) if i == 1 - print(io, dep) + print(io, _dim("│"), " ", _bold(rpad(label, max_len)), " ") + _print_colored(io, string(v), color) + println(io) else - print(io, ", ", dep) + println(io, _dim("│"), " ", " "^max_len, " ", string(v)) end end + else + # Single value + print(io, _dim("│"), " ", _bold(rpad(label, max_len)), " ") + _print_colored(io, string(value), color) println(io) + end +end - elseif e isa SolverFailure - # Return code - if !isnothing(e.retcode) - print(io, "🔧 Return code: ") - _print_ansi_styled(io, e.retcode, :yellow, true) - println(io) - end +""" +$(TYPEDSIGNATURES) - if !isnothing(e.context) - println(io, "📂 Context: ", e.context) - end +Print colored text based on a color symbol. - if !isnothing(e.suggestion) - println(io, "💡 Suggestion: ", e.suggestion) - end +# Arguments +- `io::IO`: Output stream. +- `text::AbstractString`: The text to print. +- `color::Symbol`: The color symbol (`:yellow`, `:green`, or any other for default). + +# Notes +- `:yellow` and `:green` apply ANSI styling; all other colors print as plain text. +""" +function _print_colored(io, text, color::Symbol) + if color == :yellow + print(io, _yellow(text)) + elseif color == :green + print(io, _green(text)) + else + print(io, text) end +end + +""" + format_user_friendly_error(io::IO, e::CTException) + +Display an error in a user-friendly format with clear sections and user code location. - # Add user code location +# Arguments +- `io::IO`: Output stream +- `e::CTException`: The exception to display +""" +function _format_user_friendly_error(io::IO, e::CTException) + # Line 1: TypeName → func file:line user_frames = _extract_user_frames(stacktrace(catch_backtrace())) - if !isempty(user_frames) - println(io, "📍 In your code:") - # Show up to 3 most relevant user frames - for (i, frame) in enumerate(user_frames[1:min(3, length(user_frames))]) - file_name = basename(string(frame.file)) - line_info = frame.line - func_name = frame.func + frame = isempty(user_frames) ? nothing : user_frames[1] - if i == 1 - # The most recent frame (where error occurred) - println(io, " $func_name at $file_name:$line_info") - else - # Previous frames (call stack) - show call hierarchy with visual arrows - arrow_prefix = " " * " "^(i-2) * "└── " - println(io, "$(arrow_prefix)$func_name at $file_name:$line_info") - end + type_name = string(nameof(typeof(e))) + print(io, _red(type_name)) + + if !isnothing(frame) + func_name = string(frame.func) + file_name = basename(string(frame.file)) + line_num = frame.line + print(io, " ", _dim("→"), " ", _bold(func_name), " ") + print(io, _yellow("$(file_name):$(line_num)")) + end + println(io) + + # Blank pipe separator + println(io, _dim("│")) + + # Message + println(io, _dim("│"), " ", _bold(e.msg)) + + # Build field pairs + primary_pairs = _build_primary_pairs(e) + secondary_pairs = _build_secondary_pairs(e) + all_pairs = vcat(primary_pairs, secondary_pairs) + + if !isempty(all_pairs) + # Global max_len for alignment + max_len = maximum(length(p[1]) for p in all_pairs) + + # Blank pipe separator + println(io, _dim("│")) + + # Primary fields + for p in primary_pairs + _print_pipe_field(io, p[1], p[2], max_len, p[3]) + end + + # Separator between primary and secondary + if !isempty(primary_pairs) && !isempty(secondary_pairs) + println(io, _dim("│")) + end + + # Secondary fields + for p in secondary_pairs + _print_pipe_field(io, p[1], p[2], max_len, p[3]) end end - #println(io, "━"^70 * "\n") + # Closing visual + println(io, _dim("└─")) end """ diff --git a/test/suite/exceptions/test_exception_display.jl b/test/suite/exceptions/test_exception_display.jl index 0ec180e6..a2a8ebc5 100644 --- a/test/suite/exceptions/test_exception_display.jl +++ b/test/suite/exceptions/test_exception_display.jl @@ -27,15 +27,15 @@ function test_exception_display() output = String(take!(io)) # Check for key sections in user-friendly display - Test.@test contains(output, "Control Toolbox Error") + Test.@test contains(output, "IncorrectArgument") Test.@test contains(output, "Test error") - Test.@test contains(output, "Got:") + Test.@test contains(output, "Got") Test.@test contains(output, "value1") - Test.@test contains(output, "Expected:") + Test.@test contains(output, "Expected") Test.@test contains(output, "value2") - Test.@test contains(output, "Context:") + Test.@test contains(output, "Context") Test.@test contains(output, "test function") - Test.@test contains(output, "Suggestion:") + Test.@test contains(output, "Hint") Test.@test contains(output, "Fix it like this") end @@ -51,9 +51,9 @@ function test_exception_display() # Check for compact format Test.@test contains(output, "IncorrectArgument") Test.@test contains(output, "Test error") - Test.@test contains(output, "Got:") + Test.@test contains(output, "Got") Test.@test contains(output, "value1") - Test.@test contains(output, "Expected:") + Test.@test contains(output, "Expected") Test.@test contains(output, "value2") # Reset to default @@ -69,10 +69,10 @@ function test_exception_display() output = String(take!(io)) Test.@test contains(output, "Simple error") - Test.@test !contains(output, "Got:") - Test.@test !contains(output, "Expected:") - Test.@test !contains(output, "Context:") - Test.@test !contains(output, "Suggestion:") + Test.@test !contains(output, "Got ") + Test.@test !contains(output, "Expected ") + Test.@test !contains(output, "Context ") + Test.@test !contains(output, "Hint ") end Test.@testset "PreconditionError - User-Friendly Display" begin @@ -87,11 +87,11 @@ function test_exception_display() Test.@test_nowarn showerror(io, e) output = String(take!(io)) - Test.@test contains(output, "Control Toolbox Error") + Test.@test contains(output, "PreconditionError") Test.@test contains(output, "State must be set before dynamics") - Test.@test contains(output, "Reason:") + Test.@test contains(output, "Reason") Test.@test contains(output, "state has not been defined yet") - Test.@test contains(output, "Suggestion:") + Test.@test contains(output, "Hint") Test.@test contains(output, "Call state!(ocp, dimension)") end @@ -104,7 +104,7 @@ function test_exception_display() Test.@test_nowarn showerror(io, e) output = String(take!(io)) Test.@test contains(output, "Feature not implemented") - Test.@test contains(output, "Required method:") + Test.@test contains(output, "Method") Test.@test contains(output, "MyType") # CTBase.set_show_full_stacktrace!(false) @@ -119,7 +119,7 @@ function test_exception_display() Test.@test_nowarn showerror(io, e) output = String(take!(io)) Test.@test contains(output, "Syntax error") - Test.@test contains(output, "Location:") + Test.@test contains(output, "Location") Test.@test contains(output, "line 42") # CTBase.set_show_full_stacktrace!(false) @@ -163,7 +163,7 @@ function test_exception_display() output = String(take!(io)) Test.@test contains(output, "AmbiguousDescription") Test.@test contains(output, "(:f,)") - Test.@test contains(output, "Available descriptions:") + Test.@test contains(output, "Available") Test.@test contains(output, "(:a, :b)") Test.@test contains(output, "algorithm selection") @@ -185,11 +185,11 @@ function test_exception_display() Test.@test_nowarn showerror(io, e) output = String(take!(io)) Test.@test contains(output, "ExtensionError") - Test.@test contains(output, "Missing dependencies:") + Test.@test contains(output, "Missing") Test.@test contains(output, "Plots") Test.@test contains(output, "PlotlyJS") - # Check for ANSI-formatted julia> using (colors now work in documentation) - Test.@test contains(output, "\e[1;32mjulia>\e[0m\e[35m using \e[0m") + # Check for generated Hint + Test.@test contains(output, "Run: using") # CTBase.set_show_full_stacktrace!(false) end @@ -243,11 +243,11 @@ function test_exception_display() Test.@test contains(output, "NotImplemented") Test.@test contains(output, "Not implemented feature") - Test.@test contains(output, "Required method:") + Test.@test contains(output, "Method") Test.@test contains(output, "MyType") - Test.@test contains(output, "Context:") + Test.@test contains(output, "Context") Test.@test contains(output, "testing context") - Test.@test contains(output, "Suggestion:") + Test.@test contains(output, "Hint") Test.@test contains(output, "use this instead") end @@ -263,8 +263,8 @@ function test_exception_display() Test.@test contains(output, "ParsingError") Test.@test contains(output, "Parse error") # Should not contain optional sections that are not provided - Test.@test !contains(output, "Location:") - Test.@test !contains(output, "Suggestion:") + Test.@test !contains(output, "Location ") + Test.@test !contains(output, "Hint ") end Test.@testset "ParsingError - All optional fields" begin @@ -277,9 +277,9 @@ function test_exception_display() Test.@test contains(output, "ParsingError") Test.@test contains(output, "Parse error") - Test.@test contains(output, "Location:") + Test.@test contains(output, "Location") Test.@test contains(output, "line 10") - Test.@test contains(output, "Suggestion:") + Test.@test contains(output, "Hint") Test.@test contains(output, "check syntax") end @@ -293,12 +293,12 @@ function test_exception_display() output = String(take!(io)) Test.@test contains(output, "ExtensionError") - Test.@test contains(output, "Missing dependencies:") + Test.@test contains(output, "Missing") Test.@test contains(output, "TestDep") # Should not contain optional sections that are not provided - Test.@test !contains(output, "Feature:") - Test.@test !contains(output, "Context:") - Test.@test !contains(output, "Purpose:") + Test.@test !contains(output, "Feature ") + Test.@test !contains(output, "Context ") + Test.@test !contains(output, "Purpose ") end Test.@testset "PreconditionError - Missing optional fields" begin @@ -308,9 +308,9 @@ function test_exception_display() output = String(take!(io)) Test.@test contains(output, "Simple error") - Test.@test !contains(output, "Reason:") - Test.@test !contains(output, "Context:") - Test.@test !contains(output, "Suggestion:") + Test.@test !contains(output, "Reason ") + Test.@test !contains(output, "Context ") + Test.@test !contains(output, "Hint ") end Test.@testset "AmbiguousDescription - Missing optional fields" begin @@ -321,9 +321,9 @@ function test_exception_display() Test.@test contains(output, "AmbiguousDescription") Test.@test contains(output, "(:f,)") - Test.@test !contains(output, "Available descriptions:") - Test.@test !contains(output, "Context:") - Test.@test !contains(output, "Suggestion:") + Test.@test !contains(output, "Available ") + Test.@test !contains(output, "Context ") + Test.@test !contains(output, "Hint ") end Test.@testset "User code location display" begin @@ -341,10 +341,10 @@ function test_exception_display() output = String(take!(io)) - # The output should contain the user code location section - # (this tests the lines 173-187 that were uncovered) - Test.@test contains(output, "Control Toolbox Error") - Test.@test contains(output, "In your code:") + # The output should contain the type name and closing marker + # (this tests the location-aware formatting path) + Test.@test contains(output, "IncorrectArgument") + Test.@test contains(output, "└─") # In a real test environment, this should show user frames # The exact content depends on the test environment end @@ -364,11 +364,11 @@ function test_exception_display() output = String(take!(io)) Test.@test contains(output, "SolverFailure") Test.@test contains(output, "ODE integration failed") - Test.@test contains(output, "Return code:") + Test.@test contains(output, "Retcode") Test.@test contains(output, ":Unstable") - Test.@test contains(output, "Suggestion:") + Test.@test contains(output, "Hint") Test.@test contains(output, "Reduce time step") - Test.@test contains(output, "Context:") + Test.@test contains(output, "Context") Test.@test contains(output, "SciML integrator") # CTBase.set_show_full_stacktrace!(false) @@ -386,9 +386,9 @@ function test_exception_display() Test.@test contains(output, "SolverFailure") Test.@test contains(output, "Solver failed") # Should not contain optional sections that are not provided - Test.@test !contains(output, "Return code:") - Test.@test !contains(output, "Context:") - Test.@test !contains(output, "Suggestion:") + Test.@test !contains(output, "Retcode ") + Test.@test !contains(output, "Context ") + Test.@test !contains(output, "Hint ") end Test.@testset "SolverFailure - All optional fields" begin @@ -406,11 +406,11 @@ function test_exception_display() Test.@test contains(output, "SolverFailure") Test.@test contains(output, "Optimization did not converge") - Test.@test contains(output, "Return code:") + Test.@test contains(output, "Retcode") Test.@test contains(output, ":MaxIterations") - Test.@test contains(output, "Context:") + Test.@test contains(output, "Context") Test.@test contains(output, "IPOPT solver") - Test.@test contains(output, "Suggestion:") + Test.@test contains(output, "Hint") Test.@test contains(output, "Increase max iterations") end @@ -420,9 +420,9 @@ function test_exception_display() Test.@test_nowarn showerror(io, e) output = String(take!(io)) - Test.@test contains(output, "Got:") + Test.@test contains(output, "Got") Test.@test contains(output, "bad_value") - Test.@test !contains(output, "Expected:") + Test.@test !contains(output, "Expected ") end Test.@testset "AmbiguousDescription - diagnostic rendering" begin @@ -433,7 +433,7 @@ function test_exception_display() ) Test.@test_nowarn showerror(io, e) output = String(take!(io)) - Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "Diagnostic") Test.@test contains(output, "Empty catalog") # "unknown symbols" branch @@ -443,7 +443,7 @@ function test_exception_display() ) Test.@test_nowarn showerror(io, e) output = String(take!(io)) - Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "Diagnostic") Test.@test contains(output, "Unknown symbols") # "no complete match" branch @@ -453,7 +453,7 @@ function test_exception_display() ) Test.@test_nowarn showerror(io, e) output = String(take!(io)) - Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "Diagnostic") Test.@test contains(output, "No complete match") # else branch — arbitrary diagnostic value @@ -463,7 +463,7 @@ function test_exception_display() ) Test.@test_nowarn showerror(io, e) output = String(take!(io)) - Test.@test contains(output, "Diagnostic:") + Test.@test contains(output, "Diagnostic") Test.@test contains(output, "custom diagnostic") end diff --git a/test/suite/exceptions/test_exceptions.jl b/test/suite/exceptions/test_exceptions.jl index 90696407..e1b5e644 100644 --- a/test/suite/exceptions/test_exceptions.jl +++ b/test/suite/exceptions/test_exceptions.jl @@ -30,10 +30,10 @@ function test_exceptions() context="test context", ) output_enriched = sprint(showerror, e_enriched) - Test.@test occursin("Available descriptions", output_enriched) + Test.@test occursin("Available", output_enriched) Test.@test occursin("(:a, :b)", output_enriched) Test.@test occursin("(:c, :d)", output_enriched) - Test.@test occursin("Suggestion", output_enriched) + Test.@test occursin("Hint", output_enriched) Test.@test occursin("Try one of the available descriptions", output_enriched) Test.@test occursin("Context", output_enriched) Test.@test occursin("test context", output_enriched) @@ -61,7 +61,7 @@ function test_exceptions() Test.@test occursin("vector of length 3", output_enriched) Test.@test occursin("Expected", output_enriched) Test.@test occursin("vector of length 2", output_enriched) - Test.@test occursin("Suggestion", output_enriched) + Test.@test occursin("Hint", output_enriched) Test.@test occursin("Resize your vector", output_enriched) Test.@test occursin("Context", output_enriched) Test.@test occursin("initialization", output_enriched) @@ -86,7 +86,7 @@ function test_exceptions() output_enriched = sprint(showerror, e_enriched) Test.@test occursin("Type", output_enriched) Test.@test occursin("MyAbstractType", output_enriched) - Test.@test occursin("Suggestion", output_enriched) + Test.@test occursin("Hint", output_enriched) Test.@test occursin("Implement this method", output_enriched) Test.@test occursin("Context", output_enriched) Test.@test occursin("algorithm execution", output_enriched) @@ -111,7 +111,7 @@ function test_exceptions() output_enriched = sprint(showerror, e_enriched) Test.@test occursin("Reason", output_enriched) Test.@test occursin("state has already been defined", output_enriched) - Test.@test occursin("Suggestion", output_enriched) + Test.@test occursin("Hint", output_enriched) Test.@test occursin("Create a new OCP instance", output_enriched) Test.@test occursin("Context", output_enriched) Test.@test occursin("state definition", output_enriched) @@ -135,7 +135,7 @@ function test_exceptions() output_enriched = sprint(showerror, e_enriched) Test.@test occursin("Location", output_enriched) Test.@test occursin("line 42, column 15", output_enriched) - Test.@test occursin("Suggestion", output_enriched) + Test.@test occursin("Hint", output_enriched) Test.@test occursin("Check syntax balance", output_enriched) end @@ -173,7 +173,7 @@ function test_exceptions() context="reference generation", ) output_enriched = sprint(showerror, e_enriched) - Test.@test occursin("Missing dependencies", output_enriched) + Test.@test occursin("Missing", output_enriched) Test.@test occursin("Documenter", output_enriched) Test.@test occursin("Markdown", output_enriched) Test.@test occursin("to generate documentation", output_enriched) @@ -196,9 +196,9 @@ function test_exceptions() context="SciML integrator", ) output_enriched = sprint(showerror, e_enriched) - Test.@test occursin("Return code", output_enriched) + Test.@test occursin("Retcode", output_enriched) Test.@test occursin(":Unstable", output_enriched) - Test.@test occursin("Suggestion", output_enriched) + Test.@test occursin("Hint", output_enriched) Test.@test occursin("Reduce time step", output_enriched) Test.@test occursin("Context", output_enriched) Test.@test occursin("SciML integrator", output_enriched)