diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cac3a21cc..44200a6051 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: 'JuliaC' + 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(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: | + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() - uses: julia-actions/julia-runtest@v1 with: depwarn: ${{ matrix.machine.depwarn }} diff --git a/Project.toml b/Project.toml index 0c3312ad97..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" @@ -49,7 +50,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/MyApp/Project.toml b/test/JuliaC/MyApp/Project.toml new file mode 100644 index 0000000000..e53523ee74 --- /dev/null +++ b/test/JuliaC/MyApp/Project.toml @@ -0,0 +1,8 @@ +name = "MyApp" +uuid = "4768c16f-14ab-4f22-afac-abe0e8586942" + +[deps] +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" + +[compat] +MathOptInterface = "1" diff --git a/test/JuliaC/MyApp/src/MyApp.jl b/test/JuliaC/MyApp/src/MyApp.jl new file mode 100644 index 0000000000..3d2c400a94 --- /dev/null +++ b/test/JuliaC/MyApp/src/MyApp.jl @@ -0,0 +1,44 @@ +# 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. + +# !!! 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 + 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") + 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(io -> write(io, model), filename, "w") + return 0 +end + +end # module MOFWriter diff --git a/test/JuliaC/test_juliac.jl b/test/JuliaC/test_juliac.jl new file mode 100644 index 0000000000..5130bb520d --- /dev/null +++ b/test/JuliaC/test_juliac.jl @@ -0,0 +1,101 @@ +# 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. + +# !!! 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 + +import JSON +import JuliaC + +function compile() + output_dir = mktempdir() + outname = joinpath(output_dir, "MyApp") + image_recipe = JuliaC.ImageRecipe( + output_type = "--output-exe", + file = joinpath(@__DIR__, "MyApp"), + trim_mode = "no", + add_ccallables = false, + verbose = true, + ) + 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 + +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, + ), + ) + 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], + ), + ], + ), + "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 c72cf35162..4a8d8aaa31 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,7 +18,11 @@ # * Nonlinear # * Test # * Utilities -# +# * 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.