diff --git a/CHANGELOG.md b/CHANGELOG.md index cb75f7bc..7465c5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ 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.30.3-beta] - unreleased + +### 🐛 Bug Fixes + +**`Descriptions.complete` — truncation marker and closest-matches hint** ([#553](https://github.com/control-toolbox/CTBase.jl/issues/553)). +- The candidate list is no longer silently head-truncated: when the catalog has more + descriptions than `max_show`, a `… and N more` line is appended so the `Available` + list is not mistaken for exhaustive. +- The `"Try one of the closest matches:"` hint now actually lists the closest matching + descriptions — previously the computed matches were discarded and the hint was empty. +- The number of candidates shown in error output was raised from 10 to 20. + +Both are display-only corrections; the resolution logic of `complete` is unchanged. + +### ✅ Compatibility + +- **No breaking changes**: only error-message display and the shown-candidate ceiling + changed; `complete`'s resolution behaviour, signatures, and return values are + unchanged. No migration required. + ## [0.30.2-beta] - unreleased ### 📚 Documentation diff --git a/Project.toml b/Project.toml index 2860b494..1c4bb3e5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTBase" uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" -version = "0.30.2-beta" +version = "0.30.3-beta" authors = ["Olivier Cots ", "Jean-Baptiste Caillau "] [deps] diff --git a/src/Descriptions/complete.jl b/src/Descriptions/complete.jl index c558a894..c31ae095 100644 --- a/src/Descriptions/complete.jl +++ b/src/Descriptions/complete.jl @@ -71,7 +71,7 @@ function complete(list::Symbol...; descriptions::Tuple{Vararg{Description}})::De if maximum(table[:, 2]) == 0 # Find similar descriptions for helpful suggestions similar_descs = _find_similar_descriptions(list, descriptions; max_results=5) - all_candidates = _format_description_candidates(descriptions; max_show=10) + all_candidates = _format_description_candidates(descriptions; max_show=20) # Build contextual suggestion suggestion = if !isempty(similar_descs) @@ -93,7 +93,7 @@ function complete(list::Symbol...; descriptions::Tuple{Vararg{Description}})::De throw( Exceptions.AmbiguousDescription( list; - candidates=all_candidates, + candidates=isempty(similar_descs) ? all_candidates : similar_descs, suggestion=suggestion, context="description completion", diagnostic=diagnostic, diff --git a/src/Descriptions/similarity.jl b/src/Descriptions/similarity.jl index f711d910..3ec4f996 100644 --- a/src/Descriptions/similarity.jl +++ b/src/Descriptions/similarity.jl @@ -103,7 +103,9 @@ Format description candidates from a catalog for display in error messages. - `max_show::Int=5`: Maximum number of descriptions to include in the output # Returns -- `Vector{String}`: A vector of formatted description strings +- `Vector{String}`: A vector of formatted description strings. If the catalog has more + than `max_show` descriptions, an extra `"… and N more"` marker line is appended so the + list is not mistaken for exhaustive. # Example @@ -116,17 +118,26 @@ julia> CTBase.Descriptions.format_description_candidates(descriptions; max_show= "(:a, :b)" "(:a, :c)" "(:x, :y)" + +julia> descriptions = ((:a, :b), (:a, :c), (:x, :y), (:p, :q), (:r, :s), (:t, :u)) +julia> CTBase.Descriptions.format_description_candidates(descriptions; max_show=2) +3-element Vector{String}: + "(:a, :b)" + "(:a, :c)" + "… and 4 more" ``` """ function _format_description_candidates( descriptions::Tuple{Vararg{Description}}; max_show::Int=5 )::Vector{String} - if isempty(descriptions) - return String[] - end + isempty(descriptions) && return String[] - # Take up to max_show descriptions - to_show = descriptions[1:min(max_show, length(descriptions))] + n = length(descriptions) + formatted = Vector{String}([string(desc) for desc in descriptions[1:min(max_show, n)]]) + + if n > max_show + push!(formatted, "… and $(n - max_show) more") + end - return Vector{String}([string(desc) for desc in to_show]) + return formatted end diff --git a/test/suite/descriptions/test_complete.jl b/test/suite/descriptions/test_complete.jl index b8ffc97d..fcf06018 100644 --- a/test/suite/descriptions/test_complete.jl +++ b/test/suite/descriptions/test_complete.jl @@ -212,6 +212,57 @@ function test_complete() end end + Test.@testset "issue 553 - truncation marker and closest matches" begin + # ================================================================ + # NON-REGRESSION: issue #553 + # 1) candidate list must not be silently truncated - a + # "… and N more" marker is appended when > max_show. + # 2) the "closest matches" hint must actually list the similar + # descriptions (previously discarded, leaving the hint empty). + # ================================================================ + + # --- Build a catalog of 12, the last two GPU ones (issue scenario) --- + descs = () + for i in 1:10 + descs = Descriptions.add(descs, (:ipopt, Symbol(:cpu_, i))) + end + descs = Descriptions.add(descs, (:gpu, :exact)) + descs = Descriptions.add(descs, (:gpu, :krylov)) + Test.@test length(descs) == 12 + + # 1a. Marker appended by the formatter when > max_show + formatted = Descriptions._format_description_candidates(descs; max_show=10) + Test.@test length(formatted) == 11 # 10 shown + marker + Test.@test formatted[end] == "… and 2 more" + + # 1b. max_show raised to 20 in complete: catalog of 22 must still + # surface the marker line in the raised exception. + descs22 = () + for i in 1:22 + descs22 = Descriptions.add(descs22, (:ipopt, Symbol(:cpu_, i))) + end + err22 = try + Descriptions.complete(:zzz; descriptions=descs22) + catch e + e + end + Test.@test err22 isa Exceptions.AmbiguousDescription + Test.@test length(err22.candidates) == 21 # 20 shown + marker + Test.@test err22.candidates[end] == "… and 2 more" + + # 2. Closest matches are not discarded: candidates == the nearest + # descriptions once similar_descs is non-empty. + err = try + Descriptions.complete(:adnlp, :gpu; descriptions=descs) + catch e + e + end + Test.@test err isa Exceptions.AmbiguousDescription + Test.@test occursin("closest matches", err.suggestion) + Test.@test !isempty(err.candidates) + Test.@test Set(err.candidates) == Set(["(:gpu, :exact)", "(:gpu, :krylov)"]) + end + Test.@testset "Diagnostic field verification" begin # Empty catalog - should have diagnostic try diff --git a/test/suite/descriptions/test_similarity.jl b/test/suite/descriptions/test_similarity.jl index 92c83859..e8842ed8 100644 --- a/test/suite/descriptions/test_similarity.jl +++ b/test/suite/descriptions/test_similarity.jl @@ -164,47 +164,54 @@ function test_similarity() Test.@testset "format_description_candidates - basic" begin descriptions = ((:a, :b), (:a, :c), (:x, :y), (:p, :q), (:r, :s), (:t, :u)) formatted = Descriptions._format_description_candidates(descriptions) - Test.@test length(formatted) == 5 # default max_show=5 + Test.@test length(formatted) == 6 # default max_show=5 + "… and 1 more" marker Test.@test formatted[1] == "(:a, :b)" Test.@test formatted[5] == "(:r, :s)" + Test.@test formatted[end] == "… and 1 more" # Custom max_show formatted3 = Descriptions._format_description_candidates( descriptions; max_show=3 ) - Test.@test length(formatted3) == 3 + Test.@test length(formatted3) == 4 # 3 shown + "… and 3 more" marker Test.@test formatted3[1] == "(:a, :b)" Test.@test formatted3[3] == "(:x, :y)" + Test.@test formatted3[end] == "… and 3 more" end Test.@testset "format_description_candidates - boundaries" begin - # Exactly max_show descriptions + # Exactly max_show descriptions - no marker descriptions = ((:a, :b), (:c, :d), (:e, :f), (:g, :h), (:i, :j)) formatted = Descriptions._format_description_candidates( descriptions; max_show=5 ) Test.@test length(formatted) == 5 + Test.@test !(occursin("more", formatted[end])) - # Less than max_show + # Less than max_show - no marker descriptions2 = ((:a, :b), (:c, :d)) formatted2 = Descriptions._format_description_candidates( descriptions2; max_show=5 ) Test.@test length(formatted2) == 2 - # More than max_show + # More than max_show - marker appended descriptions3 = ((:a, :b), (:c, :d), (:e, :f), (:g, :h), (:i, :j), (:k, :l)) formatted3 = Descriptions._format_description_candidates( descriptions3; max_show=3 ) - Test.@test length(formatted3) == 3 + Test.@test length(formatted3) == 4 # 3 shown + "… and 3 more" marker + Test.@test formatted3[1] == "(:a, :b)" + Test.@test formatted3[3] == "(:e, :f)" + Test.@test formatted3[end] == "… and 3 more" # max_show=1 formatted4 = Descriptions._format_description_candidates( descriptions3; max_show=1 ) - Test.@test length(formatted4) == 1 + Test.@test length(formatted4) == 2 # 1 shown + "… and 5 more" marker Test.@test formatted4[1] == "(:a, :b)" + Test.@test formatted4[end] == "… and 5 more" end Test.@testset "format_description_candidates - edge cases" begin