From f4e8bdafd4986a6bb5988248ae5074404374feba Mon Sep 17 00:00:00 2001 From: Christopher Dilks Date: Mon, 19 Feb 2024 17:27:43 -0500 Subject: [PATCH] feat: test algorithms with sanitizers and coverage (#106) --- .github/install-dependency-packages.sh | 4 + .github/pages-index.html | 13 ++ .github/workflows/ci.yml | 277 ++++++++++++++++++++----- .github/workflows/macos.yml | 6 + .github/workflows/minver.yml | 6 + README.md | 5 +- examples/meson.build | 17 +- meson.build | 21 +- meson.options | 8 +- src/iguana/algorithms/meson.build | 11 + src/iguana/tests/iguana-test.cc | 126 +++++++++++ src/iguana/tests/meson.build | 34 +++ 12 files changed, 456 insertions(+), 72 deletions(-) create mode 100644 .github/pages-index.html create mode 100644 src/iguana/tests/iguana-test.cc create mode 100644 src/iguana/tests/meson.build diff --git a/.github/install-dependency-packages.sh b/.github/install-dependency-packages.sh index f65e7dac..39a776c4 100755 --- a/.github/install-dependency-packages.sh +++ b/.github/install-dependency-packages.sh @@ -8,12 +8,16 @@ set -e GENERAL_PACKAGE_LIST_LINUX=( python gcc + clang make cmake tree pkgconf ninja meson + gcovr # for coverage + python-pygments # for coverage report syntax colors + llvm # for `llvm-symbolizer`, for human-readable sanitizer results ) IGUANA_PACKAGE_LIST_LINUX=( fmt diff --git a/.github/pages-index.html b/.github/pages-index.html new file mode 100644 index 00000000..51986a6f --- /dev/null +++ b/.github/pages-index.html @@ -0,0 +1,13 @@ + + + + Iguana + + +

Iguana

+ + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faeca294..ec1a2a58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,23 @@ on: required: false type: string default: 'latest' + test_matrix: + description: 'Test matrix for `test_iguana` job (JSON)' + required: false + type: string + # test coverage and sanitizers + # FIXME: `b_sanitize=memory` is not used because `libc++` needs to be re-compiled with `-fsanitize=memory`, otherwise + # we are bothered by false positives (e.g., from `std::map` insertion) + default: >- + { + "include": [ + { "mode": "coverage", "CC": "gcc", "CXX": "g++", "buildtype": "release" }, + { "mode": "address sanitizer", "CC": "clang", "CXX": "clang++", "buildtype": "debug" }, + { "mode": "thread sanitizer", "CC": "clang", "CXX": "clang++", "buildtype": "debug" }, + { "mode": "undefined behavior sanitizer", "CC": "clang", "CXX": "clang++", "buildtype": "debug" }, + { "mode": "leak sanitizer", "CC": "clang", "CXX": "clang++", "buildtype": "debug" } + ] + } defaults: run: @@ -28,6 +45,25 @@ env: jobs: + # download test data + ######################################################### + + download_validation_files: + name: Download validation files + runs-on: ${{ inputs.runner }} + steps: + - name: download + run: wget --no-check-certificate http://clasweb.jlab.org/clas12offline/distribution/clas12-timeline/validation_files.tar.gz + - name: untar + run: tar xzvf validation_files.tar.gz + - name: select one file + run: mv -v $(find validation_files -type f -name "*.hipo" | head -n1) test_data.hipo + - uses: actions/upload-artifact@v4 + with: + name: validation_files + retention-days: 1 + path: test_data.hipo + # dependencies ######################################################### @@ -48,13 +84,13 @@ jobs: ref: ${{ env.hipo_version }} - name: build run: | - cmake -S . -B build -G Ninja --install-prefix $(pwd)/hipo + cmake -S . -B build -G Ninja --install-prefix $(pwd)/hipo -DCMAKE_POSITION_INDEPENDENT_CODE=ON # using PIE build, for sanitizer readibility cmake --build build cmake --install build tar czvf hipo{.tar.gz,} - uses: actions/upload-artifact@v4 with: - name: build_deps_hipo + name: install_deps_hipo retention-days: 1 path: hipo.tar.gz @@ -69,12 +105,19 @@ jobs: container: image: ${{ inputs.container }} strategy: - fail-fast: false + fail-fast: true matrix: - binding: [ cpp, python ] include: - - { binding: cpp, binding_opts: '' } - - { binding: python, binding_opts: '-Dbind_python=True' } + # build C++-only version with various compilers and buildtypes + - { id: cpp-gcc-release, CC: gcc, CXX: g++, buildtype: release, binding_opts: '' } + - { id: cpp-gcc-debug, CC: gcc, CXX: g++, buildtype: debug, binding_opts: '' } + - { id: cpp-clang-release, CC: clang, CXX: clang++, buildtype: release, binding_opts: '' } + - { id: cpp-clang-debug, CC: clang, CXX: clang++, buildtype: debug, binding_opts: '' } + # build with bindings + - { id: python, CC: gcc, CXX: g++, buildtype: release, binding_opts: '-Dbind_python=True' } + env: + CC: ${{ matrix.CC }} + CXX: ${{ matrix.CXX }} steps: ### setup - uses: actions/checkout@v4 @@ -88,31 +131,30 @@ jobs: - name: get local dependencies uses: actions/download-artifact@v4 with: - pattern: build_deps_* + pattern: install_deps_* merge-multiple: true - name: untar local dependencies run: ls *.tar.gz | xargs -I{} tar xzvf {} - name: tree local dependencies run: tree hipo - name: summarize dependencies + if: ${{ matrix.id == 'cpp-gcc-release' }} run: | echo '### Dependencies' >> $GITHUB_STEP_SUMMARY echo '| Dependency | Version |' >> $GITHUB_STEP_SUMMARY echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY echo "| \`hipo\` | ${{ env.hipo_version }} |" >> $GITHUB_STEP_SUMMARY cat pkg_summary.md >> $GITHUB_STEP_SUMMARY - ### build iguana - - name: resolve local dependencies - run: meson/resolve-dependencies.py --ini native.ini --hipo ./hipo - name: meson setup - run: meson setup --native-file=native.ini build-iguana + run: meson setup build-iguana $(meson/resolve-dependencies.py --cli --hipo ./hipo) - name: meson configure run: | - meson configure \ - --prefix=$(pwd)/iguana \ - -Dexamples=True \ - -Ddocumentation=False \ - ${{ matrix.binding_opts }} \ + meson configure \ + --prefix=$(pwd)/iguana \ + -Dbuildtype=${{ matrix.buildtype }} \ + -Dexamples=True \ + -Ddocumentation=False \ + ${{ matrix.binding_opts }} \ build-iguana - name: dump build options run: meson configure build-iguana | cat @@ -123,7 +165,7 @@ jobs: if: always() run: cat build-iguana/meson-logs/meson-log.txt - name: readelf/otool iguana examples - if: ${{ matrix.binding == 'cpp' }} + if: ${{ matrix.id == 'cpp-gcc-release' || matrix.id == 'cpp-gcc-debug' || matrix.id == 'cpp-clang-release' || matrix.id == 'cpp-clang-debug' }} run: | binaries=$(find iguana/bin -type f -name "iguana-example-*") libraries=$(find iguana -type f -name "*.so") @@ -145,37 +187,127 @@ jobs: - run: tree iguana ### upload artifacts - name: tar - run: tar czvf iguana{.tar.gz,} + run: | + tar czvf iguana{.tar.gz,} + tar czvf build-iguana{.tar.gz,} - uses: actions/upload-artifact@v4 with: - name: build_iguana_${{ matrix.binding }} + name: install_iguana_${{ matrix.id }} retention-days: 1 path: iguana.tar.gz + - uses: actions/upload-artifact@v4 + if: ${{ matrix.id == 'cpp-gcc-release' || matrix.id == 'cpp-gcc-debug' || matrix.id == 'cpp-clang-release' || matrix.id == 'cpp-clang-debug' }} + with: + name: build_iguana_${{ matrix.id }} + retention-days: 1 + path: build-iguana.tar.gz - # download test data + # run tests ######################################################### - download_validation_files: - name: Download validation files + test_iguana: + name: Test Iguana + needs: + - build_hipo + - download_validation_files + - build_iguana runs-on: ${{ inputs.runner }} + container: + image: ${{ inputs.container }} + strategy: + fail-fast: false + matrix: ${{ fromJson(inputs.test_matrix) }} + env: + CC: ${{ matrix.CC }} + CXX: ${{ matrix.CXX }} + build_id: ${{ matrix.CC }}-${{ matrix.buildtype }} steps: - - name: download - run: wget --no-check-certificate http://clasweb.jlab.org/clas12offline/distribution/clas12-timeline/validation_files.tar.gz - - name: untar - run: tar xzvf validation_files.tar.gz - - name: select one file - run: mv -v $(find validation_files -type f -name "*.hipo" | head -n1) test_data.hipo - - uses: actions/upload-artifact@v4 + ### setup + - uses: actions/checkout@v4 + with: # settings needed for version number detection + clean: false + fetch-tags: true + fetch-depth: 0 + - name: get test data + uses: actions/download-artifact@v4 with: name: validation_files + - name: get iguana build dir + uses: actions/download-artifact@v4 + with: + name: build_iguana_cpp-${{ env.build_id }} + ### dependencies + - name: install dependency packages + run: .github/install-dependency-packages.sh ${{ inputs.runner }} ${{ inputs.verset }} + - name: get local dependencies + uses: actions/download-artifact@v4 + with: + pattern: install_deps_* + merge-multiple: true + - name: untar local dependencies + run: ls *.tar.gz | xargs -I{} tar xzvf {} + ### re-build iguana + - name: meson configure + run: | + meson configure \ + -Dtest_data_file=$(pwd)/test_data.hipo \ + -Dtest_num_events=1000 \ + build-iguana + case "${{ matrix.mode }}" in + coverage) + meson configure \ + -Db_coverage=true \ + build-iguana + ;; + *sanitizer) + san=$(echo ${{ matrix.mode }} | sed 's; .*;;g') + meson configure \ + -Db_sanitize=$san \ + -Db_lundef=false \ + -Db_pie=true \ + build-iguana + ;; + esac + - name: dump build options + run: meson configure build-iguana | cat + - name: meson install + run: meson install -C build-iguana + ### run tests + - name: TEST algorithms + run: | + echo -e "\e[1;35m[NOTE]: log files are found in artifact 'logs_build_iguana_${{ matrix.mode }}' \e[0m" + meson test --print-errorlogs -C build-iguana + - name: TEST coverage + if: ${{ matrix.mode == 'coverage' }} + run: | + ninja -C build-iguana coverage + mv build-iguana/meson-logs/coveragereport coverage-report + echo '### Coverage Report' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat build-iguana/meson-logs/coverage.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '- for details, see the `coverage-report` artifact' >> $GITHUB_STEP_SUMMARY + echo '- to compare to the report from the `main` branch, see ' >> $GITHUB_STEP_SUMMARY + ### upload artifacts + - uses: actions/upload-artifact@v4 + if: ${{ matrix.mode != 'coverage' }} + with: + name: logs_build_iguana_${{ matrix.mode }} retention-days: 1 - path: test_data.hipo + path: build-iguana/meson-logs + - uses: actions/upload-artifact@v4 + if: ${{ matrix.mode == 'coverage' }} + with: + name: coverage-report + retention-days: 7 + path: coverage-report - # run tests + # run examples ######################################################### - test_iguana: - name: Test Iguana + test_examples: + name: Test Examples needs: - download_validation_files - build_iguana @@ -185,10 +317,9 @@ jobs: strategy: fail-fast: false matrix: - binding: [ cpp, python ] include: - - { binding: cpp, extension: '' } - - { binding: python, extension: '.py' } + - { id: cpp-gcc-release, extension: '' } + - { id: python, extension: '.py' } steps: ### dependencies and test data - uses: actions/checkout@v4 @@ -200,12 +331,12 @@ jobs: - name: get dependency build artifacts uses: actions/download-artifact@v4 with: - pattern: build_deps_* + pattern: install_deps_* merge-multiple: true - name: get iguana build artifacts uses: actions/download-artifact@v4 with: - name: build_iguana_${{ matrix.binding }} + name: install_iguana_${{ matrix.id }} - name: get test data uses: actions/download-artifact@v4 with: @@ -218,7 +349,7 @@ jobs: run: tree ### setup python virtaul environment (for python binding tests) - name: install python binding runtime dependencies - if: ${{ matrix.binding == 'python' }} + if: ${{ matrix.id == 'python' }} run: | python -m venv .venv source .venv/bin/activate @@ -226,13 +357,13 @@ jobs: python -m pip install -r iguana_src/bind/python/requirements.txt ### set env vars - depends on runner and binding - name: source environment for Linux and python - if: ${{ inputs.runner == 'ubuntu-latest' && matrix.binding == 'python' }} + if: ${{ inputs.runner == 'ubuntu-latest' && matrix.id == 'python' }} run: | source iguana/bin/this_iguana.sh verbose echo PKG_CONFIG_PATH=$PKG_CONFIG_PATH >> $GITHUB_ENV echo PYTHONPATH=$PYTHONPATH >> $GITHUB_ENV - name: source environment for macOS and python - if: ${{ inputs.runner == 'macos-latest' && matrix.binding == 'python' }} + if: ${{ inputs.runner == 'macos-latest' && matrix.id == 'python' }} run: | source iguana/bin/this_iguana.sh verbose ld echo PKG_CONFIG_PATH=$PKG_CONFIG_PATH >> $GITHUB_ENV @@ -245,10 +376,13 @@ jobs: run: iguana/bin/iguana-example-01-bank-rows${{ matrix.extension }} test_data.hipo ${{ env.num_events }} - name: test 02 run: iguana/bin/iguana-example-02-YAMLReader${{ matrix.extension }} - if: ${{ matrix.binding == 'cpp' }} #FIXME + if: ${{ matrix.id == 'cpp-gcc-release' }} #FIXME - name: test 03 run: iguana/bin/iguana-example-03-zvertex-filter${{ matrix.extension }} test_data.hipo ${{ env.num_events }} - if: ${{ matrix.binding == 'cpp' }} #FIXME + if: ${{ matrix.id == 'cpp-gcc-release' }} #FIXME + + # test consumers + ######################################################### test_consumer_builds: name: Test consumer builds @@ -262,6 +396,8 @@ jobs: fail-fast: false matrix: tool: [ cmake, make, meson ] + env: + build_id: cpp-gcc-release steps: ### dependencies and test data - uses: actions/checkout@v4 @@ -270,12 +406,12 @@ jobs: - name: get dependency build artifacts uses: actions/download-artifact@v4 with: - pattern: build_deps_* + pattern: install_deps_* merge-multiple: true - name: get iguana build artifacts uses: actions/download-artifact@v4 with: - name: build_iguana_cpp + name: install_iguana_${{ env.build_id }} - name: get test data uses: actions/download-artifact@v4 with: @@ -309,7 +445,7 @@ jobs: ######################################################### doc_generate: - if: ${{ inputs.runner == 'ubuntu-latest' }} + if: ${{ inputs.runner == 'ubuntu-latest' && inputs.verset == 'latest' }} name: Generate documentation runs-on: ${{ inputs.runner }} steps: @@ -318,15 +454,53 @@ jobs: uses: mattnotmitt/doxygen-action@v1 with: doxyfile-path: doc/Doxyfile - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: doxygen retention-days: 1 path: doc/api/ - doc_deploy: - if: ${{ (github.head_ref == 'main' || github.ref_name == 'main') && inputs.runner == 'ubuntu-latest' }} - name: Deploy documentation - needs: doc_generate + # deployment + ######################################################### + + collect_webpages: + if: ${{ (github.head_ref == 'main' || github.ref_name == 'main') && inputs.runner == 'ubuntu-latest' && inputs.verset == 'latest' }} + name: Collect webpages + needs: + - doc_generate + - test_iguana + runs-on: ${{ inputs.runner }} + steps: + - uses: actions/checkout@v4 + with: + path: iguana_src + - name: download doxygen documentation + uses: actions/download-artifact@v4 + with: + name: doxygen + path: doxygen + - name: download coverage report + uses: actions/download-artifact@v4 + with: + name: coverage-report + path: coverage-report + - run: tree + - name: collect + run: | + mkdir pages + cp iguana_src/.github/pages-index.html pages/index.html + mv doxygen pages/ + mv coverage-report pages/ + - run: tree + - uses: actions/upload-pages-artifact@v3 + with: + retention-days: 1 + path: pages/ + + deploy_webpages: + if: ${{ (github.head_ref == 'main' || github.ref_name == 'main') && inputs.runner == 'ubuntu-latest' && inputs.verset == 'latest' }} + name: Deploy webpages + needs: collect_webpages permissions: pages: write id-token: write @@ -346,6 +520,7 @@ jobs: name: Final needs: - test_iguana + - test_examples - test_consumer_builds runs-on: ${{ inputs.runner }} steps: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7ceedd0e..8d35745d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -16,3 +16,9 @@ jobs: uses: ./.github/workflows/ci.yml with: runner: macos-latest + test_matrix: >- + { + "include": [ + { "mode": "test", "CC": "gcc", "CXX": "g++", "buildtype": "release" } + ] + } diff --git a/.github/workflows/minver.yml b/.github/workflows/minver.yml index 312f43a5..2beabf50 100644 --- a/.github/workflows/minver.yml +++ b/.github/workflows/minver.yml @@ -18,3 +18,9 @@ jobs: runner: ubuntu-latest container: archlinux/archlinux:latest verset: minver + test_matrix: >- + { + "include": [ + { "mode": "test", "CC": "gcc", "CXX": "g++", "buildtype": "release" } + ] + } diff --git a/README.md b/README.md index 7527d5b7..2e919099 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,12 @@ 1. [Setup Guide](doc/setup.md) 1. [Troubleshooting](doc/troubleshooting.md) 1. [Design Notes](doc/design.md) -1. [API documentation](https://jeffersonlab.github.io/iguana/) +1. [API documentation](https://jeffersonlab.github.io/iguana/doxygen) 1. [Developing a new Algorithm](src/iguana/algorithms/example/README.md) 1. [Repository Maintenance](doc/maintenance.md) +## Status +1. [Coverage Report](https://jeffersonlab.github.io/iguana/coverage-report) + > [!CAUTION] > This is still a _prototype_ design diff --git a/examples/meson.build b/examples/meson.build index 6fe166d2..6a976fe7 100644 --- a/examples/meson.build +++ b/examples/meson.build @@ -5,21 +5,6 @@ example_sources = [ 'iguana-example-03-zvertex-filter.cc', ] -# add dependencies' libraries to rpath -example_rpaths = [ - hipo_dep.get_variable(pkgconfig: 'libdir'), -] -if host_machine.system() != 'darwin' - # FIXME(darwin): not sure how to set multiple rpaths on darwin executables, aside - # from running `install_name_tool -add_rpath` post-installation; luckily, - # darwin-built examples only need `hipo_dep` libraries in the rpath and they - # don't need the `iguana` library path explictly included in the rpath, so - # this `if` block just keeps `example_rpaths` to only one element. If we need - # more rpath elements someday, either we need to use `install_name_tool` or, - # preferably, resolution of https://github.com/mesonbuild/meson/issues/5760 - example_rpaths += '$ORIGIN' / '..' / get_option('libdir') -endif - # build executables foreach src : example_sources executable( @@ -29,7 +14,7 @@ foreach src : example_sources dependencies: project_deps, link_with: project_libs, install: true, - install_rpath: ':'.join(example_rpaths), + install_rpath: ':'.join(project_exe_rpath), ) endforeach diff --git a/meson.build b/meson.build index eeed83f5..2647caf7 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,10 @@ project( ) project_description = 'Implementation Guardian of Analysis Algorithms' +# meson modules +pkg = import('pkgconfig') +fs = import('fs') + # resolve dependencies # NOTE: those that are typically installed by package managers should use `meson/minimum-version.sh` fmt_dep = dependency( @@ -62,6 +66,21 @@ project_pkg_vars = [ 'dep_libdirs=' + ':'.join(dep_lib_paths), ] +# executables' rpath +project_exe_rpath = [ + hipo_dep.get_variable(pkgconfig: 'libdir'), +] +if host_machine.system() != 'darwin' + # FIXME(darwin): not sure how to set multiple rpaths on darwin executables, aside + # from running `install_name_tool -add_rpath` post-installation; luckily, + # darwin-built examples only need `hipo_dep` libraries in the rpath and they + # don't need the `iguana` library path explictly included in the rpath, so + # this `if` block just keeps `project_exe_rpath` to only one element. If we need + # more rpath elements someday, either we need to use `install_name_tool` or, + # preferably, resolution of https://github.com/mesonbuild/meson/issues/5760 + project_exe_rpath += '$ORIGIN' / '..' / get_option('libdir') +endif + # set preprocessor macros add_project_arguments( '-DIGUANA_ETC="' + get_option('prefix') / project_etc + '"', @@ -71,6 +90,7 @@ add_project_arguments( # build and install shared libraries subdir('src/iguana/services') subdir('src/iguana/algorithms') +subdir('src/iguana/tests') # build bindings if get_option('bind_python') @@ -78,7 +98,6 @@ if get_option('bind_python') endif # generate pkg-config file -pkg = import('pkgconfig') pkg.generate( name: meson.project_name(), description: project_description, diff --git a/meson.options b/meson.options index 93858080..c128e1a8 100644 --- a/meson.options +++ b/meson.options @@ -1,3 +1,5 @@ -option('examples', type: 'boolean', value: false, description: 'Build Iguana C++ examples') -option('documentation', type: 'boolean', value: false, description: 'Generate API documentation (requires Doxygen)') -option('bind_python', type: 'boolean', value: false, description: 'Generate Python bindings and their examples') +option('examples', type: 'boolean', value: false, description: 'Build Iguana C++ examples') +option('documentation', type: 'boolean', value: false, description: 'Generate API documentation (requires Doxygen)') +option('test_data_file', type: 'string', value: '', description: 'Sample HIPO file for testing. Must be an absolute path or relative to the build directory. If unspecified, tests are not built') +option('test_num_events', type: 'string', value: '10', description: 'Number of events from `test_data_file` to test') +option('bind_python', type: 'boolean', value: false, description: 'Generate Python bindings and their examples') diff --git a/src/iguana/algorithms/meson.build b/src/iguana/algorithms/meson.build index 2180c5cf..0ca46f61 100644 --- a/src/iguana/algorithms/meson.build +++ b/src/iguana/algorithms/meson.build @@ -1,3 +1,4 @@ +# algorithm source files algo_sources = [ 'Algorithm.cc', 'AlgorithmFactory.cc', @@ -8,6 +9,7 @@ algo_sources = [ 'clas12/LorentzTransformer.cc', ] +# algorithm headers algo_public_headers = [ 'Algorithm.h', 'AlgorithmBoilerplate.h', @@ -19,10 +21,19 @@ algo_public_headers = [ 'clas12/LorentzTransformer.h', ] +# algorithm configurations algo_config_dirs = [ 'clas12/config', ] +# algorithm unique names and required banks, for those we want to test automatically +algos_and_banks_for_unit_testing = { + 'example::ExampleAlgorithm': ['REC::Particle'], + 'clas12::EventBuilderFilter': ['REC::Particle'], + 'clas12::ZVertexFilter': ['REC::Particle'], + 'clas12::LorentzTransformer': ['REC::Particle'], +} + algo_lib = shared_library( 'IguanaAlgorithms', algo_sources, diff --git a/src/iguana/tests/iguana-test.cc b/src/iguana/tests/iguana-test.cc new file mode 100644 index 00000000..c75da515 --- /dev/null +++ b/src/iguana/tests/iguana-test.cc @@ -0,0 +1,126 @@ +#include +#include +#include + +int main(int argc, char **argv) { + + // parse arguments + int opt; + std::string command = ""; + std::string data_file = ""; + int num_events = 10; + std::string algo_name = ""; + std::vector bank_names; + bool verbose = false; + auto Usage = [&]() { + fmt::print(stderr, "\nUSAGE: {} [OPTIONS]...\n", argv[0]); + fmt::print("\n"); + fmt::print(" OPTIONS:\n"); + fmt::print("\n"); + fmt::print(" {:<20} {}\n\n", "-c COMMAND", "which test command to use:"); + fmt::print(" {:<20} {:<15} {}\n", "", "algorithm", "call `Run` on an algorithm"); + fmt::print(" {:<20} {:<15} {}\n", "", "unit", "call `Test` on an algorithm, for unit tests"); + fmt::print("\n"); + fmt::print(" {:<20} {}\n", "-f FILE", "input data file, for when COMMAND==algorithm"); + fmt::print("\n"); + fmt::print(" {:<20} {}\n", "-n NUM_EVENTS", "number of events from the data file"); + fmt::print(" {:<20} default: {}\n", "", num_events); + fmt::print("\n"); + fmt::print(" {:<20} {}\n", "-a ALGORITHM", "the name of the algorithm"); + fmt::print("\n"); + fmt::print(" {:<20} {}\n", "-b BANKS", "add a bank to process"); + fmt::print("\n"); + fmt::print(" {:<20} {}\n", "-v", "increase verbosity"); + fmt::print("\n"); + return 2; + }; + if(argc==1) + return Usage(); + while((opt=getopt(argc, argv, "c:f:n:a:b:v|")) != -1) { + switch(opt) { + case 'c': + command = std::string(optarg); + break; + case 'f': + data_file = std::string(optarg); + break; + case 'n': + num_events = std::stoi(optarg); + break; + case 'a': + algo_name = std::string(optarg); + break; + case 'b': + bank_names.push_back(std::string(optarg)); + break; + case 'v': + verbose = true; + break; + default: + return Usage(); + } + } + if(command == "" || algo_name == "" || bank_names.empty()) { + fmt::print(stderr, "ERROR: need at least a command, algorithm name, and banks\n"); + return Usage(); + } + if(command == "algorithm" && data_file == "") { + fmt::print(stderr, "ERROR: need a data file for command 'algorithm'\n"); + return Usage(); + } + if(verbose) { + fmt::print("TEST IGUANA:\n"); + fmt::print(" {:>20} = {}\n", "command", command); + fmt::print(" {:>20} = {}\n", "data_file", data_file); + fmt::print(" {:>20} = {}\n", "num_events", num_events); + fmt::print(" {:>20} = {}\n", "algo_name", algo_name); + fmt::print(" {:>20} = {}\n", "banks", fmt::join(bank_names,", ")); + fmt::print("\n"); + } + + // open the HIPO file; we use 2 readers, one for 'before' (i.e., not passed through iguana), and one for 'after' + // (passed through iguana), so we may compare them + hipo::reader reader_before(data_file.c_str()); // NOTE: not copy-constructable, so make two separate readers + hipo::reader reader_after(data_file.c_str()); + auto banks_before = reader_before.getBanks(bank_names); + auto banks_after = reader_after.getBanks(bank_names); + + // define the algorithm + iguana::AlgorithmSequence seq; + seq.Add(algo_name); + seq.SetOption(algo_name, "log", verbose ? "debug" : "info"); + + // start the algorithm + seq.Start(banks_after); + + // event loop + int it_ev = 0; + while(reader_after.next(banks_after) && (num_events==0 || it_ev++ < num_events)) { + // iterate the 'before' reader too + reader_before.next(banks_before); + // run the algorithm + if(command=="algorithm") + seq.Run(banks_after); + else if(command=="unit") { + fmt::print(stderr, "ERROR: unit tests are not yet implemented (TODO)\n"); + return 1; + } + else { + fmt::print(stderr, "ERROR: unknown command '{}'\n", command); + return 1; + } + // print the banks, before and after + if(verbose) { + for(decltype(bank_names)::size_type it_bank=0; it_bank < bank_names.size(); it_bank++) { + fmt::print("{:=^70}\n", fmt::format(" BEFORE: {} ", bank_names.at(it_bank))); + banks_before.at(it_bank).show(); + fmt::print("{:=^70}\n", fmt::format(" AFTER: {} ", bank_names.at(it_bank))); + banks_after.at(it_bank).show(); + } + } + } + + // stop the algorithm + seq.Stop(); + return 0; +} diff --git a/src/iguana/tests/meson.build b/src/iguana/tests/meson.build new file mode 100644 index 00000000..b001325e --- /dev/null +++ b/src/iguana/tests/meson.build @@ -0,0 +1,34 @@ +test_exe_name = 'iguana-test' + +test_exe = executable( + test_exe_name, + test_exe_name + '.cc', + include_directories: project_inc, + dependencies: project_deps, + link_with: project_libs, + install: true, + install_rpath: ':'.join(project_exe_rpath), +) + +if fs.is_file(get_option('test_data_file')) + message('Test sample file provided; you may run tests with `meson test`') + foreach algo, banks : algos_and_banks_for_unit_testing + bank_args = [] + foreach bank : banks + bank_args += ['-b', bank] + endforeach + test( + test_exe_name + ' ' + algo.replace('::', '__'), + test_exe, + args: [ + '-c', 'algorithm', + '-f', get_option('test_data_file'), + '-n', get_option('test_num_events'), + '-a', algo + ] + bank_args, + ) + endforeach +else + stat_file = get_option('test_data_file')=='' ? 'provided' : 'found' + message('Test sample file NOT ' + stat_file + '; you may run tests manually with `' + test_exe_name + '`') +endif