diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 2015dfd..82ea251 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -22,27 +22,43 @@ jobs: - name: Update apt repositories for ccache run: sudo apt update - name: Install dependencies - run: sudo apt-get install util-linux libmount-dev slurm-wlm libslurm-dev slurmd slurmctld slurm-client - - name: Set up ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - key: ccache-linux-${{ matrix.buildtype }}-${{ matrix.sanitizer }} - - name: Setup compiler and build tools - run: sudo apt install --no-install-recommends --yes g++-12 meson ninja-build + run: sudo apt-get install util-linux libmount-dev libslurm-dev + # run: sudo apt-get install util-linux libmount-dev slurm-wlm libslurm-dev slurmd slurmctld slurm-client + # disable for now, because the cache has never actually worked + # ironically, this means that it slows down the build + #- name: Set up ccache + # uses: hendrikmuhs/ccache-action@v1.2 + # with: + # key: ccache-linux-${{ matrix.buildtype }}-${{ matrix.sanitizer }} + - name: Install compiler and build tools + run: sudo apt install --no-install-recommends --yes g++-12 ninja-build + - name: install meson + run: | + wget https://github.com/mesonbuild/meson/releases/download/1.6.1/meson-1.6.1.tar.gz && \ + tar -xzf meson-1.6.1.tar.gz && \ + mv meson-1.6.1 meson && \ + mv meson/meson.py meson/meson + - name: install squashfs-mount + run: | + wget https://github.com/eth-cscs/squashfs-mount/archive/refs/tags/v1.1.0.tar.gz + tar -xzf v1.1.0.tar.gz + ./meson/meson setup build-sqfs squashfs-mount-1.1.0 + ./meson/meson compile -C build-sqfs + sudo ./meson/meson install -C build-sqfs - name: Configure run: | - CXX="ccache g++-12" meson setup \ + CC="gcc-12" CXX="g++-12" ./meson/meson setup \ --buildtype ${{ matrix.buildtype }} \ -Db_sanitize=${{ matrix.sanitizer }} \ + -Dtests=enabled \ --warnlevel 3 \ --werror \ build . - name: Build - run: ninja -C build - - name: Setup Test - run: cd test/setup && ./setup - - name: Test + run: ./meson/meson compile -Cbuild + - name: test unit + run: ./meson/meson test -Cbuild --verbose unit + - name: test cli run: | - export ASAN_OPTIONS=fast_unwind_on_malloc=0:strict_string_checks=1:detect_leaks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 - export UBSAN_OPTIONS=print_stacktrace=1 - cd build && ./unit + sudo mkdir /user-tools /user-environment + ./meson/meson test -Cbuild --verbose cli diff --git a/install-alps-local.sh b/install-alps-local.sh index ab66ac6..bb6b652 100755 --- a/install-alps-local.sh +++ b/install-alps-local.sh @@ -12,18 +12,13 @@ install=$HOME/.local/$arch rm -rf $build rm -rf $pyenv - -echo "== use python to install meson and ninja in venv: $pyenv" -python3 -m venv $pyenv -source $pyenv/bin/activate -pip install --upgrade pip -pip install meson ninja echo "== configure in $build" -CC=gcc-12 CXX=g++-12 meson setup --prefix=$install $build $root + +CC=gcc-12 CXX=g++-12 uenv run prgenv-gnu/24.11:v1 --view=default -- meson setup --prefix=$install $build $root echo "== build" -meson compile -C$build +uenv run prgenv-gnu/24.11:v1 --view=default -- meson compile -C$build echo "== install" -meson install -C$build --skip-subprojects +uenv run prgenv-gnu/24.11:v1 --view=default -- meson install -C$build --skip-subprojects echo "" echo "== succesfully installed" diff --git a/meson.build b/meson.build index 4cc9bd3..14d5919 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,7 @@ project('uenv', ['cpp'], 'warning_level=3', ], version: files('VERSION'), - meson_version: '>=0.57') + meson_version: '>=1.3') version = meson.project_version() oras_version = get_option('oras_version') @@ -22,13 +22,13 @@ add_global_arguments('-Wno-missing-field-initializers', language : 'cpp') # use meson wrap to provide the dependencies, because the 'default_library=static' option # will be propogated to each dependency, for a statically linked executable. -catch_dep = subproject('catch2', default_options: ['werror=false', 'warning_level=0']).get_variable('catch2_with_main_dep') +catch_dep = subproject('catch2', default_options: ['werror=false', 'warning_level=0', 'tests=false']).get_variable('catch2_with_main_dep') cli11_dep = subproject('cli11', default_options: ['werror=false', 'warning_level=0']).get_variable('CLI11_dep') fmt_dep = subproject('fmt', default_options: ['werror=false', 'warning_level=0']).get_variable('fmt_dep') json_dep = subproject('nlohmann_json', default_options: ['werror=false', 'warning_level=0']).get_variable('nlohmann_json_dep') spdlog_dep = subproject('spdlog', default_options: ['werror=false', 'warning_level=0','std_format=disabled','external_fmt=enabled']).get_variable('spdlog_dep') sqlite3_dep = subproject('sqlite3', default_options: ['werror=false', 'warning_level=0']).get_variable('sqlite3_dep') -subproject('curl', default_options: ['werror=false', 'warning_level=0']) +subproject('curl', default_options: ['werror=false', 'warning_level=0', 'tests=disabled', 'unittests=disabled', 'tool=disabled']) curl_dep = dependency('libcurl', required: true) barkeep_dep = declare_dependency( @@ -106,7 +106,7 @@ if uenv_cli oras_download = custom_target( 'oras-download', output: 'oras.tar.gz', - command: ['curl', '-L', oras_url, '-o', '@OUTPUT@'], + command: ['wget', '--quiet', oras_url, '-O', '@OUTPUT@'], install: false, ) @@ -120,25 +120,11 @@ if uenv_cli ) endif -unit_src = [ - 'test/unit/dates.cpp', - 'test/unit/env.cpp', - 'test/unit/envvars.cpp', - 'test/unit/fs.cpp', - 'test/unit/lex.cpp', - 'test/unit/main.cpp', - 'test/unit/parse.cpp', - 'test/unit/signal.cpp', - 'test/unit/repository.cpp', - 'test/unit/subprocess.cpp', -] - -unit = executable('unit', - sources: unit_src, - dependencies: [catch_dep, lib_dep], - build_by_default: true, - install: false) - if uenv_slurm_plugin subdir('src/slurm') endif + +if get_option('tests').enabled() + subdir('test') +endif + diff --git a/meson_options.txt b/meson_options.txt index 2fd5be4..8270251 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,6 @@ +option('tests', type: 'feature', value: 'disabled') + option('slurm_plugin', type: 'boolean', value: true) option('cli', type: 'boolean', value: true) + option('oras_version', type: 'string', value: '1.2.0') diff --git a/readme.md b/readme.md index f4f5807..df50324 100644 --- a/readme.md +++ b/readme.md @@ -40,35 +40,61 @@ cd uenv2 The software uses meson wrap to bring its own dependencies, all of which are built as static libraries. To build you only need -* meson +* meson >= 1.5 * ninja -* g++ that supports C++17 (including the `std::filesystem` library implementation +* g++ that supports C++20 (we test g++12 regularly) On your laptop these requirements can be met using your package manager of choice. On an Alps vCluster, we want to use the system compiler "as is" without using a uenv or modules. The `g++` requirement is met by the `g++-12` compiler, that is installed on the vClusters as part of the boot image. The easiest way to set up meson and ninja is to pip install them to create an isolated build environment. +On Alps the version of Python3 is ancient, so it can't support the required meson version. +The `prgenv-gnu/24.11` uenv provides all of the tools required. + +``` +alias e="uenv run prgenv-gnu/24.11:v1 --view=default --" +CXX=g++-12 e meson setup -Dtests=enabled build +e meson compile -Cbuild ``` -python3 -m venv ./.env -source .env/bin/activate -pip install ninja meson -mkdir build -cd build -CC=gcc-12 CXX=g++-12 meson setup .. +* use the system version of gcc-12 in order to create a statically linked binary that can run outside the uenv +* the integraton tests can't be run through the alias (or in the uenv), because it isn't possible to use `uenv run` or `uenv start` inside a uenv. + +## testing + +There are three sets of tests: + +* `unit`: unit tests for the C++ library components (Catch2) +* `cli`: tests for the CLI interface (bats) +* `slurm`: tests for the Slurm plugin (bats) -ninja +To build tests, use the `-Dtests=enabled` flag to meson + +``` +meson setup -Dtests=enabled ``` -## testing +The tests are installed in the `test` sub-directory of the build path: +``` +cd build/test -**NOTE**: the tests require setup stages that are not straightforward to set up on Alps. A PR that fixes these issues is coming soon. +# run the unit tests +./unit -The C++ library has unit tests, that are built by default as the `unit` executable in the build path: +# run the cli tests +./bats cli.bats +``` -```bash -> ./unit -Randomness seeded to: 478418581 -=============================================================================== -All tests passed (137 assertions in 16 test cases) +The tests can also be run using meson: ``` +# run all the tests +meson test + +# or run test suites separately +meson test cli +meson test unit +``` + +**NOTE**: the slurm integration tests require configuring Slurm to use the plugin, which requires root permissions. +For this reason, they can't be tested on Alps vClusters. + diff --git a/src/slurm/meson.build b/src/slurm/meson.build index 3875c85..cab8edd 100644 --- a/src/slurm/meson.build +++ b/src/slurm/meson.build @@ -7,23 +7,13 @@ configure_file(input : 'config.hpp.in', libmount_dep = dependency('mount') -#lib_src = ['./lib/parse_args.cpp', - #'./lib/database.cpp', - #'./lib/filesystem.cpp', - #'./lib/mount.cpp', - #'./lib/sqlite.cpp', - #'./lib/strings.cpp'] - module_src = ['plugin.cpp', 'mount.cpp'] -#module_inc = include_directories('src') - module_dep = [libmount_dep, sqlite3_dep, lib_dep] shared_module('slurm-uenv-mount', sources: module_src, dependencies: module_dep, - #include_directories: module_inc, cpp_args: ['-Wall', '-Wpedantic', '-Wextra'], install: true) diff --git a/src/uenv/meson.build b/src/uenv/meson.build index 87e8a1e..22b9dfe 100644 --- a/src/uenv/meson.build +++ b/src/uenv/meson.build @@ -1,28 +1,24 @@ -version_path = meson.current_build_dir()+'config.h' -fs = import('fs') -if not fs.is_file(version_path) - uenv_version = meson.project_version() - uenv_version_array = uenv_version.split('-') - if uenv_version_array.length()==2 - uenv_version_prerelease = uenv_version_array[1] - else - uenv_version_prerelease = '' - endif - uenv_version_array = uenv_version_array[0].split('.') - uenv_version_major = uenv_version_array[0].to_int() - uenv_version_minor = uenv_version_array[1].to_int() - uenv_version_patch = uenv_version_array[2].to_int() +uenv_version = meson.project_version() +uenv_version_array = uenv_version.split('-') +if uenv_version_array.length()==2 + uenv_version_prerelease = uenv_version_array[1] +else + uenv_version_prerelease = '' +endif +uenv_version_array = uenv_version_array[0].split('.') +uenv_version_major = uenv_version_array[0].to_int() +uenv_version_minor = uenv_version_array[1].to_int() +uenv_version_patch = uenv_version_array[2].to_int() - conf = configuration_data() +conf = configuration_data() - conf.set('version', uenv_version) - conf.set('version_major', uenv_version_major) - conf.set('version_minor', uenv_version_minor) - conf.set('version_patch', uenv_version_patch) - conf.set('version_prerelease', uenv_version_prerelease) +conf.set('version', uenv_version) +conf.set('version_major', uenv_version_major) +conf.set('version_minor', uenv_version_minor) +conf.set('version_patch', uenv_version_patch) +conf.set('version_prerelease', uenv_version_prerelease) - configure_file(input : 'config.h.in', - output : 'config.h', - configuration : conf) -endif +configure_file(input : 'config.h.in', + output : 'config.h', + configuration : conf) diff --git a/test/integration/cli.bats b/test/integration/cli.bats index 21fbf8b..e641d7d 100644 --- a/test/integration/cli.bats +++ b/test/integration/cli.bats @@ -1,32 +1,23 @@ function setup() { - bats_install_path=$(realpath ./install) - export BATS_LIB_PATH=$bats_install_path/bats-helpers - # set the cluster name to be arapiles # this is required for tests to work when run on a vCluster # that sets this variable export CLUSTER_NAME=arapiles + #echo "BATS_LIB_PATH $BATS_LIB_PATH" 1>&3 bats_load_library bats-support bats_load_library bats-assert load ./common - export REPOS=$(realpath ../scratch/repos) - export SQFS_LIB=$(realpath ../scratch/sqfs) - - export SRC_PATH=$(realpath ../../) - - export PATH="$(realpath ../../build):$PATH" + # TODO: set the BUILD_PATH from a template for out of tree builds + export PATH="$BUILD_PATH:$PATH" unset UENV_MOUNT_LIST # set up location for creation of working repos - export REPO_ROOT=/tmp/uenv-repo - rm -rf $REPO_ROOT - mkdir -p $REPO_ROOT - - # make a scratch space in the same file system as the repo root - mkdir -p $REPO_ROOT/scratch + export TMP=$DATA/scratch + rm -rf $TMP + mkdir -p $TMP # remove the bash function uenv, if an older version of uenv is installed on # the system @@ -34,8 +25,7 @@ function setup() { } function teardown() { - export REPO_ROOT=/tmp/uenv-repo - #rm -rf $REPO_ROOT + : } @test "noargs" { @@ -159,7 +149,7 @@ function teardown() { @test "repo create" { # using UENV_REPO_PATH env variable - RP=$(mktemp -d $REPO_ROOT/create-XXXXXX) + RP=$(mktemp -d $TMP/create-XXXXXX) run uenv repo create $RP assert_success assert [ -d $RP ] @@ -177,7 +167,7 @@ function teardown() { assert_line --partial "unable to create repository" # try to create a uenv in a read-only path - RP=$REPO_ROOT/ro + RP=$TMP/ro mkdir --mode=-w $RP # run with logging to check for detailed "Permission denied" message run uenv -v repo create $RP/test @@ -223,7 +213,7 @@ function teardown() { @test "image add" { # using UENV_REPO_PATH env variable - RP=$(mktemp -d $REPO_ROOT/create-XXXXXX) + RP=$(mktemp -d $TMP/create-XXXXXX) run uenv repo create $RP assert_success @@ -271,8 +261,8 @@ function teardown() { # test that moving an image into place works # we have to move the image from the same file system as the repo - # so we use a copy in REPO_ROOT/scratch - sqfs_file=$REPO_ROOT/scratch/app43.squashfs + # so we use a copy in TMP/scratch + sqfs_file=$TMP/app43.squashfs cp $SQFS_LIB/apptool/standalone/app43.squashfs $sqfs_file [ -f $sqfs_file ] run uenv --repo=$RP image add --move quokka/24:v1@arapiles%zen3 $sqfs_file @@ -330,9 +320,8 @@ function teardown() { @test "image rm" { # using UENV_REPO_PATH env variable - export UENV_REPO_PATH=$(mktemp -d $REPO_ROOT/create-XXXXXX) - export UENV_REPO_PATH=$REPO_ROOT/xxx - run uenv repo create $RP + export UENV_REPO_PATH=$(mktemp -d $TMP/create-XXXXXX) + run uenv repo create $UENV_REPO_PATH assert_success # add uenv to a repo for us to try removing diff --git a/test/integration/install-bats b/test/integration/install-bats index 6f0c5f6..3072524 100755 --- a/test/integration/install-bats +++ b/test/integration/install-bats @@ -1,26 +1,25 @@ #!/bin/bash -install_path=$(realpath ./install) +# check that exactly one argument is passed +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Set the argument as install_path +install_path=$(realpath $1) # clean up old installation rm -rf $install_path -rm -rf ./bats +rm -rf $install_path/../bats -echo "installation path: $install_path" mkdir -p $install_path curl -L https://github.com/bats-core/bats-core/archive/refs/tags/v1.9.0.tar.gz 2> /dev/null | tar xz mv bats-core-1.9.0 $install_path/bats-core -ln -s $install_path/bats-core/bin/bats ./bats - -echo "installed bats-core: $install_path/bats-core" +rm -rf $install_path/../bats +ln -s $install_path/bats-core/bin/bats $install_path/../bats git clone --depth 1 --quiet https://github.com/bats-core/bats-assert.git $install_path/bats-helpers/bats-assert -echo "installed bats-assert: $install_path/bats-assert" - git clone --depth 1 --quiet https://github.com/bats-core/bats-support.git $install_path/bats-helpers/bats-support -echo "installed bats-support: $install_path/bats-support" - -export BATS_LIB_PATH=$install_path/bats-helpers -echo BATS_LIB_PATH=$BATS_LIB_PATH diff --git a/test/integration/install-setup_suite.sh b/test/integration/install-setup_suite.sh new file mode 100755 index 0000000..b7a04f9 --- /dev/null +++ b/test/integration/install-setup_suite.sh @@ -0,0 +1,9 @@ +#/bin/bash + +src=$1 +dst=$2 +source_path=$3 +build_path=$4 + +cp $src $dst +sed -i -e"s|@build_path@|${build_path}|g" -e"s|@source_path@|${source_path}|g" $dst diff --git a/test/integration/setup_suite.bash b/test/integration/setup_suite.bash deleted file mode 100644 index 06ee153..0000000 --- a/test/integration/setup_suite.bash +++ /dev/null @@ -1,18 +0,0 @@ -# this file is automatically detected by bats -# it performs setup and teardown to run once, before and after respectively, all of the tests are run. - -# create the input data if it has not already been created. -function setup_suite() { - DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" - scratch=$DIR/../scratch - - if [ ! -d $scratch ]; then - $DIR/../setup/setup $scratch - fi - export REPOS=$(realpath ${scratch}/repos) -} - -function teardown_suite() { - : -} - diff --git a/test/integration/setup_suite.bash.in b/test/integration/setup_suite.bash.in new file mode 100644 index 0000000..99af76d --- /dev/null +++ b/test/integration/setup_suite.bash.in @@ -0,0 +1,18 @@ +# this file is automatically detected by bats +# it performs setup and teardown to run once, before and after respectively, all of the tests are run. + +# create the input data if it has not already been created. +function setup_suite() { + test_path="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" + export DATA=$test_path/data + + export BATS_LIB_PATH=$test_path/external/bats-helpers + export REPOS=$DATA/repos + export SQFS_LIB=$DATA/sqfs + export SRC_PATH=@source_path@ + export BUILD_PATH=@build_path@ +} + +function teardown_suite() { + : +} diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 0000000..7e9b808 --- /dev/null +++ b/test/meson.build @@ -0,0 +1,67 @@ +sqlite3 = find_program('sqlite3', required: false) +if not sqlite3.found() + error('sqlite3 must be installed to set up the test framework') +endif + +integration_path = meson.current_source_dir() + '/integration' +setup_path = meson.current_source_dir() + '/setup' + +bats_install_path = meson.current_build_dir() + '/external' +bats = custom_target( + 'bats-install', + output: 'bats', + command: [integration_path + '/install-bats', bats_install_path], + build_by_default: true, + install: false, +) + +test_data_install_path = meson.current_build_dir() + '/data' +test_generated_data = custom_target ( + 'test-generated-data', + output: 'data', + command: [setup_path + '/setup', test_data_install_path], + build_by_default: true, + install: false, +) + +fs = import('fs') +fs.copyfile('integration/cli.bats', 'cli.bats') +fs.copyfile('integration/common.bash', 'common.bash') + +suite_input = integration_path + '/setup_suite.bash.in' +suite_ouput = meson.current_build_dir() + '/setup_suite.bash' +setup_suite = custom_target ( + 'setup-suite', + output: 'setup_suite.bash', + command: [ + integration_path + '/install-setup_suite.sh', + suite_input, + suite_ouput, + meson.source_root(), + meson.build_root(), + ], + build_by_default: true, + install: false, +) + +unit_src = [ + 'unit/dates.cpp', + 'unit/env.cpp', + 'unit/envvars.cpp', + 'unit/fs.cpp', + 'unit/lex.cpp', + 'unit/main.cpp', + 'unit/parse.cpp', + 'unit/signal.cpp', + 'unit/repository.cpp', + 'unit/subprocess.cpp', +] + +unit = executable('unit', + sources: unit_src, + dependencies: [catch_dep, lib_dep], + build_by_default: true, + install: false) + +test('unit', unit, is_parallel : false) +test('cli', bats, args: ['./test/cli.bats'], is_parallel : false) diff --git a/test/setup/setup b/test/setup/setup index abf12e3..d4bd6d2 100755 --- a/test/setup/setup +++ b/test/setup/setup @@ -2,7 +2,14 @@ DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) +if [ "$#" -eq 0 ]; then + install_path="$DIR/../scratch" +else + install_path="$(realpath $1)" +fi + source $DIR/setup_repos.bash -echo "setup_repos $DIR/../scratch" -setup_repos $DIR/../scratch +setup_repos $install_path + +cp -R $DIR/../data/env-files $install_path/ diff --git a/test/setup/setup_repos.bash b/test/setup/setup_repos.bash index 206846c..94b2960 100755 --- a/test/setup/setup_repos.bash +++ b/test/setup/setup_repos.bash @@ -8,10 +8,6 @@ function setup_repo_apptool() { sqfs_path=${scratch}/sqfs/apptool sources=${working}/apptool - echo "repo path ${repo}" - echo "working path $working" - echo "source path $sources" - # clean up previous builds before starting rm -rf ${repo} rm -rf ${sqfs_path} @@ -31,8 +27,6 @@ function setup_repo_apptool() { sha=$(sha256sum ${sqfs} | awk '{print $1}') id=${sha:0:16} - echo ${sha} ${name} - for img_path in "$repo/images/${sha}" "$sqfs_path/$name" do mkdir -p $img_path @@ -58,15 +52,11 @@ function setup_repo_apptool() { } function setup_repos() { - echo "= Setup Repos" scratch=$1 - echo "scratch path $scratch" rm -rf $scratch mkdir -p $scratch scratch=$(realpath $scratch) - echo - echo "== apptool repo" setup_repo_apptool $scratch } diff --git a/test/unit/env.cpp b/test/unit/env.cpp index baac02c..eb96216 100644 --- a/test/unit/env.cpp +++ b/test/unit/env.cpp @@ -4,7 +4,13 @@ #include #include #include +#include TEST_CASE("load_meta", "[env]") { - REQUIRE(uenv::load_meta("../test/data/env-files/cp2k-2024.2-v1.json")); + auto exe = util::exe_path(); + if (!exe) { + SKIP("unable to find path of unit executable"); + } + auto meta_path = exe->parent_path() / "data/env-files/cp2k-2024.2-v1.json"; + REQUIRE(uenv::load_meta(meta_path)); } diff --git a/test/unit/fs.cpp b/test/unit/fs.cpp index 0f02d09..921a86c 100644 --- a/test/unit/fs.cpp +++ b/test/unit/fs.cpp @@ -16,9 +16,17 @@ TEST_CASE("make_temp_dir", "[fs]") { } TEST_CASE("unsquashfs", "[fs]") { - std::string sqfs = "../test/scratch/sqfs/apptool/standalone/app43.squashfs"; + auto exe = util::exe_path(); + + if (!exe) { + SKIP("unable to determine the path of the unit executable"); + } + auto sqfs = + exe->parent_path() / "data/sqfs/apptool/standalone/app43.squashfs"; + if (!fs::is_regular_file(sqfs)) { + SKIP("unable to find the squashfs file for testing"); + } { - REQUIRE(fs::is_regular_file(sqfs)); auto meta = util::unsquashfs_tmp(sqfs, "meta"); REQUIRE(meta); REQUIRE(fs::is_directory(*meta));