diff --git a/tests/julia/runtests.jl b/tests/julia/runtests.jl new file mode 100644 index 0000000..4ec598d --- /dev/null +++ b/tests/julia/runtests.jl @@ -0,0 +1,11 @@ +using Test + +include(joinpath(@__DIR__, "..", "..", "concore.jl")) +using .Concore + +@testset "Concore.jl" begin + include("test_parser.jl") + include("test_config.jl") + include("test_sync.jl") + include("test_protocol.jl") +end diff --git a/tests/julia/test_config.jl b/tests/julia/test_config.jl new file mode 100644 index 0000000..0681908 --- /dev/null +++ b/tests/julia/test_config.jl @@ -0,0 +1,350 @@ +@testset "Configuration" begin + + # parse_port_file + @testset "parse_port_file" begin + + @testset "single entry" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "{'e1': 1}") + result = Concore.parse_port_file(path) + @test result == Dict("e1" => 1) + end + end + + @testset "multiple entries" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "{'e1': 1, 'e2': 2, 'e3': 3}") + result = Concore.parse_port_file(path) + @test result == Dict("e1" => 1, "e2" => 2, "e3" => 3) + end + end + + @testset "missing file returns empty dict" begin + result = Concore.parse_port_file("/nonexistent/path/file.port") + @test result == Dict{String,Int}() + @test isempty(result) + end + + @testset "empty file returns empty dict" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "") + result = Concore.parse_port_file(path) + @test result == Dict{String,Int}() + end + end + + @testset "whitespace-only file returns empty dict" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, " \n ") + result = Concore.parse_port_file(path) + @test result == Dict{String,Int}() + end + end + + @testset "single port with name containing underscore" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "{'my_edge': 5}") + result = Concore.parse_port_file(path) + @test result == Dict("my_edge" => 5) + end + end + + @testset "port with negative value" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "{'e1': -1}") + result = Concore.parse_port_file(path) + @test result == Dict("e1" => -1) + end + end + + @testset "content without braces matches nothing" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "random text here") + result = Concore.parse_port_file(path) + @test result == Dict{String,Int}() + end + end + + @testset "return type is Dict{String,Int}" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "{'e1': 1}") + result = Concore.parse_port_file(path) + @test result isa Dict{String,Int} + end + end + + @testset "port numbers with spaces" begin + mktempdir() do dir + path = joinpath(dir, "test.port") + write(path, "{'e1' : 1 , 'e2' : 2}") + result = Concore.parse_port_file(path) + @test result == Dict("e1" => 1, "e2" => 2) + end + end + + end + + # load_iport / load_oport + @testset "load_iport" begin + + @testset "loads from concore.iport file" begin + mktempdir() do dir + cd(dir) do + write("concore.iport", "{'sensor': 1, 'command': 2}") + Concore.iport = Dict{String,Int}() + result = Concore.load_iport() + @test result == Dict("sensor" => 1, "command" => 2) + @test Concore.iport == Dict("sensor" => 1, "command" => 2) + end + end + end + + @testset "returns empty dict when file missing" begin + mktempdir() do dir + cd(dir) do + Concore.iport = Dict{String,Int}() + result = Concore.load_iport() + @test isempty(result) + end + end + end + + end + + @testset "load_oport" begin + + @testset "loads from concore.oport file" begin + mktempdir() do dir + cd(dir) do + write("concore.oport", "{'output': 1}") + Concore.oport = Dict{String,Int}() + result = Concore.load_oport() + @test result == Dict("output" => 1) + @test Concore.oport == Dict("output" => 1) + end + end + end + + end + + # load_params + @testset "load_params" begin + + @testset "Python 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"), + "{'gain': 2.5, 'mode': 'pid'}") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["gain"] == 2.5 + @test Concore.params["mode"] == "pid" + Concore.inpath = old_inpath + end + end + + @testset "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"), + "gain=2.5;mode=pid") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["gain"] == 2.5 + @test Concore.params["mode"] == "pid" + Concore.inpath = old_inpath + end + end + + @testset "key=value with spaces" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.params"), + "gain = 3.0 ; mode = adaptive") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["gain"] == 3.0 + @test Concore.params["mode"] == "adaptive" + Concore.inpath = old_inpath + end + end + + @testset "Windows quoted params" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.params"), + "\"{'gain': 1.0}\"") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["gain"] == 1.0 + Concore.inpath = old_inpath + end + end + + @testset "missing params file does nothing" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + Concore.params = Dict{String,Any}("existing" => 1.0) + Concore.load_params() + @test Concore.params == Dict{String,Any}("existing" => 1.0) + Concore.inpath = old_inpath + end + end + + @testset "empty params file does nothing" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.params"), "") + Concore.params = Dict{String,Any}("existing" => 1.0) + Concore.load_params() + @test Concore.params == Dict{String,Any}("existing" => 1.0) + Concore.inpath = old_inpath + end + end + + @testset "numeric values are Float64" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.params"), + "{'x': 42}") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["x"] isa Float64 + @test Concore.params["x"] == 42.0 + Concore.inpath = old_inpath + end + end + + @testset "string values are strings" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.params"), + "{'mode': 'auto'}") + Concore.params = Dict{String,Any}() + Concore.load_params() + @test Concore.params["mode"] isa AbstractString + Concore.inpath = old_inpath + end + end + + end + + # tryparam + @testset "tryparam" begin + + @testset "returns value when key exists" begin + Concore.params = Dict{String,Any}("gain" => 2.5) + @test Concore.tryparam("gain", 0.0) == 2.5 + end + + @testset "returns default when key missing" begin + Concore.params = Dict{String,Any}() + @test Concore.tryparam("missing_key", 99.0) == 99.0 + end + + @testset "returns default of correct type" begin + Concore.params = Dict{String,Any}() + @test Concore.tryparam("x", "fallback") == "fallback" + end + + @testset "works with string values" begin + Concore.params = Dict{String,Any}("mode" => "pid") + @test Concore.tryparam("mode", "none") == "pid" + end + + @testset "does not modify params" begin + Concore.params = Dict{String,Any}("a" => 1.0) + Concore.tryparam("b", 2.0) + @test !haskey(Concore.params, "b") + end + + end + + # default_maxtime + @testset "default_maxtime" begin + + @testset "reads from file" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.maxtime"), "500") + result = Concore.default_maxtime(100) + @test result == 500 + @test Concore.maxtime == 500 + Concore.inpath = old_inpath + end + end + + @testset "uses default when file missing" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + result = Concore.default_maxtime(200) + @test result == 200 + @test Concore.maxtime == 200 + Concore.inpath = old_inpath + end + end + + @testset "uses default when file has bad content" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.maxtime"), "not_a_number") + result = Concore.default_maxtime(300) + @test result == 300 + @test Concore.maxtime == 300 + Concore.inpath = old_inpath + end + end + + @testset "handles whitespace in file" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "concore.maxtime"), " 750 \n") + result = Concore.default_maxtime(100) + @test result == 750 + Concore.inpath = old_inpath + end + end + + @testset "sets module global" begin + mktempdir() do dir + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + Concore.default_maxtime(42) + @test Concore.maxtime == 42 + Concore.inpath = old_inpath + end + end + + end + +end diff --git a/tests/julia/test_parser.jl b/tests/julia/test_parser.jl new file mode 100644 index 0000000..0769ff2 --- /dev/null +++ b/tests/julia/test_parser.jl @@ -0,0 +1,288 @@ +@testset "safe_parse_list" begin + + # Standard numeric formats + @testset "standard float list" begin + @test Concore.safe_parse_list("[1.0, 2.0, 3.0]") == [1.0, 2.0, 3.0] + end + + @testset "single element" begin + @test Concore.safe_parse_list("[42.0]") == [42.0] + end + + @testset "two elements" begin + @test Concore.safe_parse_list("[1.5, 2.5]") == [1.5, 2.5] + end + + @testset "many elements (10)" begin + result = Concore.safe_parse_list("[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]") + @test result == collect(0.0:9.0) + @test length(result) == 10 + end + + @testset "integer-only list" begin + @test Concore.safe_parse_list("[1, 2, 3]") == [1.0, 2.0, 3.0] + end + + @testset "mixed int and float" begin + @test Concore.safe_parse_list("[1, 2.5, 3]") == [1.0, 2.5, 3.0] + end + + @testset "zero values" begin + @test Concore.safe_parse_list("[0.0]") == [0.0] + @test Concore.safe_parse_list("[0]") == [0.0] + @test Concore.safe_parse_list("[0.0, 0.0, 0.0]") == [0.0, 0.0, 0.0] + end + + # Negative numbers + @testset "negative float" begin + @test Concore.safe_parse_list("[-1.5]") == [-1.5] + end + + @testset "negative integers" begin + @test Concore.safe_parse_list("[-1, -2, -3]") == [-1.0, -2.0, -3.0] + end + + @testset "mixed positive and negative" begin + @test Concore.safe_parse_list("[-1.5, 0.0, 2.5]") == [-1.5, 0.0, 2.5] + end + + @testset "very small negative" begin + @test Concore.safe_parse_list("[-0.001]") ≈ [-0.001] + end + + # Scientific notation / extreme values + @testset "scientific notation" begin + @test Concore.safe_parse_list("[1e-15, 1e15]") ≈ [1e-15, 1e15] + end + + @testset "small scientific notation" begin + @test Concore.safe_parse_list("[1.5e-10]") ≈ [1.5e-10] + end + + @testset "large scientific notation" begin + @test Concore.safe_parse_list("[3.14e8]") ≈ [3.14e8] + end + + @testset "negative scientific notation" begin + @test Concore.safe_parse_list("[-1.23e-4]") ≈ [-1.23e-4] + end + + @testset "very large number" begin + @test Concore.safe_parse_list("[1e300]") ≈ [1e300] + end + + @testset "very small number" begin + @test Concore.safe_parse_list("[1e-300]") ≈ [1e-300] + end + + # NumPy wrapper handling + @testset "np.float64 wrapper" begin + @test Concore.safe_parse_list("[np.float64(1.5)]") == [1.5] + end + + @testset "np.float32 wrapper" begin + @test Concore.safe_parse_list("[np.float32(2.0)]") == [2.0] + end + + @testset "numpy.int32 wrapper" begin + @test Concore.safe_parse_list("[numpy.int32(42)]") == [42.0] + end + + @testset "np.int64 wrapper" begin + @test Concore.safe_parse_list("[np.int64(7)]") == [7.0] + end + + @testset "multiple numpy wrappers" begin + @test Concore.safe_parse_list("[np.float64(1.0), np.float64(2.0)]") == [1.0, 2.0] + end + + @testset "numpy wrapper with negative" begin + @test Concore.safe_parse_list("[np.float64(-3.14)]") ≈ [-3.14] + end + + @testset "numpy wrapper with zero" begin + @test Concore.safe_parse_list("[np.float64(0.0)]") == [0.0] + end + + @testset "mixed numpy and plain" begin + @test Concore.safe_parse_list("[np.float64(1.5), 2.0, np.int32(3)]") == [1.5, 2.0, 3.0] + end + + # Python boolean and None handling + @testset "Python True" begin + @test Concore.safe_parse_list("[True]") == [1.0] + end + + @testset "Python False" begin + @test Concore.safe_parse_list("[False]") == [0.0] + end + + @testset "Python None" begin + @test Concore.safe_parse_list("[None]") == [0.0] + end + + @testset "multiple Python booleans" begin + @test Concore.safe_parse_list("[True, False, True]") == [1.0, 0.0, 1.0] + end + + @testset "mixed booleans and numbers" begin + @test Concore.safe_parse_list("[True, 2.5, False]") == [1.0, 2.5, 0.0] + end + + @testset "True False None together" begin + @test Concore.safe_parse_list("[True, False, None]") == [1.0, 0.0, 0.0] + end + + @testset "mixed numpy, booleans, and numbers" begin + @test Concore.safe_parse_list("[np.float64(1.5), True, 2.0, False]") == [1.5, 1.0, 2.0, 0.0] + end + + # Whitespace handling + @testset "leading whitespace" begin + @test Concore.safe_parse_list(" [1.0, 2.0]") == [1.0, 2.0] + end + + @testset "trailing whitespace" begin + @test Concore.safe_parse_list("[1.0, 2.0] ") == [1.0, 2.0] + end + + @testset "leading and trailing whitespace" begin + @test Concore.safe_parse_list(" [1.0, 2.0] ") == [1.0, 2.0] + end + + @testset "extra spaces around commas" begin + @test Concore.safe_parse_list("[1.0 , 2.0 , 3.0]") == [1.0, 2.0, 3.0] + end + + @testset "spaces inside brackets" begin + @test Concore.safe_parse_list("[ 1.0, 2.0 ]") == [1.0, 2.0] + end + + @testset "tab characters" begin + @test Concore.safe_parse_list("[\t1.0,\t2.0\t]") == [1.0, 2.0] + end + + @testset "newline in whitespace" begin + @test Concore.safe_parse_list("\n[1.0, 2.0]\n") == [1.0, 2.0] + end + + # Return types + @testset "always returns Vector{Float64}" begin + result = Concore.safe_parse_list("[1, 2, 3]") + @test result isa Vector{Float64} + end + + @testset "integer input returns Float64" begin + result = Concore.safe_parse_list("[42]") + @test eltype(result) == Float64 + end + + @testset "single element returns Vector" begin + result = Concore.safe_parse_list("[1.0]") + @test result isa Vector{Float64} + @test length(result) == 1 + end + + # Error cases + @testset "empty string throws" begin + @test_throws Exception Concore.safe_parse_list("") + end + + @testset "no brackets throws" begin + @test_throws Exception Concore.safe_parse_list("1.0, 2.0") + end + + @testset "missing closing bracket throws" begin + @test_throws Exception Concore.safe_parse_list("[1.0, 2.0") + end + + @testset "missing opening bracket throws" begin + @test_throws Exception Concore.safe_parse_list("1.0, 2.0]") + end + + @testset "empty brackets throws" begin + @test_throws Exception Concore.safe_parse_list("[]") + end + + @testset "non-numeric content throws" begin + @test_throws Exception Concore.safe_parse_list("[hello, world]") + end + + @testset "only whitespace throws" begin + @test_throws Exception Concore.safe_parse_list(" ") + end + + @testset "random text in list throws" begin + @test_throws Exception Concore.safe_parse_list("[1.0, abc, 3.0]") + end + + # Precision and edge cases + @testset "high precision value" begin + @test Concore.safe_parse_list("[3.141592653589793]") ≈ [π] atol=1e-15 + end + + @testset "many decimal places" begin + @test Concore.safe_parse_list("[1.123456789012345]") ≈ [1.123456789012345] + end + + @testset "positive zero" begin + @test Concore.safe_parse_list("[0.0]") == [0.0] + end + + @testset "negative zero" begin + result = Concore.safe_parse_list("[-0.0]") + @test result[1] == 0.0 # -0.0 == 0.0 in IEEE 754 + end + + @testset "Inf value" begin + @test Concore.safe_parse_list("[Inf]") == [Inf] + end + + @testset "negative Inf" begin + @test Concore.safe_parse_list("[-Inf]") == [-Inf] + end + + @testset "twenty elements" begin + vals = join(["$i.0" for i in 1:20], ", ") + result = Concore.safe_parse_list("[$vals]") + @test length(result) == 20 + @test result == collect(1.0:20.0) + end + + @testset "repeated values" begin + @test Concore.safe_parse_list("[1.0, 1.0, 1.0]") == [1.0, 1.0, 1.0] + end + + @testset "alternating signs" begin + @test Concore.safe_parse_list("[1.0, -1.0, 1.0, -1.0]") == [1.0, -1.0, 1.0, -1.0] + end + + # Realistic wire format strings + @testset "typical concore wire string (simtime + data)" begin + result = Concore.safe_parse_list("[5.0, 42.0, 3.14]") + @test result == [5.0, 42.0, 3.14] + end + + @testset "simtime=0 initial string" begin + result = Concore.safe_parse_list("[0.0, 0.0]") + @test result == [0.0, 0.0] + end + + @testset "large simtime" begin + result = Concore.safe_parse_list("[999.0, 1.0, 2.0, 3.0]") + @test result[1] == 999.0 + end + + @testset "Python str output format" begin + @test Concore.safe_parse_list("[1.0, 2.0, 3.0]") == [1.0, 2.0, 3.0] + end + + @testset "Python str with integers" begin + @test Concore.safe_parse_list("[0, 1, 2]") == [0.0, 1.0, 2.0] + end + + @testset "numpy array str format" begin + @test Concore.safe_parse_list("[np.float64(1.5), np.float64(2.5)]") == [1.5, 2.5] + end + +end diff --git a/tests/julia/test_protocol.jl b/tests/julia/test_protocol.jl new file mode 100644 index 0000000..2cb915b --- /dev/null +++ b/tests/julia/test_protocol.jl @@ -0,0 +1,508 @@ +@testset "Protocol Core" begin + + # Reset state helper + function reset_concore_state!() + Concore.simtime = 0.0 + Concore.delay = 0.0 # zero delay for fast tests + Concore.s = "" + Concore.olds = "" + Concore.retrycount = 0 + end + + # concore_write (Vector{Float64}) + @testset "concore_write basics" begin + + @testset "creates file with correct format" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 5.0 + + Concore.concore_write(1, "test_signal", [42.0, 3.14]) + + filepath = joinpath(Concore.outpath * "1", "test_signal") + @test isfile(filepath) + content = read(filepath, String) + @test content == "[5.0, 42.0, 3.14]" + Concore.outpath = old_outpath + end + end + + @testset "creates output directory" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + + Concore.concore_write(1, "signal", [1.0]) + + @test isdir(Concore.outpath * "1") + Concore.outpath = old_outpath + end + end + + @testset "integer-valued floats keep .0 suffix" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [42.0]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test content == "[0.0, 42.0]" + @test occursin("42.0", content) + @test !occursin("42,", content) + Concore.outpath = old_outpath + end + end + + @testset "delta=0 does not advance simtime" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 10.0 + + Concore.concore_write(1, "test", [1.0]; delta=0) + + @test Concore.simtime == 10.0 + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test startswith(content, "[10.0") + Concore.outpath = old_outpath + end + end + + @testset "delta=1 writes correct timestamp in wire" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 10.0 + + Concore.concore_write(1, "test", [1.0]; delta=1) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test startswith(content, "[11.0") + Concore.outpath = old_outpath + end + end + + @testset "empty value array writes only simtime" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 3.0 + + Concore.concore_write(1, "test", Float64[]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test content == "[3.0]" + Concore.outpath = old_outpath + end + end + + @testset "multiple values" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 1.0 + + Concore.concore_write(1, "test", [10.0, 20.0, 30.0, 40.0, 50.0]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test content == "[1.0, 10.0, 20.0, 30.0, 40.0, 50.0]" + Concore.outpath = old_outpath + end + end + + @testset "negative values" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [-1.5, -2.5]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test content == "[0.0, -1.5, -2.5]" + Concore.outpath = old_outpath + end + end + + @testset "non-integer floats format correctly" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [3.14159]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test occursin("3.14159", content) + Concore.outpath = old_outpath + end + end + + @testset "different port numbers" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [1.0]) + Concore.concore_write(2, "test", [2.0]) + Concore.concore_write(3, "test", [3.0]) + + @test isfile(joinpath(Concore.outpath * "1", "test")) + @test isfile(joinpath(Concore.outpath * "2", "test")) + @test isfile(joinpath(Concore.outpath * "3", "test")) + Concore.outpath = old_outpath + end + end + + @testset "overwrite existing file" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [1.0]) + Concore.concore_write(1, "test", [2.0]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test content == "[0.0, 2.0]" + Concore.outpath = old_outpath + end + end + + end + + # concore_write (String variant) + @testset "concore_write string variant" begin + + @testset "writes raw string" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + + Concore.concore_write(1, "raw_test", "[99.0, 1.0, 2.0]") + + content = read(joinpath(Concore.outpath * "1", "raw_test"), String) + @test content == "[99.0, 1.0, 2.0]" + Concore.outpath = old_outpath + end + end + + @testset "string write creates directory" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + + Concore.concore_write(1, "test", "hello") + + @test isdir(Concore.outpath * "1") + Concore.outpath = old_outpath + end + end + + @testset "string write preserves exact content" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + + raw = "[0.0, np.float64(1.5), True]" + Concore.concore_write(1, "test", raw) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test content == raw + Concore.outpath = old_outpath + end + end + + end + + # concore_read + @testset "concore_read" begin + + @testset "reads data file and extracts values after simtime" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "signal"), "[5.0, 42.0, 3.14]") + + result = Concore.concore_read(1, "signal", "[0.0, 0.0, 0.0]") + + @test result == [42.0, 3.14] + @test Concore.simtime == 5.0 + Concore.inpath = old_inpath + end + end + + @testset "falls back to initstr when file missing" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + + result = Concore.concore_read(1, "nosuchfile", "[0.0, 1.0, 2.0]") + + @test result == [1.0, 2.0] + @test Concore.simtime == 0.0 + Concore.inpath = old_inpath + end + end + + @testset "updates simtime from data" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "signal"), "[10.0, 1.0]") + + Concore.concore_read(1, "signal", "[0.0, 0.0]") + + @test Concore.simtime == 10.0 + Concore.inpath = old_inpath + end + end + + @testset "simtime uses max (doesn't go backwards)" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + + Concore.simtime = 20.0 + write(joinpath(Concore.inpath * "1", "signal"), "[5.0, 1.0]") + Concore.concore_read(1, "signal", "[0.0, 0.0]") + + @test Concore.simtime == 20.0 + Concore.inpath = old_inpath + end + end + + @testset "accumulates into s string for sync" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "signal"), "[0.0, 1.0]") + + @test Concore.s == "" + Concore.concore_read(1, "signal", "[0.0, 0.0]") + @test Concore.s != "" + Concore.inpath = old_inpath + end + end + + @testset "read from different ports" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + mkpath(Concore.inpath * "2") + write(joinpath(Concore.inpath * "1", "s1"), "[0.0, 10.0]") + write(joinpath(Concore.inpath * "2", "s2"), "[0.0, 20.0]") + + r1 = Concore.concore_read(1, "s1", "[0.0, 0.0]") + r2 = Concore.concore_read(2, "s2", "[0.0, 0.0]") + + @test r1 == [10.0] + @test r2 == [20.0] + Concore.inpath = old_inpath + end + end + + @testset "single value (just simtime + one value)" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "signal"), "[0.0, 99.0]") + + result = Concore.concore_read(1, "signal", "[0.0, 0.0]") + @test result == [99.0] + @test length(result) == 1 + Concore.inpath = old_inpath + end + end + + @testset "multiple values" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "signal"), "[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]") + + result = Concore.concore_read(1, "signal", "[0.0, 0.0]") + @test result == [1.0, 2.0, 3.0, 4.0, 5.0] + @test length(result) == 5 + Concore.inpath = old_inpath + end + end + + end + + # Write then Read round-trip + @testset "write-read round-trip" begin + + @testset "data survives round-trip" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "io") + Concore.inpath = joinpath(dir, "io") + Concore.simtime = 5.0 + + original = [42.0, 3.14, -1.5] + Concore.concore_write(1, "roundtrip", original) + + reset_concore_state!() + Concore.inpath = joinpath(dir, "io") + result = Concore.concore_read(1, "roundtrip", "[0.0, 0.0, 0.0, 0.0]") + + @test result ≈ original + @test Concore.simtime == 5.0 + + Concore.inpath = old_inpath + Concore.outpath = old_outpath + end + end + + @testset "integer values round-trip correctly" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "io") + Concore.inpath = joinpath(dir, "io") + Concore.simtime = 0.0 + + original = [1.0, 2.0, 3.0] + Concore.concore_write(1, "roundtrip", original) + + reset_concore_state!() + Concore.inpath = joinpath(dir, "io") + result = Concore.concore_read(1, "roundtrip", "[0.0, 0.0, 0.0, 0.0]") + + @test result == original + + Concore.inpath = old_inpath + Concore.outpath = old_outpath + end + end + + @testset "simtime preserved in round-trip" begin + mktempdir() do dir + reset_concore_state!() + old_inpath = Concore.inpath + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "io") + Concore.inpath = joinpath(dir, "io") + Concore.simtime = 99.0 + + Concore.concore_write(1, "roundtrip", [1.0]) + + reset_concore_state!() + Concore.inpath = joinpath(dir, "io") + Concore.concore_read(1, "roundtrip", "[0.0, 0.0]") + + @test Concore.simtime == 99.0 + + Concore.inpath = old_inpath + Concore.outpath = old_outpath + end + end + + end + + # Wire format verification + @testset "wire format" begin + + @testset "output starts with [ and ends with ]" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [1.0, 2.0]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test startswith(content, "[") + @test endswith(content, "]") + Concore.outpath = old_outpath + end + end + + @testset "values separated by comma-space" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [1.0, 2.0, 3.0]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test content == "[0.0, 1.0, 2.0, 3.0]" + @test occursin(", ", content) + Concore.outpath = old_outpath + end + end + + @testset "no trailing comma" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [1.0]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test !endswith(content, ",]") + @test !endswith(content, ", ]") + Concore.outpath = old_outpath + end + end + + @testset "no newline in output" begin + mktempdir() do dir + reset_concore_state!() + old_outpath = Concore.outpath + Concore.outpath = joinpath(dir, "out") + Concore.simtime = 0.0 + + Concore.concore_write(1, "test", [1.0, 2.0]) + + content = read(joinpath(Concore.outpath * "1", "test"), String) + @test !occursin('\n', content) + Concore.outpath = old_outpath + end + end + + end + +end diff --git a/tests/julia/test_sync.jl b/tests/julia/test_sync.jl new file mode 100644 index 0000000..bb11855 --- /dev/null +++ b/tests/julia/test_sync.jl @@ -0,0 +1,149 @@ +@testset "Synchronization" begin + + function reset_sync_state!() + Concore.simtime = 0.0 + Concore.delay = 0.0 + Concore.s = "" + Concore.olds = "" + Concore.retrycount = 0 + end + + # initval + @testset "initval" begin + + @testset "sets simtime from first element" begin + reset_sync_state!() + Concore.initval("[5.0, 1.0, 2.0]") + @test Concore.simtime == 5.0 + end + + @testset "returns data portion without simtime" begin + reset_sync_state!() + result = Concore.initval("[5.0, 1.0, 2.0]") + @test result == [1.0, 2.0] + end + + @testset "single value after simtime" begin + reset_sync_state!() + result = Concore.initval("[0.0, 42.0]") + @test result == [42.0] + @test Concore.simtime == 0.0 + end + + @testset "simtime=0" begin + reset_sync_state!() + result = Concore.initval("[0.0, 0.0]") + @test result == [0.0] + @test Concore.simtime == 0.0 + end + + @testset "large simtime" begin + reset_sync_state!() + result = Concore.initval("[999.0, 1.0]") + @test Concore.simtime == 999.0 + @test result == [1.0] + end + + @testset "multiple data values" begin + reset_sync_state!() + result = Concore.initval("[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]") + @test result == [1.0, 2.0, 3.0, 4.0, 5.0] + end + + @testset "return type is Vector{Float64}" begin + reset_sync_state!() + result = Concore.initval("[0.0, 1.0]") + @test result isa Vector{Float64} + end + + @testset "overwrites previous simtime" begin + reset_sync_state!() + Concore.simtime = 100.0 + Concore.initval("[5.0, 1.0]") + @test Concore.simtime == 5.0 + end + + @testset "handles negative values" begin + reset_sync_state!() + result = Concore.initval("[0.0, -1.5, -2.5]") + @test result == [-1.5, -2.5] + end + + end + + # unchanged + @testset "unchanged" begin + + @testset "returns true when no reads happened (s == olds == empty)" begin + reset_sync_state!() + @test Concore.unchanged() == true + end + + @testset "returns false after s is modified (simulating read)" begin + reset_sync_state!() + Concore.s = "some data" + @test Concore.unchanged() == false + end + + @testset "returns true on second call without new data" begin + reset_sync_state!() + Concore.s = "some data" + Concore.unchanged() # first call: detects change, returns false + @test Concore.unchanged() == true # second call: no new data + end + + @testset "clears s when returning true" begin + reset_sync_state!() + Concore.unchanged() + @test Concore.s == "" + end + + @testset "updates olds when returning false" begin + reset_sync_state!() + Concore.s = "new data" + Concore.unchanged() + @test Concore.olds == "new data" + end + + @testset "detects new data after reset" begin + reset_sync_state!() + Concore.s = "first read" + @test Concore.unchanged() == false + @test Concore.unchanged() == true + Concore.s = "second read" + @test Concore.unchanged() == false + end + + @testset "accumulation pattern (read appends to s)" begin + reset_sync_state!() + Concore.s *= "[0.0, 1.0]" + @test Concore.unchanged() == false + Concore.s *= "[1.0, 2.0]" + @test Concore.unchanged() == false + end + + end + + # Sync pattern integration + @testset "sync pattern with read" begin + + @testset "unchanged detects concore_read activity" begin + mktempdir() do dir + reset_sync_state!() + old_inpath = Concore.inpath + Concore.inpath = joinpath(dir, "in") + mkpath(Concore.inpath * "1") + write(joinpath(Concore.inpath * "1", "signal"), "[0.0, 1.0]") + + @test Concore.unchanged() == true + Concore.concore_read(1, "signal", "[0.0, 0.0]") + @test Concore.unchanged() == false + @test Concore.unchanged() == true + + Concore.inpath = old_inpath + end + end + + end + +end