diff --git a/.circleci/config.yml b/.circleci/config.yml index c48679e1..2a60366d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,52 @@ version: 2.1 # See https://circleci.com/docs/reusing-config/ commands: + # Build SDPB locally, using homebrew package manager (used for MacOS) + # NB: this step takes much longer than Docker since one has to + # build Elemental and other dependencies! + native-brew-build-test: + description: Build and test SDPB natively, installing dependencies via homebrew + steps: + - checkout + - run: + name: Install dependencies via Homebrew + command: | + export HOMEBREW_NO_INSTALL_CLEANUP=1 + export HOMEBREW_NO_ANALYTICS=1 + brew install gmp mpfr boost rapidjson libarchive openblas flint cmake open-mpi metis automake libtool pkgconf bison + - run: + name: Install Elemental + command: | + git clone https://gitlab.com/bootstrapcollaboration/elemental.git + cd elemental + mkdir build + cd build + cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/install -DCMAKE_CXX_COMPILER=mpicxx -DCMAKE_C_COMPILER=mpicc + make && make install + cd ../.. + - run: + name: Install MPSolve + command: | + git clone https://github.com/robol/MPSolve.git + cd MPSolve + ./autogen.sh + CC=mpicc CXX=mpicxx LDFLAGS="-L$(brew --prefix gmp)/lib" CPPFLAGS="-I$(brew --prefix gmp)/include" YACC=$(brew --prefix bison)/bin/yacc ./configure --prefix=$HOME/install --disable-dependency-tracking --disable-examples --disable-ui --disable-graphical-debugger --disable-documentation + make && make install + cd .. + - run: + name: Build SDPB + command: | + CXXFLAGS="${CXXFLAGS} -D_GNU_SOURCE=1" ./waf configure --elemental-dir=$HOME/install --mpsolve-dir=$HOME/install --boost-dir=$(brew --prefix boost) --gmpxx-dir=$(brew --prefix gmp) --mpfr-dir=$(brew --prefix mpfr) --rapidjson-dir=$(brew --prefix rapidjson) --libarchive-dir=$(brew --prefix libarchive) --flint-dir=$(brew --prefix flint) --cblas-dir=$(brew --prefix openblas) --prefix=$HOME/install/sdpb/master + ./waf + ./build/sdpb --help + - run: + name: Run tests + command: ./test/run_all_tests.sh + - run: + name: Install + command: ./waf install + + docker-build-test: description: Build and test Docker image for platform parameters: @@ -144,15 +190,12 @@ jobs: platform: linux/arm64 tag: arm64 - # TODO MacOS M1 will be available on free plan after 24 Jun 2024 - # build-macos-m1: - # macos: - # xcode: 15.3.0 - # resource_class: macos.m1.medium.gen1 - # steps: - # - docker-build-test: - # platform: linux/arm64 - # tag: arm64 + build-test-macos: + macos: + xcode: 26.1.1 + resource_class: m4pro.medium + steps: + - native-brew-build-test # Test deploy process for local registry # TODO:docker manifest push works for DockerHub, but fails for local registry. @@ -200,6 +243,11 @@ workflows: tags: only: /^\d+\.\d+\.\d+$/ + - build-test-macos: + filters: + tags: + only: /^\d+\.\d+\.\d+$/ + - deploy-master: filters: branches: diff --git a/docs/site_installs/Apple_MacBook.md b/docs/site_installs/Apple_MacBook.md index a02ee91f..9bb13ee4 100644 --- a/docs/site_installs/Apple_MacBook.md +++ b/docs/site_installs/Apple_MacBook.md @@ -13,7 +13,7 @@ to path following instructions shown after installing. Then you can install packages required for SDPB: - brew install gmp mpfr boost rapidjson libarchive openblas flint cmake open-mpi + brew install gmp mpfr boost rapidjson libarchive openblas flint cmake open-mpi metis automake libtool pkgconf bison You can see installation directory and another information for a package (e.g. `boost`) by calling `brew info ` (e.g. `brew info boost`). @@ -42,7 +42,7 @@ On other laptops, Python 2 fails instead, so that one has to call `python3 ./waf git clone https://github.com/robol/MPSolve.git cd MPSolve ./autogen.sh - CC=mpicc CXX=mpicxx ./configure --prefix=$HOME/install --disable-dependency-tracking --disable-examples --disable-ui --disable-graphical-debugger --disable-documentation + CC=mpicc CXX=mpicxx LDFLAGS="-L$(brew --prefix gmp)/lib" CPPFLAGS="-I$(brew --prefix gmp)/include" YACC=$(brew --prefix bison)/bin/yacc ./configure --prefix=$HOME/install --disable-dependency-tracking --disable-examples --disable-ui --disable-graphical-debugger --disable-documentation make && make install cd .. @@ -54,7 +54,7 @@ On other laptops, Python 2 fails instead, so that one has to call `python3 ./waf ### Configure - CXXFLAGS="${CXXFLAGS} -D_GNU_SOURCE=1" ./waf configure --elemental-dir=$HOME/install --mpsolve-dir=$HOME/install --boost-dir=/opt/homebrew/Cellar/boost/1.84.0_1 --gmpxx-dir=/opt/homebrew/Cellar/gmp/6.3.0 --mpfr-dir=/opt/homebrew/Cellar/mpfr/4.2.1 --rapidjson-dir=/opt/homebrew/Cellar/rapidjson/1.1.0 --libarchive-dir=/opt/homebrew/Cellar/libarchive/3.7.3 --flint-dir=/opt/homebrew/Cellar/flint/3.1.0 --cblas-dir=/opt/homebrew/Cellar/openblas/0.3.27 --prefix=$HOME/install/sdpb-master + CXXFLAGS="${CXXFLAGS} -D_GNU_SOURCE=1" ./waf configure --elemental-dir=$HOME/install --mpsolve-dir=$HOME/install --boost-dir=$(brew --prefix boost) --gmpxx-dir=$(brew --prefix gmp) --mpfr-dir=$(brew --prefix mpfr) --rapidjson-dir=$(brew --prefix rapidjson) --libarchive-dir=$(brew --prefix libarchive) --flint-dir=$(brew --prefix flint) --cblas-dir=$(brew --prefix openblas) --prefix=$HOME/install/sdpb/master If waf fails to find some package, e.g. `boost`, check the installation directory by calling, e.g. `brew info boost` and update `--boost-dir` argument above. @@ -63,11 +63,11 @@ The above `./waf configure` command works or x86 processors (e.g. Intel i5), but M2) with linker warnings `found architecture 'arm64', required architecture 'x86_64` in `build/config.log`. In that case, you should set `-arch arm64` flag explicitly: - CXXFLAGS="${CXXFLAGS} -D_GNU_SOURCE=1 -arch arm64" LDFLAGS="${LDFLAGS} -arch arm64" ./waf configure --elemental-dir=$HOME/install --mpsolve-dir=$HOME/install --boost-dir=/opt/homebrew/Cellar/boost/1.84.0_1 --gmpxx-dir=/opt/homebrew/Cellar/gmp/6.3.0 --mpfr-dir=/opt/homebrew/Cellar/mpfr/4.2.1 --rapidjson-dir=/opt/homebrew/Cellar/rapidjson/1.1.0 --libarchive-dir=/opt/homebrew/Cellar/libarchive/3.7.3 --flint-dir=/opt/homebrew/Cellar/flint/3.1.0 --cblas-dir=/opt/homebrew/Cellar/openblas/0.3.27 --prefix=$HOME/install/sdpb-master + CXXFLAGS="${CXXFLAGS} -D_GNU_SOURCE=1 -arch arm64" LDFLAGS="${LDFLAGS} -arch arm64" ./waf configure --elemental-dir=$HOME/install --mpsolve-dir=$HOME/install --boost-dir=$(brew --prefix boost) --gmpxx-dir=$(brew --prefix gmp) --mpfr-dir=$(brew --prefix mpfr) --rapidjson-dir=$(brew --prefix rapidjson) --libarchive-dir=$(brew --prefix libarchive) --flint-dir=$(brew --prefix flint) --cblas-dir=$(brew --prefix openblas) --prefix=$HOME/install/sdpb/master ### Compile and install ./waf # build binaries ./build/sdpb --help # run SDPB binary ./test/run_all_tests.sh # run tests to check correctness - ./waf install # install sdpb to --prefix=$HOME/install/sdpb-master + ./waf install # install sdpb to --prefix=$HOME/install/sdpb/master diff --git a/test/src/integration_tests/cases/end-to-end.test.cxx b/test/src/integration_tests/cases/end-to-end.test.cxx index 4a8ece04..234b3f62 100644 --- a/test/src/integration_tests/cases/end-to-end.test.cxx +++ b/test/src/integration_tests/cases/end-to-end.test.cxx @@ -1,5 +1,7 @@ #include "integration_tests/common.hxx" +#include + // Realistic end-to-end test for pmp2sdp + sdpb // JSON input taken from "SingletScalar_cT_test_nmax6" and // "SingletScalarAllowed_test_nmax6" @@ -22,7 +24,7 @@ namespace int num_procs = 6; int precision = 768; Named_Args_Map pmp2sdp_args; - std::string default_sdpb_args; + std::vector default_sdpb_args; std::vector sdpb_out_filenames; std::vector sdpb_out_txt_keys; bool check_sdp = true; @@ -92,7 +94,7 @@ namespace {"--output", sdp_path}, {"--precision", std::to_string(precision)}}); - runner.create_nested("pmp2sdp").mpi_run({"build/pmp2sdp"}, args, + runner.create_nested("pmp2sdp").mpi_run({"build/pmp2sdp", args}, num_procs); if(check_sdp) @@ -115,17 +117,17 @@ namespace {"--outDir", (output_dir / "out").string()}, {"--checkpointDir", (output_dir / "ck").string()}}; runner.create_nested("sdpb").mpi_run( - {"build/sdpb", default_sdpb_args}, args, num_procs); + {"build/sdpb", default_sdpb_args, args}, num_procs); if(run_sdpb_twice) { runner.create_nested("sdpb-2").mpi_run( - {"build/sdpb", default_sdpb_args}, args, num_procs); + {"build/sdpb", default_sdpb_args, args}, num_procs); } - // Read c,B,y and check that (c - B.y) equals to the vector written to c_minus_By/c_minus_By.json - check_c_minus_By(sdp_path, output_dir / "out", precision, - diff_precision, - runner.create_nested("check_c_minus_By")); + // Read c,B,y and check that (c - B.y) equals to the vector written to c_minus_By/c_minus_By.json + check_c_minus_By(sdp_path, output_dir / "out", precision, + diff_precision, + runner.create_nested("check_c_minus_By")); // SDPB runs with --precision= // We check test output up to lower precision= @@ -146,7 +148,7 @@ namespace {"--verbosity", "debug"}, }; runner.create_nested("spectrum") - .mpi_run({"build/spectrum"}, args, num_procs); + .mpi_run({"build/spectrum", args}, num_procs); // Cannot check block paths if the same spectrum.json // is generated several times by different PMP inputs, @@ -240,14 +242,14 @@ TEST_CASE("end-to-end_tests") INFO("Spectrum should find isolated zero for the last block " "(corresponding to x=4/3)."); End_To_End_Test test("1d-isolated-zeros"); - test.default_sdpb_args - = "--checkpointInterval 3600 --maxRuntime 1340 " - "--dualityGapThreshold 1.0e-30 --primalErrorThreshold 1.0e-30 " - "--dualErrorThreshold 1.0e-30 --initialMatrixScalePrimal 1.0e20 " - "--initialMatrixScaleDual 1.0e20 --feasibleCenteringParameter 0.1 " - "--infeasibleCenteringParameter 0.3 --stepLengthReduction 0.7 " - "--maxComplementarity 1.0e100 --maxIterations 1000 --verbosity 1 " - "--procGranularity 1 --writeSolution x,y"; + test.default_sdpb_args = boost::program_options::split_unix( + "--checkpointInterval 3600 --maxRuntime 1340 " + "--dualityGapThreshold 1.0e-30 --primalErrorThreshold 1.0e-30 " + "--dualErrorThreshold 1.0e-30 --initialMatrixScalePrimal 1.0e20 " + "--initialMatrixScaleDual 1.0e20 --feasibleCenteringParameter 0.1 " + "--infeasibleCenteringParameter 0.3 --stepLengthReduction 0.7 " + "--maxComplementarity 1.0e100 --maxIterations 1000 --verbosity 1 " + "--procGranularity 1 --writeSolution x,y"); test.num_procs = 1; test.check_sdp = false; // Write SDP to zip archive to test that spectrum can read sdp.zip/pmp_info.json @@ -262,15 +264,15 @@ TEST_CASE("end-to-end_tests") "which caused a bug."); INFO("Test data from Harvard cluster, gmp/6.2.1 mpfr/4.2.0"); End_To_End_Test test("dfibo-0-0-j=3-c=3.0000-d=3-s=6"); - test.default_sdpb_args - = "--findDualFeasible --findPrimalFeasible " - "--initialMatrixScalePrimal 1e10 --initialMatrixScaleDual 1e10 " - "--maxComplementarity 1e30 --dualErrorThreshold 1e-10 " - "--primalErrorThreshold 1e-153 --maxRuntime 259200 " - "--checkpointInterval 3600 --maxIterations 1000 " - "--feasibleCenteringParameter=0.1 --infeasibleCenteringParameter=0.3 " - "--stepLengthReduction=0.7 " - "--maxSharedMemory=100K"; // forces split_factor=3 for Q window + test.default_sdpb_args = boost::program_options::split_unix( + "--findDualFeasible --findPrimalFeasible " + "--initialMatrixScalePrimal 1e10 --initialMatrixScaleDual 1e10 " + "--maxComplementarity 1e30 --dualErrorThreshold 1e-10 " + "--primalErrorThreshold 1e-153 --maxRuntime 259200 " + "--checkpointInterval 3600 --maxIterations 1000 " + "--feasibleCenteringParameter=0.1 --infeasibleCenteringParameter=0.3 " + "--stepLengthReduction=0.7 " + "--maxSharedMemory=100K"); // forces split_factor=3 for Q window for(std::string sdp_format : {"", "bin", "json"}) { DYNAMIC_SECTION( @@ -291,14 +293,14 @@ TEST_CASE("end-to-end_tests") "Scalars3d/SingletScalar2020.hs"); INFO("Test data is generated with SDPB 2.5.1 on Caltech cluster."); INFO("SDPB should find primal-dual optimal solution."); - std::string default_sdpb_args - = "--checkpointInterval 3600 --maxRuntime 1340 " - "--dualityGapThreshold 1.0e-30 --primalErrorThreshold 1.0e-30 " - "--dualErrorThreshold 1.0e-30 --initialMatrixScalePrimal 1.0e20 " - "--initialMatrixScaleDual 1.0e20 --feasibleCenteringParameter 0.1 " - "--infeasibleCenteringParameter 0.3 --stepLengthReduction 0.7 " - "--maxComplementarity 1.0e100 --maxIterations 1000 --verbosity 2 " - "--procGranularity 1 --writeSolution x,y,z"; + auto default_sdpb_args = boost::program_options::split_unix( + "--checkpointInterval 3600 --maxRuntime 1340 " + "--dualityGapThreshold 1.0e-30 --primalErrorThreshold 1.0e-30 " + "--dualErrorThreshold 1.0e-30 --initialMatrixScalePrimal 1.0e20 " + "--initialMatrixScaleDual 1.0e20 --feasibleCenteringParameter 0.1 " + "--infeasibleCenteringParameter 0.3 --stepLengthReduction 0.7 " + "--maxComplementarity 1.0e100 --maxIterations 1000 --verbosity 2 " + "--procGranularity 1 --writeSolution x,y,z"); SECTION("primal_dual_optimal") { End_To_End_Test test("SingletScalar_cT_test_nmax6/primal_dual_optimal"); @@ -330,16 +332,16 @@ TEST_CASE("end-to-end_tests") "Scalars3d/SingletScalar2020.hs"); INFO("Test data is generated with SDPB 2.5.1 on Caltech cluster."); std::string name = "SingletScalarAllowed_test_nmax6"; - std::string default_sdpb_args - = "--checkpointInterval 3600 --maxRuntime 1341 " - "--dualityGapThreshold 1.0e-30 --primalErrorThreshold 1.0e-200 " - "--dualErrorThreshold 1.0e-200 --initialMatrixScalePrimal 1.0e20 " - "--initialMatrixScaleDual 1.0e20 --feasibleCenteringParameter 0.1 " - "--infeasibleCenteringParameter 0.3 --stepLengthReduction 0.7 " - "--maxComplementarity 1.0e100 --maxIterations 1000 --verbosity 2 " - "--procGranularity 1 --writeSolution y,z " - "--detectPrimalFeasibleJump --detectDualFeasibleJump " - "--maxSharedMemory=100.1K"; // forces split_factor=3 for Q window; also test floating-point --maxSharedMemory value + auto default_sdpb_args = boost::program_options::split_unix( + "--checkpointInterval 3600 --maxRuntime 1341 " + "--dualityGapThreshold 1.0e-30 --primalErrorThreshold 1.0e-200 " + "--dualErrorThreshold 1.0e-200 --initialMatrixScalePrimal 1.0e20 " + "--initialMatrixScaleDual 1.0e20 --feasibleCenteringParameter 0.1 " + "--infeasibleCenteringParameter 0.3 --stepLengthReduction 0.7 " + "--maxComplementarity 1.0e100 --maxIterations 1000 --verbosity 2 " + "--procGranularity 1 --writeSolution y,z " + "--detectPrimalFeasibleJump --detectDualFeasibleJump " + "--maxSharedMemory=100.1K"); // forces split_factor=3 for Q window; also test floating-point --maxSharedMemory value SECTION("primal_feasible_jump") { diff --git a/test/src/integration_tests/cases/outer_limits.test.cxx b/test/src/integration_tests/cases/outer_limits.test.cxx index e2809499..76314f32 100644 --- a/test/src/integration_tests/cases/outer_limits.test.cxx +++ b/test/src/integration_tests/cases/outer_limits.test.cxx @@ -29,8 +29,8 @@ TEST_CASE("outer_limits") // TODO allow running pmp2functions in parallel runner.create_nested("pmp2functions") - .run({"build/pmp2functions", std::to_string(precision), pmp_json, - functions_json}); + .run({"build/pmp2functions", + {std::to_string(precision), pmp_json, functions_json}}); Test_Util::REQUIRE_Equal::diff_functions_json( functions_json, functions_orig_json, precision, precision / 2); } @@ -54,7 +54,7 @@ TEST_CASE("outer_limits") { INFO("run outer_limits"); runner.create_nested("outer_limits") - .mpi_run({"build/outer_limits"}, args, num_procs); + .mpi_run({"build/outer_limits", args}, num_procs); auto out_orig_json = data_dir / "out_orig.json"; Test_Util::REQUIRE_Equal::diff_outer_limits(out_json, out_orig_json, diff --git a/test/src/integration_tests/cases/pmp2sdp.test.cxx b/test/src/integration_tests/cases/pmp2sdp.test.cxx index 3758621f..28d755ce 100644 --- a/test/src/integration_tests/cases/pmp2sdp.test.cxx +++ b/test/src/integration_tests/cases/pmp2sdp.test.cxx @@ -33,7 +33,7 @@ TEST_CASE("pmp2sdp") auto sdp_path = (runner.output_dir / "sdp").string(); args["--output"] = sdp_path; - runner.create_nested("run").mpi_run({"build/pmp2sdp"}, args); + runner.create_nested("run").mpi_run({"build/pmp2sdp", args}); Test_Util::REQUIRE_Equal::diff_sdp(sdp_path, sdp_orig, precision, diff_precision, @@ -69,7 +69,7 @@ TEST_CASE("pmp2sdp") args["--output"] = sdp_path.string(); if(!output_format.empty()) args["--outputFormat"] = output_format; - runner.create_nested("run").mpi_run({"build/pmp2sdp"}, args); + runner.create_nested("run").mpi_run({"build/pmp2sdp", args}); { INFO("Check that pmp2sdp actually uses --outputFormat=" @@ -117,7 +117,7 @@ TEST_CASE("pmp2sdp") args["--input"] = input; args["--output"] = sdp_dir.string(); // We allow pmp2sdp to overwrite exsiting sdp_temp - runner.mpi_run({"build/pmp2sdp"}, args, num_procs, 0, + runner.mpi_run({"build/pmp2sdp", args}, num_procs, 0, "exists and will be overwritten"); } SECTION("sdp_dir_exists") @@ -137,7 +137,7 @@ TEST_CASE("pmp2sdp") args["--input"] = input; args["--output"] = sdp_dir.string(); // We allow pmp2sdp to overwrite exsiting sdp - runner.mpi_run({"build/pmp2sdp"}, args, num_procs, 0, + runner.mpi_run({"build/pmp2sdp", args}, num_procs, 0, "exists and will be overwritten"); } @@ -158,7 +158,7 @@ TEST_CASE("pmp2sdp") args["--output"] = sdp_readonly_zip.string(); args["--zip"] = ""; // We allow pmp2sdp to overwrite exsiting sdp - runner.mpi_run({"build/pmp2sdp"}, args, num_procs, 0, + runner.mpi_run({"build/pmp2sdp", args}, num_procs, 0, "exists and will be overwritten"); } @@ -180,7 +180,7 @@ TEST_CASE("pmp2sdp") args["--output"] = sdp_path; auto sdp_invalid_zip = (runner.output_dir / "sdp.invalid.zip").string(); - runner.mpi_run({"build/pmp2sdp"}, args, num_procs, 1, "No such file"); + runner.mpi_run({"build/pmp2sdp", args}, num_procs, 1, "No such file"); } } @@ -207,7 +207,7 @@ TEST_CASE("pmp2sdp") auto sdp_path = (runner.output_dir / "sdp").string(); args["--output"] = sdp_path; - runner.create_nested("run").mpi_run({"build/pmp2sdp"}, args, num_procs); + runner.create_nested("run").mpi_run({"build/pmp2sdp", args}, num_procs); auto sdp_orig = Test_Config::test_data_dir / "end-to-end_tests" / "1d-constraints" / "output" / "sdp"; @@ -235,7 +235,7 @@ TEST_CASE("pmp2sdp") args["--input"] = input_nsv; auto sdp_path = (runner.output_dir / "sdp").string(); args["--output"] = sdp_path; - runner.mpi_run({"build/pmp2sdp"}, args, num_procs, 1, + runner.mpi_run({"build/pmp2sdp", args}, num_procs, 1, "Found different objective vectors in input files:"); } } diff --git a/test/src/integration_tests/cases/sdpb.test.cxx b/test/src/integration_tests/cases/sdpb.test.cxx index 6efa43c5..191b5d47 100644 --- a/test/src/integration_tests/cases/sdpb.test.cxx +++ b/test/src/integration_tests/cases/sdpb.test.cxx @@ -32,7 +32,7 @@ TEST_CASE("sdpb") // but we keep it here to check backward compatibility, // i.e. SDPB shouldn't fail when we pass this option. args["--procsPerNode"] = std::to_string(num_procs); - runner.mpi_run({"build/sdpb"}, args, num_procs, required_exit_code, + runner.mpi_run({"build/sdpb", args}, num_procs, required_exit_code, required_error_msg); }; @@ -136,7 +136,7 @@ TEST_CASE("sdpb") fs::perms::none); const Test_Util::Test_Case_Runner runner_noread = runner.create_nested("noread"); - runner_noread.mpi_run({"build/sdpb"}, args, num_procs, 1, + runner_noread.mpi_run({"build/sdpb", args}, num_procs, 1, "Unable to open checkpoint file"); } @@ -166,7 +166,7 @@ TEST_CASE("sdpb") args["--checkpointDir"] = args["--outDir"]; const Test_Util::Test_Case_Runner runner_corrupt = runner.create_nested("read_corrupt"); - runner_corrupt.mpi_run({"build/sdpb"}, args, num_procs, 1, + runner_corrupt.mpi_run({"build/sdpb", args}, num_procs, 1, "Corrupted data in file"); } } diff --git a/test/src/integration_tests/common.hxx b/test/src/integration_tests/common.hxx index a1888dcc..95abf943 100644 --- a/test/src/integration_tests/common.hxx +++ b/test/src/integration_tests/common.hxx @@ -2,6 +2,7 @@ #include -#include "util/diff.hxx" #include "util/Test_Case_Runner.hxx" #include "util/Test_Config.hxx" +#include "util/diff.hxx" +#include "util/process.hxx" diff --git a/test/src/integration_tests/main.cxx b/test/src/integration_tests/main.cxx index 2d63dff9..83276383 100644 --- a/test/src/integration_tests/main.cxx +++ b/test/src/integration_tests/main.cxx @@ -2,14 +2,14 @@ #include "sdpb_util/assert.hxx" #include -#include #include +#include +#include #ifndef CATCH_AMALGAMATED_CUSTOM_MAIN #error "To override main, pass '-D CATCH_AMALGAMATED_CUSTOM_MAIN' to compiler" #endif -namespace bp = boost::process; namespace fs = std::filesystem; namespace @@ -47,10 +47,10 @@ int main(int argc, char *argv[]) // Build a new command line parser on top of Catch2's using namespace Catch::Clara; - std::string mpirun = "mpirun"; + std::string mpirun_str = "mpirun"; // bind mpirun_command variable to a new option --mpirun auto cli = session.cli() - | Opt(mpirun, "mpirun")["--mpirun"]( + | Opt(mpirun_str, "mpirun")["--mpirun"]( "mpirun command, e.g. --mpirun=srun or " "--mpirun=\"mpirun --mca btl vader,openib,self\"." "By default, --mpirun=mpirun"); @@ -63,16 +63,14 @@ int main(int argc, char *argv[]) if(returnCode != 0) // Indicates a command line error return returnCode; - // TODO process arguments like --mpirun="mpirun --mca btl vader,openib,self" - Test_Config::mpirun = mpirun; - + Test_Config::mpirun = Command::split(mpirun_str); // Check if we can run mpirun - auto mpirun_help_command = mpirun + " --help"; + auto mpirun_help = Test_Config::mpirun + "--help"; std::error_code ec; - int check_mpi = bp::system(mpirun_help_command, bp::std_out > bp::null, ec); + const int check_mpi = run_command(mpirun_help, {stdin, nullptr, stderr}, ec); if(check_mpi != 0) { - std::cout << "Failed to run MPI: " << mpirun_help_command << std::endl; + std::cout << "Failed to run MPI: " << mpirun_help << std::endl; if(ec.value() != 0) std::cout << "Error code: " << ec << std::endl; else diff --git a/test/src/integration_tests/util/Test_Case_Runner.cxx b/test/src/integration_tests/util/Test_Case_Runner.cxx index 723bb7f1..56493c63 100644 --- a/test/src/integration_tests/util/Test_Case_Runner.cxx +++ b/test/src/integration_tests/util/Test_Case_Runner.cxx @@ -1,62 +1,13 @@ #include "Test_Case_Runner.hxx" #include "Test_Config.hxx" +#include "process.hxx" #include "sdpb_util/assert.hxx" #include +#include namespace fs = std::filesystem; -namespace -{ - // concatenate args with " " separator - inline void build_args_stream(std::ostringstream &) {} - template - void build_args_stream(std::ostringstream &os, const T &item, - const ArgPack &...args) - { - os << item << " "; - build_args_stream(os, args...); - } - - // concatenate args with " " separator - inline std::string build_args_string(const std::string &arg) - { - return arg; - } - template - std::string build_args_string(const ArgPack &...args) - { - std::ostringstream os; - build_args_stream(os, args...); - return os.str(); - } - - template std::string build_command_line(const Ts &...args) - { - return build_args_string(args...); - } - - // mpirun -n 2 - std::string build_mpirun_prefix(int numProcs) - { - return build_command_line(Test_Config::mpirun, "-n", - std::to_string(numProcs)); - } - - std::string build_string_from_named_args( - const Test_Util::Test_Case_Runner::Named_Args_Map &named_args) - { - std::stringstream ss; - for(const auto &[key, value] : named_args) - { - ss << " " << key; - if(!value.empty()) - ss << "=" << value; - } - return ss.str(); - } -} - namespace Test_Util { // NB: name should be a valid path relative to test_log_dir @@ -85,18 +36,16 @@ namespace Test_Util return Test_Case_Runner(name + separator + suffix); } - void - Test_Case_Runner::run(const std::string &command, int required_exit_code, - const std::string &required_error_msg) const + void Test_Case_Runner::run(const Command &command, int required_exit_code, + const std::string &required_error_msg) const { - namespace bp = boost::process; - CAPTURE(command); CAPTURE(stdout_path); CAPTURE(stderr_path); - int exit_code = bp::system(command, bp::std_out > stdout_path.string(), - bp::std_err > stderr_path.string()); + // int exit_code = bp::system(command, bp::std_out > stdout_path.string(), + // bp::std_err > stderr_path.string()); + int exit_code = run_command(command, {stdin, stdout_path, stderr_path}); // NB: We need separate stderr output to process stderr_string. // TODO: ideally, we want to redirect bp::std_err to both stdout_path and stderr_path // instead of appending stderr to the end ot stdout. @@ -149,53 +98,25 @@ namespace Test_Util } } - void Test_Case_Runner::mpi_run(const std::string &command, int numProcs, - int required_exit_code, + void Test_Case_Runner::mpi_run(const Command &command, const int numProcs, + const int required_exit_code, const std::string &required_error_msg) const { - auto mpi_command - = build_command_line(build_mpirun_prefix(numProcs), command); + auto mpi_command = Test_Config::mpirun; + mpi_command += {"-n", std::to_string(numProcs)}; + mpi_command += command; run(mpi_command, required_exit_code, required_error_msg); } - void Test_Case_Runner::run(const std::vector &args, - const Named_Args_Map &named_args, - int required_exit_code, - const std::string &required_error_msg) const - { - auto args_string = boost::algorithm::join(args, " "); - auto named_args_string = build_string_from_named_args(named_args); - auto command = build_command_line(args_string, named_args_string); - run(command, required_exit_code, required_error_msg); - } - - void Test_Case_Runner::mpi_run(const std::vector &args, - const Named_Args_Map &named_args, - int numProcs, int required_exit_code, - const std::string &required_error_msg) const - { - std::vector args_with_mpi(args); - args_with_mpi.insert(args_with_mpi.begin(), build_mpirun_prefix(numProcs)); - run(args_with_mpi, named_args, required_exit_code, required_error_msg); - } - fs::path Test_Case_Runner::unzip_to_temp_dir(const fs::path &zip_path) const { - auto temp_dir = output_dir; + const auto temp_dir = output_dir; fs::create_directories(temp_dir); static int unique_suffix; - auto filename + const auto filename = zip_path.filename().string() + "." + std::to_string(unique_suffix++); auto output_path = temp_dir / filename; - auto unzip = boost::process::search_path("unzip"); - if(unzip.empty()) - FAIL("Cannot find unzip"); - - // control.json may differ by "command" field - // thus we exclude this file from comparison - auto unzip_command - = build_command_line("unzip -o", zip_path, "-d", output_path); - run(unzip_command); + run({"unzip", {"-o", zip_path, "-d", output_path}}); return output_path; } } \ No newline at end of file diff --git a/test/src/integration_tests/util/Test_Case_Runner.hxx b/test/src/integration_tests/util/Test_Case_Runner.hxx index 4e4d0669..efb9bd3c 100644 --- a/test/src/integration_tests/util/Test_Case_Runner.hxx +++ b/test/src/integration_tests/util/Test_Case_Runner.hxx @@ -1,10 +1,12 @@ #pragma once +#include "process.hxx" + #include #include -#include #include +#include namespace Test_Util { @@ -16,7 +18,7 @@ namespace Test_Util // inside the section! Otherwise, some output files will be removed. struct Test_Case_Runner : boost::noncopyable { - typedef std::map Named_Args_Map; + using Named_Args_Map = Command::Named_Args_Map; const std::string name; const std::filesystem::path data_dir; @@ -31,16 +33,9 @@ namespace Test_Util create_nested(const std::string &suffix, const std::string &separator = "/") const; - void run(const std::string &command, int required_exit_code = 0, - const std::string &required_error_msg = "") const; - void mpi_run(const std::string &command, int numProcs = 2, - int required_exit_code = 0, - const std::string &required_error_msg = "") const; - void run(const std::vector &args, - const Named_Args_Map &named_args = {}, int required_exit_code = 0, + void run(const Command &command, int required_exit_code = 0, const std::string &required_error_msg = "") const; - void mpi_run(const std::vector &args, - const Named_Args_Map &named_args = {}, int numProcs = 2, + void mpi_run(const Command &command, int numProcs = 2, int required_exit_code = 0, const std::string &required_error_msg = "") const; diff --git a/test/src/integration_tests/util/Test_Config.hxx b/test/src/integration_tests/util/Test_Config.hxx index 1a362495..08dc6367 100644 --- a/test/src/integration_tests/util/Test_Config.hxx +++ b/test/src/integration_tests/util/Test_Config.hxx @@ -1,12 +1,14 @@ #pragma once +#include "process.hxx" + #include #include #include namespace Test_Config { - inline std::string mpirun = "mpirun"; + inline Command mpirun{"mpirun", {}, {}}; const std::filesystem::path test_data_dir = "test/data"; const std::filesystem::path test_output_dir = "test/out"; const std::filesystem::path test_log_dir = "test/out/log"; diff --git a/test/src/integration_tests/util/process.cxx b/test/src/integration_tests/util/process.cxx new file mode 100644 index 00000000..27d05af4 --- /dev/null +++ b/test/src/integration_tests/util/process.cxx @@ -0,0 +1,210 @@ +#include "process.hxx" + +#include "sdpb_util/assert.hxx" +#include "sdpb_util/ostream/ostream_vector.hxx" + +#include +#include +#include + +// There are two versions for Boost.Process, V1 and V2. +// V2 was introduced in Boost 1.80 and became default in Boost 1.89. +// Prior to Boost 1.86, V2 failed to compile on Alpine Linux (musl-libc), +// see https://github.com/boostorg/process/pull/376 +// NB: The code for choosing Boost.Process version should match the one used in waf-tools/boost.py +#if BOOST_VERSION < 108600 +#define USE_BOOST_PROCESS_V1 +#include +namespace bp = boost::process; +#elif BOOST_VERSION < 108900 +#include +namespace bp = boost::process::v2; +#else +#include +namespace bp = boost::process; +#endif + +// struct Command + +Command::Command(const std::string &exe, const std::vector &args, + const Named_Args_Map &named_args) + : exe(exe), args(args) +{ + *this += named_args; +} +Command::Command(const std::string &exe, const Named_Args_Map &named_args) + : Command(exe, {}, named_args) +{} + +Command Command::split(const std::string &cmd) +{ + const auto cmd_with_args = boost::program_options::split_unix(cmd); + ASSERT(!cmd_with_args.empty(), "Cannot split cmd=", cmd); + auto begin = cmd_with_args.begin(); + const auto end = cmd_with_args.end(); + const auto exe = *begin++; + const std::vector args(begin, end); + return Command(exe, args); +} +Command &Command::operator+=(const std::string &arg) +{ + if(!arg.empty()) + args.push_back(arg); + return *this; +} +Command &Command::operator+=(const std::vector &more_args) +{ + args.reserve(args.size() + more_args.size()); + for(auto &arg : more_args) + *this += arg; + return *this; +} +Command &Command::operator+=(const Named_Args_Map &more_args) +{ + args.reserve(args.size() + more_args.size() * 2); + for(const auto &[key, value] : more_args) + { + *this += key; + *this += value; + } + return *this; +} +Command &Command::operator+=(const Command &other) +{ + *this += other.exe; + return *this += other.args; +} +std::string Command::to_string() const +{ + std::ostringstream os; + os << *this; + return os.str(); +} +std::ostream &operator<<(std::ostream &os, const Command &cmd) +{ + os << cmd.exe; + for(const auto &arg : cmd.args) + { + os << ' ' << arg; + } + return os; +} + +// struct Command + +template struct is_in_variant; + +template +struct is_in_variant> + : std::disjunction...> +{}; + +template +inline constexpr bool is_in_variant_v = is_in_variant::value; + +// Helper structure for Process_Stdio. +// TODO: shall we use it in API instead of Process_Stdio (which uses std::variant)? +// This would allow us to replace std::visit with template functions, +// but then we'd have to move implementation to header file +template + && is_in_variant_v + && is_in_variant_v, + int>> +struct TProcess_Stdio +{ + TIn in; + TOut out; + TErr err; + + TProcess_Stdio(const TIn &in, const TOut &out, const TErr &err) + : in(in), out(out), err(err) + {} +}; + +#ifdef USE_BOOST_PROCESS_V1 +template auto to_bp_redirect_arg(const T &value) +{ + return value; +} +template <> auto to_bp_redirect_arg(const std::nullptr_t &) +{ + return bp::null; +} +#endif + +template +int run_command(const Command &command, + const TProcess_Stdio &proc_stdio, + std::error_code &ec) +{ + try + { +#ifdef USE_BOOST_PROCESS_V1 + const auto in = bp::std_in < to_bp_redirect_arg(proc_stdio.in); + const auto out = bp::std_out > to_bp_redirect_arg(proc_stdio.out); + const auto err = bp::std_err > to_bp_redirect_arg(proc_stdio.err); + return bp::system(command.to_string(), in, out, err, ec); +#else + boost::asio::io_context ctx; + bp::process_stdio io{bp::detail::process_input_binding(proc_stdio.in), + bp::detail::process_output_binding(proc_stdio.out), + bp::detail::process_error_binding(proc_stdio.err)}; + boost::system::error_code local_ec; + const auto exe = find_executable(command.exe).string(); + ASSERT(!exe.empty(), "Cannot find executable: ", command.exe); + const auto &args = command.args; + return bp::execute(bp::process(ctx, exe, args, io), local_ec); + ec.assign(local_ec.value(), local_ec.category()); +#endif + } + catch(std::exception &ex) + { + std::ostringstream os; + os << "Failed to run command: exe=" << command.exe + << ", args=" << command.args << ":\n" + << ex.what(); + RUNTIME_ERROR(os.str()); + } +} + +int run_command(const Command &command, const Process_Stdio &proc_stdio, + std::error_code &ec) +{ + return std::visit( + [&](auto &&in) { + return std::visit( + [&](auto &&out) { + return std::visit( + [&](auto &&err) { + return run_command(command, TProcess_Stdio(in, out, err), ec); + }, + proc_stdio.err); + }, + proc_stdio.out); + }, + proc_stdio.in); +} + +int run_command(const Command &command, const Process_Stdio &proc_stdio) +{ + std::error_code ec; + const int exit_code = run_command(command, proc_stdio, ec); + ASSERT(!ec, ec.message()); + return exit_code; +} + +std::filesystem::path find_executable(const std::string &filename) +{ + // Boost.Process returns empty string for paths with directory separators. + // We still want an executable, so we simply return the path. + // TODO: check also that the file exists and is executable? + if(std::filesystem::path(filename).has_parent_path()) + return filename; +#ifdef USE_BOOST_PROCESS_V1 + return bp::search_path(filename).string(); +#else + return bp::environment::find_executable(filename).string(); +#endif +} diff --git a/test/src/integration_tests/util/process.hxx b/test/src/integration_tests/util/process.hxx new file mode 100644 index 00000000..23f13a43 --- /dev/null +++ b/test/src/integration_tests/util/process.hxx @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +struct Command +{ + using Named_Args_Map = std::map; + + std::string exe; + std::vector args; + + Command(const std::string &exe, const std::vector &args, + const Named_Args_Map &named_args = {}); + Command(const std::string &exe, const Named_Args_Map &named_args); + [[nodiscard]] static Command split(const std::string &cmd); + + Command &operator+=(const std::string &arg); + Command &operator+=(const std::vector &more_args); + Command &operator+=(const Named_Args_Map &more_args); + Command &operator+=(const Command &other); + + template friend Command operator+(Command lhs, const T &rhs) + { + lhs += rhs; + return lhs; + } + + std::string to_string() const; +}; + +std::ostream &operator<<(std::ostream &os, const Command &cmd); + +struct Process_Stdio +{ + // NB: separate variant option for nullptr is necessary for constructing bp::process_stdio. + // Passing FILE* nullptr will lead to segfault. + using In = std::variant; + using Out = std::variant; + using Err = Out; + + In in = stdin; + Out out = stdout; + Err err = stderr; +}; + +int run_command(const Command &command, const Process_Stdio &, + std::error_code &ec); +int run_command(const Command &command, const Process_Stdio & = {}); + +std::filesystem::path find_executable(const std::string &path); diff --git a/waf-tools/boost.py b/waf-tools/boost.py index 56dbe7c5..3bcec8b6 100644 --- a/waf-tools/boost.py +++ b/waf-tools/boost.py @@ -46,20 +46,58 @@ def configure(conf): if conf.options.boost_libs: boost_libs = conf.options.boost_libs.split() else: - boost_libs = ['boost_system', 'boost_date_time', 'boost_filesystem', + boost_libs = ['boost_date_time', 'boost_filesystem', 'boost_program_options', 'boost_iostreams', 'boost_serialization'] + # Boost.System library is header-only since 1.69 + # libboost_system stub has been removed since 1.89 + boost_system_lib = 'boost_system' + boost_system_lib_found = any(name == boost_system_lib for name in boost_libs) + + # Boost.Process v2 + boost_process_lib = 'boost_process' + boost_process_lib_found = any(name == boost_process_lib for name in boost_libs) + boost_stacktrace_lib_found = any(name.startswith('boost_stacktrace') for name in boost_libs) # link to boost_stacktrace library instead of header-only compilation: boost_defines = ['BOOST_STACKTRACE_LINK'] if boost_stacktrace_lib_found else [] + conf.start_msg('Checking for Boost') + + # Link to boost_system, if necessary + if not boost_system_lib_found: + for boost_system_libs in [[], [boost_system_lib]]: + if check_config(conf, + msg=f' Checking for Boost.System, boost_system_libs={boost_system_libs}', + fragment=""" +#include +namespace sys = boost::system; +int main() +{ + sys::error_code ec; + const auto& cat = sys::generic_category(); +} +""", + includes=boost_incdir, + uselib_store='boost', + libpath=boost_libdir, + lib=boost_system_libs, + use=['cxx17'], + mandatory=False): + boost_system_lib_found = True + boost_libs += boost_system_libs + break + + if not boost_system_lib_found: + conf.fatal('Could not find Boost.System') + + # Check other Boost libraries check_config(conf, fragment="""#include #include #include #include #include -#include #include #include int main() @@ -69,8 +107,6 @@ def configure(conf): boost::iostreams::file_sink("foo"); boost::iostreams::filtering_ostream(); boost::iostreams::gzip_compressor(); -boost::process::ipstream pipe_stream; -boost::process::search_path("unzip"); boost::serialization::version_type version; boost::stacktrace::stacktrace(); } @@ -82,6 +118,63 @@ def configure(conf): use=['cxx17'], defines=boost_defines) + # Link to boost_process, if necessary + if not boost_process_lib_found: + for boost_process_libs in [[], [boost_process_lib]]: + if check_config(conf, + msg=f' Checking for Boost.Process, boost_process_libs={boost_process_libs}', + fragment=""" +#include + +// There are two versions for Boost.Process, V1 and V2. +// V2 was introduced in Boost 1.80 and became default in Boost 1.89. +// Prior to Boost 1.86, V2 failed to compile on Alpine Linux (musl-libc), +// see https://github.com/boostorg/process/pull/376 +// NB: The code for choosing Boost.Process version should match test/src/integration_tests/util/process.cxx +#if BOOST_VERSION < 108600 +#define USE_BOOST_PROCESS_V1 +#include +namespace bp = boost::process; + +#elif BOOST_VERSION < 108900 +#include +namespace bp = boost::process::v2; + +#else +#include +namespace bp = boost::process; + +#endif + +#include + +std::filesystem::path find_executable(const std::string &filename) +{ +#ifdef USE_BOOST_PROCESS_V1 + return bp::search_path(filename).string(); +#else + return bp::environment::find_executable(filename).string(); +#endif +} + +int main() +{ + auto ls = find_executable("ls"); +} +""", + includes=boost_incdir, + uselib_store='boost', + libpath=boost_libdir, + lib=boost_libs + boost_process_libs, + use=['cxx17'], + mandatory=False): + boost_process_lib_found = True + boost_libs += boost_process_libs + break + + if not boost_process_lib_found: + conf.fatal('Could not find Boost.Process') + # If boost_stacktrace library not defined by user, try linking to one of the libraries # listed in https://www.boost.org/doc/libs/1_84_0/doc/html/stacktrace/configuration_and_build.html # boost_stacktrace_backtrace and boost_stacktrace_addr2line can print source code location for each frame. @@ -108,6 +201,8 @@ def configure(conf): mandatory=False): break + conf.end_msg(True) + def options(opt): boost = opt.add_option_group('Boost Options') diff --git a/wscript b/wscript index da849d66..8c46886a 100644 --- a/wscript +++ b/wscript @@ -296,6 +296,7 @@ def build(bld): 'test/src/integration_tests/util/diff_sdpb_out.cxx', 'test/src/integration_tests/util/diff_spectrum.cxx', 'test/src/integration_tests/util/Parse_SDP.cxx', + 'test/src/integration_tests/util/process.cxx', 'test/src/integration_tests/util/Test_Case_Runner.cxx', 'test/src/integration_tests/cases/end-to-end.test.cxx', 'test/src/integration_tests/cases/outer_limits.test.cxx',