From be5f336165a880bf375ef0210e3b6699e5773654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 30 Sep 2026 15:55:23 +0200 Subject: [PATCH 1/5] Add JuliaC CI coverage for the MOF writer --- .github/workflows/ci.yml | 25 +++++++++ test/JuliaC/MOFWriter/Project.toml | 8 +++ test/JuliaC/MOFWriter/src/MOFWriter.jl | 32 +++++++++++ test/JuliaC/Project.toml | 12 +++++ test/JuliaC/test_juliac.jl | 73 ++++++++++++++++++++++++++ test/runtests.jl | 1 + 6 files changed, 151 insertions(+) create mode 100644 test/JuliaC/MOFWriter/Project.toml create mode 100644 test/JuliaC/MOFWriter/src/MOFWriter.jl create mode 100644 test/JuliaC/Project.toml create mode 100644 test/JuliaC/test_juliac.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cac3a21cc..d5566d9eaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,3 +74,28 @@ jobs: with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} + JuliaC: + name: JuliaC - ${{ github.event_name }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v3 + with: + version: '1' + arch: x64 + - uses: julia-actions/cache@v3 + - uses: julia-actions/julia-buildpkg@v1 + - shell: julia --color=yes --project=test/JuliaC/MOFWriter {0} + run: | + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() + - shell: julia --color=yes --project=test/JuliaC {0} + run: | + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() + - run: julia --color=yes --project=JuliaC runtests.jl + working-directory: test + env: + MOI_TEST_MODULES: JuliaC diff --git a/test/JuliaC/MOFWriter/Project.toml b/test/JuliaC/MOFWriter/Project.toml new file mode 100644 index 0000000000..fb6faff9ef --- /dev/null +++ b/test/JuliaC/MOFWriter/Project.toml @@ -0,0 +1,8 @@ +name = "MOFWriter" +uuid = "4768c16f-14ab-4f22-afac-abe0e8586942" + +[deps] +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" + +[compat] +MathOptInterface = "1" diff --git a/test/JuliaC/MOFWriter/src/MOFWriter.jl b/test/JuliaC/MOFWriter/src/MOFWriter.jl new file mode 100644 index 0000000000..54a71df5b2 --- /dev/null +++ b/test/JuliaC/MOFWriter/src/MOFWriter.jl @@ -0,0 +1,32 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module MOFWriter + +import MathOptInterface as MOI + +function @main(args::Vector{String})::Cint + @assert length(args) == 1 + model = MOI.FileFormats.MOF.Model() + x = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + MOI.add_constraint(model, x, MOI.GreaterThan(0.0)) + f = MOI.ScalarNonlinearFunction( + :+, + Any[MOI.ScalarNonlinearFunction(:sin, Any[x]), 2, 3.0], + ) + c = MOI.add_constraint(model, f, MOI.LessThan(7.0)) + MOI.set(model, MOI.ConstraintName(), c, "c") + objective = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{typeof(objective)}(), objective) + open(only(args), "w") do io + return write(io, model) + end + return 0 +end + +end # module MOFWriter diff --git a/test/JuliaC/Project.toml b/test/JuliaC/Project.toml new file mode 100644 index 0000000000..00927abf26 --- /dev/null +++ b/test/JuliaC/Project.toml @@ -0,0 +1,12 @@ +[deps] +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +JSON = "1" +JuliaC = "0.3" +MathOptInterface = "1" +ParallelTestRunner = "2.4.1" diff --git a/test/JuliaC/test_juliac.jl b/test/JuliaC/test_juliac.jl new file mode 100644 index 0000000000..b857a54cba --- /dev/null +++ b/test/JuliaC/test_juliac.jl @@ -0,0 +1,73 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestJuliaC + +using Test + +import JSON +import JuliaC + +function compile(output_dir) + image_recipe = JuliaC.ImageRecipe( + output_type = "--output-exe", + file = joinpath(@__DIR__, "MOFWriter"), + trim_mode = "no", + add_ccallables = false, + verbose = true, + ) + link_recipe = JuliaC.LinkRecipe(; + image_recipe, + outname = joinpath(output_dir, "MOFWriter"), + ) + bundle_recipe = JuliaC.BundleRecipe(; link_recipe, output_dir) + JuliaC.compile_products(image_recipe) + JuliaC.link_products(link_recipe) + JuliaC.bundle_products(bundle_recipe) + return +end + +@testset "JuliaC MOF writer" begin + mktempdir() do output_dir + compile(output_dir) + app = joinpath(output_dir, "bin", "MOFWriter") + output = joinpath(output_dir, "compiled.mof.json") + run(`$app $output`) + object = JSON.parsefile(output) + @test object["variables"] == [Dict("name" => "x")] + @test object["objective"]["sense"] == "min" + @test object["objective"]["function"] == Dict( + "type" => "ScalarAffineFunction", + "terms" => [Dict("coefficient" => 1.0, "variable" => "x")], + "constant" => 0.0, + ) + @test object["has_scalar_nonlinear"] + @test length(object["constraints"]) == 2 + c = only(filter(c -> get(c, "name", "") == "c", object["constraints"])) + @test c["set"] == Dict("type" => "LessThan", "upper" => 7.0) + @test c["function"] == Dict( + "type" => "ScalarNonlinearFunction", + "root" => Dict("type" => "node", "index" => 2), + "node_list" => [ + Dict("type" => "sin", "args" => ["x"]), + Dict( + "type" => "+", + "args" => + [Dict("type" => "node", "index" => 1), 2, 3.0], + ), + ], + ) + bound = only(filter(c -> !haskey(c, "name"), object["constraints"])) + @test bound["function"] == Dict("type" => "Variable", "name" => "x") + @test bound["set"] == Dict("type" => "GreaterThan", "lower" => 0.0) + reference = joinpath(output_dir, "reference.mof.json") + project = joinpath(@__DIR__, "MOFWriter") + run(`$(Base.julia_cmd()) --project=$project -m MOFWriter $reference`) + @test read(output, String) == read(reference, String) + end +end + +end # module TestJuliaC diff --git a/test/runtests.jl b/test/runtests.jl index c72cf35162..769db154be 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,6 +18,7 @@ # * Nonlinear # * Test # * Utilities +# * JuliaC (opt-in; requires the test/JuliaC environment) # # If present, the tests run only those submodules defined above. `General` is # not a submodule, but it runs all of the top-level tests in MOI. From 9d6c4bec89782d3b83e7a3b9c8a3b3a80cfe7aff Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 1 Oct 2026 10:36:15 +1300 Subject: [PATCH 2/5] Refactor into a generic test module --- .github/workflows/ci.yml | 40 +++----- Project.toml | 3 +- test/JuliaC/{MOFWriter => MyApp}/Project.toml | 2 +- .../src/MOFWriter.jl => MyApp/src/MyApp.jl} | 22 ++++- test/JuliaC/Project.toml | 12 --- test/JuliaC/test_juliac.jl | 94 ++++++++++++------- test/runtests.jl | 7 +- 7 files changed, 101 insertions(+), 79 deletions(-) rename test/JuliaC/{MOFWriter => MyApp}/Project.toml (88%) rename test/JuliaC/{MOFWriter/src/MOFWriter.jl => MyApp/src/MyApp.jl} (65%) delete mode 100644 test/JuliaC/Project.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5566d9eaf..e34543447c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,14 @@ jobs: arch: x86 downgrade: false depwarn: error + include: + - moi_test_modules: 'JuliaCI' + machine: + version: '1' + os: ubuntu-latest + arch: default + downgrade: false + depwarn: yes steps: - uses: actions/checkout@v7 - uses: julia-actions/setup-julia@v3 @@ -62,6 +70,13 @@ jobs: with: mode: deps - uses: julia-actions/julia-buildpkg@v1 + - if: ${{ contains(moi_test_modules, 'JuliaC') }} + # Instantiate MyApp/Project.toml iff we are running the JuliaC tests + shell: julia --color=yes --project=test/JuliaC/MyApp {0} + run: | + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() - uses: julia-actions/julia-runtest@v1 with: depwarn: ${{ matrix.machine.depwarn }} @@ -74,28 +89,3 @@ jobs: with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} - JuliaC: - name: JuliaC - ${{ github.event_name }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v3 - with: - version: '1' - arch: x64 - - uses: julia-actions/cache@v3 - - uses: julia-actions/julia-buildpkg@v1 - - shell: julia --color=yes --project=test/JuliaC/MOFWriter {0} - run: | - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate() - - shell: julia --color=yes --project=test/JuliaC {0} - run: | - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate() - - run: julia --color=yes --project=JuliaC runtests.jl - working-directory: test - env: - MOI_TEST_MODULES: JuliaC diff --git a/Project.toml b/Project.toml index 0c3312ad97..224b7bc30c 100644 --- a/Project.toml +++ b/Project.toml @@ -49,7 +49,8 @@ julia = "1.10" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8" JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2" ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" [targets] -test = ["BenchmarkTools", "CliqueTrees", "JSONSchema", "ParallelTestRunner"] +test = ["BenchmarkTools", "CliqueTrees", "JSONSchema", "JuliaC", "ParallelTestRunner"] diff --git a/test/JuliaC/MOFWriter/Project.toml b/test/JuliaC/MyApp/Project.toml similarity index 88% rename from test/JuliaC/MOFWriter/Project.toml rename to test/JuliaC/MyApp/Project.toml index fb6faff9ef..e53523ee74 100644 --- a/test/JuliaC/MOFWriter/Project.toml +++ b/test/JuliaC/MyApp/Project.toml @@ -1,4 +1,4 @@ -name = "MOFWriter" +name = "MyApp" uuid = "4768c16f-14ab-4f22-afac-abe0e8586942" [deps] diff --git a/test/JuliaC/MOFWriter/src/MOFWriter.jl b/test/JuliaC/MyApp/src/MyApp.jl similarity index 65% rename from test/JuliaC/MOFWriter/src/MOFWriter.jl rename to test/JuliaC/MyApp/src/MyApp.jl index 54a71df5b2..3d2c400a94 100644 --- a/test/JuliaC/MOFWriter/src/MOFWriter.jl +++ b/test/JuliaC/MyApp/src/MyApp.jl @@ -4,12 +4,26 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. -module MOFWriter +# !!! info +# There is a one-to-one correspondence between the `run_` functions in this +# module and the `test_` functions in test_juliac.jl. If you add to one, you +# must also add to the other. + +module MyApp import MathOptInterface as MOI function @main(args::Vector{String})::Cint - @assert length(args) == 1 + @assert length(args) >= 1 + head = popfirst!(args) + if head == "--mathoptformat" + return run_mathoptformat(args) + end + return 1 # Incorrect head +end + +function run_mathoptformat(args::Vector{String})::Cint + filename = only(args) model = MOI.FileFormats.MOF.Model() x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") @@ -23,9 +37,7 @@ function @main(args::Vector{String})::Cint objective = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) MOI.set(model, MOI.ObjectiveFunction{typeof(objective)}(), objective) - open(only(args), "w") do io - return write(io, model) - end + open(io -> write(io, model), filename, "w") return 0 end diff --git a/test/JuliaC/Project.toml b/test/JuliaC/Project.toml deleted file mode 100644 index 00927abf26..0000000000 --- a/test/JuliaC/Project.toml +++ /dev/null @@ -1,12 +0,0 @@ -[deps] -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2" -MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[compat] -JSON = "1" -JuliaC = "0.3" -MathOptInterface = "1" -ParallelTestRunner = "2.4.1" diff --git a/test/JuliaC/test_juliac.jl b/test/JuliaC/test_juliac.jl index b857a54cba..5130bb520d 100644 --- a/test/JuliaC/test_juliac.jl +++ b/test/JuliaC/test_juliac.jl @@ -4,6 +4,19 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. +# !!! info +# There is a one-to-one correspondence between the `test_` functions in this +# module and the `run_` functions in MyApp.jl. If you add to one, you must +# also add to the other. + +# !!! info +# To run this file, you must first instantiate the MyApp Project.toml so +# that it includes MathOptInterface from the source code. From the root, do: +# ```julia +# using Pkg +# Pkg.develop(PackageSpec(path=pwd())) +# Pkg.instantiate() +# ``` module TestJuliaC using Test @@ -11,63 +24,78 @@ using Test import JSON import JuliaC -function compile(output_dir) +function compile() + output_dir = mktempdir() + outname = joinpath(output_dir, "MyApp") image_recipe = JuliaC.ImageRecipe( output_type = "--output-exe", - file = joinpath(@__DIR__, "MOFWriter"), + file = joinpath(@__DIR__, "MyApp"), trim_mode = "no", add_ccallables = false, verbose = true, ) - link_recipe = JuliaC.LinkRecipe(; - image_recipe, - outname = joinpath(output_dir, "MOFWriter"), - ) + link_recipe = JuliaC.LinkRecipe(; image_recipe, outname) bundle_recipe = JuliaC.BundleRecipe(; link_recipe, output_dir) JuliaC.compile_products(image_recipe) JuliaC.link_products(link_recipe) JuliaC.bundle_products(bundle_recipe) + return output_dir +end + +function runtests() + output_dir = compile() + is_test(name) = startswith("$name", "test_") + @testset "$name" for name in filter(is_test, names(@__MODULE__; all = true)) + getfield(@__MODULE__, name)(output_dir) + end return end -@testset "JuliaC MOF writer" begin - mktempdir() do output_dir - compile(output_dir) - app = joinpath(output_dir, "bin", "MOFWriter") - output = joinpath(output_dir, "compiled.mof.json") - run(`$app $output`) - object = JSON.parsefile(output) - @test object["variables"] == [Dict("name" => "x")] - @test object["objective"]["sense"] == "min" - @test object["objective"]["function"] == Dict( +function test_mathoptformat(output_dir) + # Run via the compiled binary + output = joinpath(output_dir, "compiled.mof.json") + run(`$(output_dir)/bin/MyApp --mathoptformat $output`) + # Compare against running as `-m` + reference = joinpath(output_dir, "reference.mof.json") + project = joinpath(@__DIR__, "MyApp") + run( + `$(Base.julia_cmd()) --project=$project -m MyApp --mathoptformat $reference`, + ) + @test read(output, String) == read(reference, String) + # Manually test the output + object = JSON.parsefile(output) + @test object["variables"] == [Dict("name" => "x")] + @test object["objective"] == Dict( + "sense" => "min", + "function" => Dict( "type" => "ScalarAffineFunction", "terms" => [Dict("coefficient" => 1.0, "variable" => "x")], "constant" => 0.0, - ) - @test object["has_scalar_nonlinear"] - @test length(object["constraints"]) == 2 - c = only(filter(c -> get(c, "name", "") == "c", object["constraints"])) - @test c["set"] == Dict("type" => "LessThan", "upper" => 7.0) - @test c["function"] == Dict( + ), + ) + con_c = Dict( + "name" => "c", + "function" => Dict( "type" => "ScalarNonlinearFunction", "root" => Dict("type" => "node", "index" => 2), "node_list" => [ Dict("type" => "sin", "args" => ["x"]), Dict( "type" => "+", - "args" => - [Dict("type" => "node", "index" => 1), 2, 3.0], + "args" => [Dict("type" => "node", "index" => 1), 2, 3.0], ), ], - ) - bound = only(filter(c -> !haskey(c, "name"), object["constraints"])) - @test bound["function"] == Dict("type" => "Variable", "name" => "x") - @test bound["set"] == Dict("type" => "GreaterThan", "lower" => 0.0) - reference = joinpath(output_dir, "reference.mof.json") - project = joinpath(@__DIR__, "MOFWriter") - run(`$(Base.julia_cmd()) --project=$project -m MOFWriter $reference`) - @test read(output, String) == read(reference, String) - end + ), + "set" => Dict("type" => "LessThan", "upper" => 7.0), + ) + con_bound = Dict( + "function" => Dict("type" => "Variable", "name" => "x"), + "set" => Dict("type" => "GreaterThan", "lower" => 0.0), + ) + @test object["constraints"] == [con_c, con_bound] + return end end # module TestJuliaC + +TestJuliaC.runtests() diff --git a/test/runtests.jl b/test/runtests.jl index 769db154be..4a8d8aaa31 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,8 +18,11 @@ # * Nonlinear # * Test # * Utilities -# * JuliaC (opt-in; requires the test/JuliaC environment) -# +# * JuliaC [opt-in] + +# JuliaC is off by default because it requires instantiating the +# `test/JuliaC/MyApp` environment. We explicitly do this in CI. + # If present, the tests run only those submodules defined above. `General` is # not a submodule, but it runs all of the top-level tests in MOI. From 7c8fbd9b93b14d3b7d0c3d3e740be86379a7c748 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 1 Oct 2026 10:37:57 +1300 Subject: [PATCH 3/5] Update --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e34543447c..cebb60fc93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: with: mode: deps - uses: julia-actions/julia-buildpkg@v1 - - if: ${{ contains(moi_test_modules, 'JuliaC') }} + - if: ${{ contains(matrix.moi_test_modules, 'JuliaC') }} # Instantiate MyApp/Project.toml iff we are running the JuliaC tests shell: julia --color=yes --project=test/JuliaC/MyApp {0} run: | From a8fa42fe76410ec563040db89711b92074a1ad7b Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 1 Oct 2026 10:42:19 +1300 Subject: [PATCH 4/5] Update --- .github/workflows/ci.yml | 16 ++++++++-------- Project.toml | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cebb60fc93..0e0d7a39e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,14 +50,14 @@ jobs: arch: x86 downgrade: false depwarn: error - include: - - moi_test_modules: 'JuliaCI' - machine: - version: '1' - os: ubuntu-latest - arch: default - downgrade: false - depwarn: yes + include: + - moi_test_modules: 'JuliaCI' + machine: + version: '1' + os: ubuntu-latest + arch: default + downgrade: false + depwarn: yes steps: - uses: actions/checkout@v7 - uses: julia-actions/setup-julia@v3 diff --git a/Project.toml b/Project.toml index 224b7bc30c..ac81ff2b40 100644 --- a/Project.toml +++ b/Project.toml @@ -33,6 +33,7 @@ CodecZlib = "0.6, 0.7" ForwardDiff = "1" JSON = "0.21, 1" JSONSchema = "1" +JuliaC = "0.3" LinearAlgebra = "1" MutableArithmetics = "1" NaNMath = "0.3, 1" From 3d6167843e3d60fe75c18508882f642aaf23ee03 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 1 Oct 2026 10:54:21 +1300 Subject: [PATCH 5/5] Update --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e0d7a39e4..44200a6051 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: downgrade: false depwarn: error include: - - moi_test_modules: 'JuliaCI' + - moi_test_modules: 'JuliaC' machine: version: '1' os: ubuntu-latest