From c9dbc6857cd0583fd5bd0555e53f4c33672c5258 Mon Sep 17 00:00:00 2001 From: bhuvan-somisetty Date: Mon, 9 Nov 2026 01:43:56 +0530 Subject: [PATCH 1/2] ci: fix C++ tryparam test path and pin Python test job to ubuntu-24.04 The comma-in-value tryparam test wrote concore.params to in/1/, but load_params reads from inpath + "1/concore.params" (in1/), so it never found the file. ubuntu-latest now resolves to 26.04, where setup-python has no 3.9 build. Pin the Python test matrix to ubuntu-24.04 so 3.9 keeps running. Signed-off-by: bhuvan-somisetty --- .github/workflows/ci.yml | 2 +- TestConcoreHpp.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb47fa0..1f01f61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: run: ruff check . --output-format=github test: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] diff --git a/TestConcoreHpp.cpp b/TestConcoreHpp.cpp index 6b6ad2c..f7311af 100644 --- a/TestConcoreHpp.cpp +++ b/TestConcoreHpp.cpp @@ -333,7 +333,7 @@ static void test_load_params_semicolon_format() { static void test_tryparam_comma_in_value() { setup_dirs(); - write_file("in/1/concore.params", "{'channels': [1, 2], 'gain': '0.5'}"); + write_file("in1/concore.params", "{'channels': [1, 2], 'gain': '0.5'}"); Concore c; c.delay = 0; @@ -342,7 +342,7 @@ static void test_tryparam_comma_in_value() { check("tryparam preserves list with comma", c.tryparam("channels", "") == "[1, 2]"); check("tryparam preserves subsequent key", c.tryparam("gain", "") == "0.5"); - rm("in/1/concore.params"); + rm("in1/concore.params"); } // ------------- main ------------------------------------------------------- From 23693ea1fd1651b7d8c5f4cd91040434a38372b4 Mon Sep 17 00:00:00 2001 From: bhuvan-somisetty Date: Fri, 13 Nov 2026 06:24:20 +0530 Subject: [PATCH 2/2] fix(julia): keep list values when loading concore.params load_params!() split dict values on the first comma, so a list like [1.0, 2.0, 3.0] became the string "[1.0". Split items at the top level only and parse list values into Vector{Float64}, same as the Python side. --- concore.jl | 55 ++++++++++++++++++++++++++----- concoredocker.jl | 55 ++++++++++++++++++++++++++----- tests/julia/test_concoredocker.jl | 13 ++++++++ tests/julia/test_config.jl | 32 ++++++++++++++++++ 4 files changed, 137 insertions(+), 18 deletions(-) diff --git a/concore.jl b/concore.jl index af47901..c6a3fd8 100644 --- a/concore.jl +++ b/concore.jl @@ -586,6 +586,46 @@ function load_oport!() return oport end +"""Split `s` on `delim`, ignoring delimiters inside brackets or quotes.""" +function _split_top_level(s::AbstractString, delim::Char)::Vector{String} + parts = String[] + depth = 0 + quote_char = nothing + start = firstindex(s) + for i in eachindex(s) + c = s[i] + if quote_char !== nothing + c == quote_char && (quote_char = nothing) + elseif c == '\'' || c == '"' + quote_char = c + elseif c in ('[', '(', '{') + depth += 1 + elseif c in (']', ')', '}') + depth -= 1 + elseif c == delim && depth == 0 + push!(parts, s[start:prevind(s, i)]) + start = nextind(s, i) + end + end + push!(parts, s[start:end]) + return parts +end + +"""Parse a single params value as a number, a list of numbers, or a string.""" +function _parse_param_value(val_str::AbstractString) + val_str = strip(val_str) + num = tryparse(Float64, val_str) + num !== nothing && return num + if startswith(val_str, "[") && endswith(val_str, "]") + isempty(strip(val_str[2:end-1])) && return Float64[] + try + return safe_parse_list(val_str) + catch + end + end + return strip(val_str, ['\'', '"']) +end + """ load_params!() @@ -614,21 +654,18 @@ function load_params!() if startswith(sparams, "{") # Python dict format: {'key': value, ...} - for m in eachmatch(r"['\"]([^'\"]+)['\"]\s*:\s*([^,}]+)", sparams) - key = m.captures[1] - val_str = strip(m.captures[2]) - val = tryparse(Float64, val_str) - params[key] = val !== nothing ? val : strip(val_str, ['\'', '"']) + inner = endswith(sparams, "}") ? sparams[2:end-1] : sparams[2:end] + for item in _split_top_level(inner, ',') + m = match(r"^\s*['\"]([^'\"]+)['\"]\s*:(.*)$"s, item) + m === nothing && continue + params[m.captures[1]] = _parse_param_value(m.captures[2]) end else # key=value;key2=value2 format for pair in split(sparams, ";") kv = split(strip(pair), "="; limit=2) length(kv) == 2 || continue - key = strip(kv[1]) - val_str = strip(kv[2]) - val = tryparse(Float64, val_str) - params[key] = val !== nothing ? val : val_str + params[strip(kv[1])] = _parse_param_value(kv[2]) end end end diff --git a/concoredocker.jl b/concoredocker.jl index e2c03c1..9baac02 100644 --- a/concoredocker.jl +++ b/concoredocker.jl @@ -587,6 +587,46 @@ function load_oport!() return oport end +"""Split `s` on `delim`, ignoring delimiters inside brackets or quotes.""" +function _split_top_level(s::AbstractString, delim::Char)::Vector{String} + parts = String[] + depth = 0 + quote_char = nothing + start = firstindex(s) + for i in eachindex(s) + c = s[i] + if quote_char !== nothing + c == quote_char && (quote_char = nothing) + elseif c == '\'' || c == '"' + quote_char = c + elseif c in ('[', '(', '{') + depth += 1 + elseif c in (']', ')', '}') + depth -= 1 + elseif c == delim && depth == 0 + push!(parts, s[start:prevind(s, i)]) + start = nextind(s, i) + end + end + push!(parts, s[start:end]) + return parts +end + +"""Parse a single params value as a number, a list of numbers, or a string.""" +function _parse_param_value(val_str::AbstractString) + val_str = strip(val_str) + num = tryparse(Float64, val_str) + num !== nothing && return num + if startswith(val_str, "[") && endswith(val_str, "]") + isempty(strip(val_str[2:end-1])) && return Float64[] + try + return safe_parse_list(val_str) + catch + end + end + return strip(val_str, ['\'', '"']) +end + """ load_params!() @@ -615,21 +655,18 @@ function load_params!() if startswith(sparams, "{") # Python dict format: {'key': value, ...} - for m in eachmatch(r"['\"]([^'\"]+)['\"]\s*:\s*([^,}]+)", sparams) - key = m.captures[1] - val_str = strip(m.captures[2]) - val = tryparse(Float64, val_str) - params[key] = val !== nothing ? val : strip(val_str, ['\'', '"']) + inner = endswith(sparams, "}") ? sparams[2:end-1] : sparams[2:end] + for item in _split_top_level(inner, ',') + m = match(r"^\s*['\"]([^'\"]+)['\"]\s*:(.*)$"s, item) + m === nothing && continue + params[m.captures[1]] = _parse_param_value(m.captures[2]) end else # key=value;key2=value2 format for pair in split(sparams, ";") kv = split(strip(pair), "="; limit=2) length(kv) == 2 || continue - key = strip(kv[1]) - val_str = strip(kv[2]) - val = tryparse(Float64, val_str) - params[key] = val !== nothing ? val : val_str + params[strip(kv[1])] = _parse_param_value(kv[2]) end end end diff --git a/tests/julia/test_concoredocker.jl b/tests/julia/test_concoredocker.jl index fbb2724..730d100 100644 --- a/tests/julia/test_concoredocker.jl +++ b/tests/julia/test_concoredocker.jl @@ -56,6 +56,19 @@ const DockerConcore = DockerRuntime.Concore end end + @testset "list values in params" begin + mktempdir() do dir + reset_docker_state!() + DockerConcore.inpath = joinpath(dir, "in") + mkpath(DockerConcore.inpath * "1") + write(joinpath(DockerConcore.inpath * "1", "concore.params"), + "{'setpoint': [1.0, 2.0, 3.0], 'gain': 2.5}") + DockerConcore.load_params!() + @test DockerConcore.params["setpoint"] == [1.0, 2.0, 3.0] + @test DockerConcore.params["gain"] == 2.5 + end + end + @testset "aliases" begin mktempdir() do dir reset_docker_state!() diff --git a/tests/julia/test_config.jl b/tests/julia/test_config.jl index 0681908..5c24895 100644 --- a/tests/julia/test_config.jl +++ b/tests/julia/test_config.jl @@ -250,6 +250,38 @@ end end + @testset "list values in dict format" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.params"), + "{'setpoint': [1.0, 2.0, 3.0], 'gain': 2.5, 'name': 'a, b', 'empty': []}") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["setpoint"] == [1.0, 2.0, 3.0] + @test Concore.params["gain"] == 2.5 + @test Concore.params["name"] == "a, b" + @test Concore.params["empty"] == Float64[] + Concore.inpath = old_inpath + end + end + + @testset "list values in key=value format" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.params"), + "setpoint=[1.0,2.0];gain=2.5") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["setpoint"] == [1.0, 2.0] + @test Concore.params["gain"] == 2.5 + Concore.inpath = old_inpath + end + end + end # tryparam