diff options
author | Daan De Meyer <daan.j.demeyer@gmail.com> | 2024-12-05 10:47:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-05 10:47:45 +0100 |
commit | 900ac3a76a5770173229f5641506611fbb7c8af7 (patch) | |
tree | 8d9d795496299d05cc274156743e9251a6431690 | |
parent | dmi: add RISC-V 64bit support (diff) | |
parent | ci: Implement coverage on top of mkosi (diff) | |
download | systemd-900ac3a76a5770173229f5641506611fbb7c8af7.tar.xz systemd-900ac3a76a5770173229f5641506611fbb7c8af7.zip |
ci: Implement coverage on top of mkosi (#35407)
32 files changed, 513 insertions, 85 deletions
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000000..73409e53ef --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,145 @@ +--- +# SPDX-License-Identifier: LGPL-2.1-or-later +name: coverage + +on: + schedule: + # Calculate coverage daily at midnight + - cron: '0 0 * * *' + +permissions: + contents: read + +jobs: + coverage: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: systemd/mkosi@07ef37c4c0dad5dfc6cec86c967a7600df1cd88c + + # Freeing up disk space with rm -rf can take multiple minutes. Since we don't need the extra free space + # immediately, we remove the files in the background. However, we first move them to a different location + # so that nothing tries to use anything in these directories anymore while we're busy deleting them. + - name: Free disk space + run: | + sudo mv /usr/local /usr/local.trash + sudo mv /opt/hostedtoolcache /opt/hostedtoolcache.trash + sudo systemd-run rm -rf /usr/local.trash /opt/hostedtoolcache.trash + + - name: Btrfs + run: | + truncate --size=100G btrfs.raw + mkfs.btrfs btrfs.raw + sudo mkdir /mnt/mkosi + LOOP="$(sudo losetup --find --show --direct-io=on btrfs.raw)" + sudo mount "$LOOP" /mnt/mkosi --options compress=zstd:1,user_subvol_rm_allowed,noatime,discard=async,space_cache=v2 + sudo chown "$(id -u):$(id -g)" /mnt/mkosi + mkdir /mnt/mkosi/tmp + echo "TMPDIR=/mnt/mkosi/tmp" >>"$GITHUB_ENV" + ln -s /mnt/mkosi/build build + + - name: Configure + run: | + # XXX: drop after the HyperV bug that breaks secure boot KVM guests is solved + sed -i "s/'firmware'\s*:\s*'auto'/'firmware' : 'uefi'/g" test/*/meson.build + + tee mkosi.local.conf <<EOF + [Distribution] + Distribution=arch + + [Build] + ToolsTree=default + ToolsTreeDistribution=arch + UseSubvolumes=yes + WithTests=no + + WorkspaceDirectory=$TMPDIR + PackageCacheDirectory=$TMPDIR/cache + + Environment= + # Build debuginfo packages since we'll be publishing the packages as artifacts. + WITH_DEBUG=1 + CFLAGS=-Og + MESON_OPTIONS=--werror + COVERAGE=1 + + [Host] + QemuMem=4G + EOF + + - name: Generate secure boot key + run: mkosi --debug genkey + + - name: Show image summary + run: mkosi summary + + - name: Build tools tree + run: mkosi -f sandbox true + + - name: PATH + run: echo "$PATH" + + - name: Configure meson + run: mkosi sandbox meson setup --buildtype=debugoptimized -Dintegration-tests=true build + + - name: Build image + run: sudo --preserve-env mkosi sandbox meson compile -C build mkosi + + - name: Initial coverage report + run: | + mkdir -p build/test/coverage + mkosi sandbox \ + lcov \ + --directory build/mkosi.builddir/arch~rolling~x86-64 \ + --capture \ + --initial \ + --exclude "*.gperf" \ + --output-file build/test/coverage/initial.coverage-info \ + --base-directory src/ \ + --ignore-errors source \ + --no-external \ + --substitute "s#src/src#src#g" + + - name: Run integration tests + run: | + sudo --preserve-env \ + mkosi sandbox \ + meson test \ + -C build \ + --no-rebuild \ + --suite integration-tests \ + --print-errorlogs \ + --no-stdsplit \ + --num-processes "$(($(nproc) - 1))" \ + --timeout-multiplier 2 \ + --max-lines 300 + + - name: Archive failed test journals + uses: actions/upload-artifact@v4 + if: failure() && (github.repository == 'systemd/systemd' || github.repository == 'systemd/systemd-stable') + with: + name: ci-coverage-${{ github.run_id }}-${{ github.run_attempt }}-arch-rolling-failed-test-journals + path: | + build/test/journal/*.journal + build/meson-logs/* + retention-days: 7 + + - name: Combine coverage reports + run: | + lcov_args=() + + while read -r file; do + lcov_args+=(--add-tracefile "${file}") + done < <(find build/test/coverage -name "TEST-*.coverage-info") + + mkosi sandbox lcov --ignore-errors inconsistent,inconsistent "${lcov_args[@]}" --output-file build/test/coverage/everything.coverage-info + + - name: List coverage report + run: mkosi sandbox lcov --ignore-errors inconsistent,inconsistent --list build/test/coverage/everything.coverage-info + + - name: Coveralls + uses: coverallsapp/github-action@cfd0633edbd2411b532b808ba7a8b5e04f76d2c8 + if: github.repository == 'systemd/systemd' || github.repository == 'systemd/systemd-stable' + with: + file: build/test/coverage/everything.coverage-info diff --git a/.github/workflows/mkosi.yml b/.github/workflows/mkosi.yml index 9e20a63179..156b8bae89 100644 --- a/.github/workflows/mkosi.yml +++ b/.github/workflows/mkosi.yml @@ -105,7 +105,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: systemd/mkosi@0825cca8084674ec8fa27502134b1bc601f79e0c + - uses: systemd/mkosi@07ef37c4c0dad5dfc6cec86c967a7600df1cd88c # Freeing up disk space with rm -rf can take multiple minutes. Since we don't need the extra free space # immediately, we remove the files in the background. However, we first move them to a different location diff --git a/mkosi.conf b/mkosi.conf index 835b1d4b9c..35a19a27aa 100644 --- a/mkosi.conf +++ b/mkosi.conf @@ -22,6 +22,7 @@ PassEnvironment= SYSEXT WITH_DEBUG ASAN_OPTIONS + COVERAGE [Output] RepartDirectories=mkosi.repart @@ -150,3 +151,4 @@ QemuKvm=yes [Include] Include=%D/mkosi.sanitizers + %D/mkosi.coverage diff --git a/mkosi.conf.d/05-tools/mkosi.conf b/mkosi.conf.d/05-tools/mkosi.conf index 746dd37870..15c336a304 100644 --- a/mkosi.conf.d/05-tools/mkosi.conf +++ b/mkosi.conf.d/05-tools/mkosi.conf @@ -4,5 +4,8 @@ ToolsTreePackages= gcc gperf + lcov + llvm meson pkgconf + rsync diff --git a/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf b/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf index 7aba50248a..5787aa8f44 100644 --- a/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf +++ b/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf @@ -10,6 +10,7 @@ ToolsTreePackages= libcap libmicrohttpd mypy + perl-json-xs python-jinja python-pytest ruff diff --git a/mkosi.coverage/mkosi.conf b/mkosi.coverage/mkosi.conf new file mode 100644 index 0000000000..a9224195b4 --- /dev/null +++ b/mkosi.coverage/mkosi.conf @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +[Match] +Environment=COVERAGE=1 + +[Content] +KernelCommandLine= + COVERAGE_BUILD_DIR=/coverage + systemd.setenv=COVERAGE_BUILD_DIR=/coverage diff --git a/mkosi.coverage/mkosi.postinst b/mkosi.coverage/mkosi.postinst new file mode 100755 index 0000000000..ccb153f76d --- /dev/null +++ b/mkosi.coverage/mkosi.postinst @@ -0,0 +1,56 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -e + +( + shopt -s nullglob + rm -f "$BUILDROOT"/coverage/*.gcda +) + +# When using -fprofile-dir=, GCC creates all gcda files under the given directory at the same location as the +# gcno file in the build directory, but with each '/' replaced with '#'. LLVM creates each gcda file under +# the given directory without replacing each '/' with '#'. Because we want all processes to be able to write +# gcda files under /coverage regardless of which user they are running as, we pre-create all files under +# /coverage and make them world readable and writable so that we don't have to mess with umasks for each +# process that writes to /coverage. +if ((LLVM)); then + rsync --recursive --include='*/' --exclude='*' --relative "$BUILDDIR" "$BUILDROOT/coverage" + find "$BUILDDIR" -name '*.gcno' | sed 's/gcno/gcda/' | xargs -I '{}' touch "$BUILDROOT/coverage/{}" +else + find "$BUILDDIR" -name '*.gcno' | sed 's/gcno/gcda/' | sed 's/\//#/g' | xargs -I '{}' touch "$BUILDROOT/coverage/{}" +fi + +chmod --recursive 777 "$BUILDROOT/coverage" + +# When built with gcov, disable ProtectSystem= and ProtectHome= in the test images, since it prevents gcov to +# write the coverage reports (*.gcda files). +mkdir -p "$BUILDROOT/usr/lib/systemd/system/service.d/" +cat >"$BUILDROOT/usr/lib/systemd/system/service.d/99-gcov-override.conf" <<EOF +[Service] +ProtectSystem=no +ProtectHome=no +EOF + +# Similarly, set ReadWritePaths= to the coverage directory in the test image to make the coverage work with +# units using DynamicUser=yes. Do this only for services with test- prefix and a couple of known-to-use +# DynamicUser=yes services, as setting this system-wide has many undesirable side-effects, as it creates its +# own namespace. +for service in capsule@ test- systemd-journal-{gatewayd,upload}; do + mkdir -p "$BUILDROOT/usr/lib/systemd/system/$service.service.d/" + cat >"$BUILDROOT/usr/lib/systemd/system/$service.service.d/99-gcov-rwpaths-override.conf" <<EOF +[Service] +ReadWritePaths=/coverage +EOF +done + +# Ditto, but for the user daemon. +mkdir -p "$BUILDROOT/usr/lib/systemd/user/test-.service.d/" +cat >"$BUILDROOT/usr/lib/systemd/user/test-.service.d/99-gcov-rwpaths-override.conf" <<EOF +[Service] +ReadWritePaths=/coverage +EOF + +# Bind the coverage directory into nspawn containers that are executed using machinectl. Unfortunately, the +# .nspawn files don't support drop-ins so we have to inject the bind mount directly into the +# systemd-nspawn@.service unit. +sed -ri "s/^ExecStart=.+$/& --bind=\/coverage/" "$BUILDROOT/usr/lib/systemd/system/systemd-nspawn@.service" diff --git a/mkosi.extra.common/usr/lib/systemd/coverage-forwarder b/mkosi.extra.common/usr/lib/systemd/coverage-forwarder new file mode 100755 index 0000000000..e6c7a88a4d --- /dev/null +++ b/mkosi.extra.common/usr/lib/systemd/coverage-forwarder @@ -0,0 +1,9 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +logger --journald <<EOF +MESSAGE=Tarball with coverage data from /coverage +COVERAGE_TAR=$(tar --create --file - --directory /coverage --zstd . | base64 --wrap=0) +EOF + +journalctl --flush diff --git a/mkosi.extra.common/usr/lib/systemd/system-preset/00-mkosi.preset b/mkosi.extra.common/usr/lib/systemd/system-preset/00-mkosi.preset index 5a15e6bcbb..269692b646 100644 --- a/mkosi.extra.common/usr/lib/systemd/system-preset/00-mkosi.preset +++ b/mkosi.extra.common/usr/lib/systemd/system-preset/00-mkosi.preset @@ -39,3 +39,5 @@ disable iscsiuio.socket # mkosi relabels the image itself so no need to do it on boot. disable selinux-autorelabel-mark.service + +enable coverage-forwarder.service diff --git a/mkosi.extra.common/usr/lib/systemd/system/coverage-forwarder.service b/mkosi.extra.common/usr/lib/systemd/system/coverage-forwarder.service new file mode 100644 index 0000000000..b332f7ec58 --- /dev/null +++ b/mkosi.extra.common/usr/lib/systemd/system/coverage-forwarder.service @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +[Unit] +Description=Forward coverage data to the journal before shutting down +ConditionEnvironment=COVERAGE_BUILD_DIR + +DefaultDependencies=no +After=systemd-journald.socket +Requires=systemd-journald.socket +After=shutdown.target initrd-switch-root.target +Before=final.target initrd-switch-root.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/coverage-forwarder + +[Install] +WantedBy=final.target initrd-switch-root.target diff --git a/mkosi.images/build/mkosi.conf.d/arch/mkosi.build.chroot b/mkosi.images/build/mkosi.conf.d/arch/mkosi.build.chroot index 6c66888afe..83c4960ac8 100755 --- a/mkosi.images/build/mkosi.conf.d/arch/mkosi.build.chroot +++ b/mkosi.images/build/mkosi.conf.d/arch/mkosi.build.chroot @@ -32,6 +32,10 @@ MKOSI_MESON_OPTIONS="-D mode=developer -D b_sanitize=${SANITIZERS:-none}" if ((WIPE)) && [[ -d "$BUILDDIR/meson-private" ]]; then MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS --wipe" fi +if ((COVERAGE)); then + MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS -D b_coverage=true" + MKOSI_CFLAGS="$MKOSI_CFLAGS -fprofile-dir=/coverage" +fi # Override the default options. We specifically disable "strip", "zipman" and "lto" as they slow down builds # significantly. OPTIONS= cannot be overridden on the makepkg command line so we append to /etc/makepkg.conf diff --git a/mkosi.images/build/mkosi.conf.d/centos-fedora/mkosi.build.chroot b/mkosi.images/build/mkosi.conf.d/centos-fedora/mkosi.build.chroot index 1c019e162c..1de1578e20 100755 --- a/mkosi.images/build/mkosi.conf.d/centos-fedora/mkosi.build.chroot +++ b/mkosi.images/build/mkosi.conf.d/centos-fedora/mkosi.build.chroot @@ -52,6 +52,10 @@ MKOSI_MESON_OPTIONS="-D mode=developer -D b_sanitize=${SANITIZERS:-none}" if ((WIPE)) && [[ -d "$BUILDDIR/meson-private" ]]; then MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS --wipe" fi +if ((COVERAGE)); then + MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS -D b_coverage=true" + MKOSI_CFLAGS="$MKOSI_CFLAGS -fprofile-dir=/coverage" +fi ( shopt -s nullglob diff --git a/mkosi.images/build/mkosi.conf.d/debian-ubuntu/mkosi.build.chroot b/mkosi.images/build/mkosi.conf.d/debian-ubuntu/mkosi.build.chroot index 45b9bd06af..5f3e53ff53 100755 --- a/mkosi.images/build/mkosi.conf.d/debian-ubuntu/mkosi.build.chroot +++ b/mkosi.images/build/mkosi.conf.d/debian-ubuntu/mkosi.build.chroot @@ -48,6 +48,10 @@ MKOSI_MESON_OPTIONS="-D mode=developer -D b_sanitize=${SANITIZERS:-none}" if ((WIPE)) && [[ -d "$BUILDDIR/meson-private" ]]; then MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS --wipe" fi +if ((COVERAGE)); then + MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS -D b_coverage=true" + MKOSI_CFLAGS="$MKOSI_CFLAGS -fprofile-dir=/coverage" +fi # TODO: Drop GENSYMBOLS_LEVEL once https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=986746 is fixed. build() { diff --git a/mkosi.images/build/mkosi.conf.d/opensuse/mkosi.build.chroot b/mkosi.images/build/mkosi.conf.d/opensuse/mkosi.build.chroot index 6c1cf2aed4..7349038638 100755 --- a/mkosi.images/build/mkosi.conf.d/opensuse/mkosi.build.chroot +++ b/mkosi.images/build/mkosi.conf.d/opensuse/mkosi.build.chroot @@ -52,6 +52,10 @@ MKOSI_MESON_OPTIONS="-D mode=developer -D b_sanitize=${SANITIZERS:-none}" if ((WIPE)) && [[ -d "$BUILDDIR/meson-private" ]]; then MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS --wipe" fi +if ((COVERAGE)); then + MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS -D b_coverage=true" + MKOSI_CFLAGS="$MKOSI_CFLAGS -fprofile-dir=/coverage" +fi # TODO: Drop when the spec is fixed (either the patch is adapted or not applied when building for upstream). sed --in-place '/0009-pid1-handle-console-specificities-weirdness-for-s390.patch/d' "pkg/$PKG_SUBDIR/systemd.spec" diff --git a/mkosi.images/initrd/mkosi.conf b/mkosi.images/initrd/mkosi.conf index b76b47ecda..ac66dd933b 100644 --- a/mkosi.images/initrd/mkosi.conf +++ b/mkosi.images/initrd/mkosi.conf @@ -4,6 +4,7 @@ Include= mkosi-initrd %D/mkosi.sanitizers + %D/mkosi.coverage [Content] ExtraTrees=%D/mkosi.extra.common @@ -12,3 +13,4 @@ Packages= findutils grep sed + tar diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 762dfd675a..6ee18838cc 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -853,7 +853,8 @@ int encrypt_credential_and_warn( CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) { if (!uid_is_valid(uid)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Scoped credential selected, but no UID specified."); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Scoped credential key type "SD_ID128_FORMAT_STR" selected, but no UID specified.", SD_ID128_FORMAT_VAL(with_key)); } else uid = UID_INVALID; diff --git a/src/test/test-creds.c b/src/test/test-creds.c index cc9cc73778..e82c8fd755 100644 --- a/src/test/test-creds.c +++ b/src/test/test-creds.c @@ -20,104 +20,104 @@ TEST(read_credential_strings) { const char *e = getenv("CREDENTIALS_DIRECTORY"); if (e) - assert_se(saved = strdup(e)); + ASSERT_NOT_NULL(saved = strdup(e)); - assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0); + ASSERT_OK_ZERO(read_credential_strings_many("foo", &x, "bar", &y)); ASSERT_NULL(x); ASSERT_NULL(y); - assert_se(mkdtemp_malloc(NULL, &tmp) >= 0); + ASSERT_OK(mkdtemp_malloc(NULL, &tmp)); - assert_se(setenv("CREDENTIALS_DIRECTORY", tmp, /* override= */ true) >= 0); + ASSERT_OK_ERRNO(setenv("CREDENTIALS_DIRECTORY", tmp, /* override= */ true)); - assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0); + ASSERT_OK_ZERO(read_credential_strings_many("foo", &x, "bar", &y)); ASSERT_NULL(x); ASSERT_NULL(y); - assert_se(p = path_join(tmp, "bar")); - assert_se(write_string_file(p, "piff", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_AVOID_NEWLINE) >= 0); + ASSERT_NOT_NULL(p = path_join(tmp, "bar")); + ASSERT_OK(write_string_file(p, "piff", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_AVOID_NEWLINE)); - assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0); + ASSERT_OK_ZERO(read_credential_strings_many("foo", &x, "bar", &y)); ASSERT_NULL(x); ASSERT_STREQ(y, "piff"); - assert_se(write_string_file(p, "paff", WRITE_STRING_FILE_TRUNCATE|WRITE_STRING_FILE_AVOID_NEWLINE) >= 0); + ASSERT_OK(write_string_file(p, "paff", WRITE_STRING_FILE_TRUNCATE|WRITE_STRING_FILE_AVOID_NEWLINE)); - assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0); + ASSERT_OK_ZERO(read_credential_strings_many("foo", &x, "bar", &y)); ASSERT_NULL(x); ASSERT_STREQ(y, "paff"); p = mfree(p); - assert_se(p = path_join(tmp, "foo")); - assert_se(write_string_file(p, "knurz", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_AVOID_NEWLINE) >= 0); + ASSERT_NOT_NULL(p = path_join(tmp, "foo")); + ASSERT_OK(write_string_file(p, "knurz", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_AVOID_NEWLINE)); - assert_se(read_credential_strings_many("foo", &x, "bar", &y) >= 0); + ASSERT_OK(read_credential_strings_many("foo", &x, "bar", &y)); ASSERT_STREQ(x, "knurz"); ASSERT_STREQ(y, "paff"); p = mfree(p); - assert_se(p = path_join(tmp, "bazz")); - assert_se(f = fopen(p, "w")); - assert_se(fwrite("x\0y", 1, 3, f) == 3); /* embedded NUL byte should result in EBADMSG when reading back with read_credential_strings_many() */ + ASSERT_NOT_NULL(p = path_join(tmp, "bazz")); + ASSERT_NOT_NULL(f = fopen(p, "w")); + ASSERT_EQ(fwrite("x\0y", 1, 3, f), 3UL); /* embedded NUL byte should result in EBADMSG when reading back with read_credential_strings_many() */ f = safe_fclose(f); y = mfree(y); - assert_se(read_credential_strings_many("bazz", &x, "bar", &y) == -EBADMSG); + ASSERT_ERROR(read_credential_strings_many("bazz", &x, "bar", &y), EBADMSG); ASSERT_STREQ(x, "knurz"); ASSERT_STREQ(y, "paff"); if (saved) - assert_se(setenv("CREDENTIALS_DIRECTORY", saved, /* override= */ 1) >= 0); + ASSERT_OK_ERRNO(setenv("CREDENTIALS_DIRECTORY", saved, /* override= */ 1)); else - assert_se(unsetenv("CREDENTIALS_DIRECTORY") >= 0); + ASSERT_OK_ERRNO(unsetenv("CREDENTIALS_DIRECTORY")); } TEST(credential_name_valid) { char buf[NAME_MAX+2]; - assert_se(!credential_name_valid(NULL)); - assert_se(!credential_name_valid("")); - assert_se(!credential_name_valid(".")); - assert_se(!credential_name_valid("..")); - assert_se(!credential_name_valid("foo/bar")); - assert_se(credential_name_valid("foo")); + ASSERT_FALSE(credential_name_valid(NULL)); + ASSERT_FALSE(credential_name_valid("")); + ASSERT_FALSE(credential_name_valid(".")); + ASSERT_FALSE(credential_name_valid("..")); + ASSERT_FALSE(credential_name_valid("foo/bar")); + ASSERT_TRUE(credential_name_valid("foo")); memset(buf, 'x', sizeof(buf)-1); buf[sizeof(buf)-1] = 0; - assert_se(!credential_name_valid(buf)); + ASSERT_FALSE(credential_name_valid(buf)); buf[sizeof(buf)-2] = 0; - assert_se(credential_name_valid(buf)); + ASSERT_TRUE(credential_name_valid(buf)); } TEST(credential_glob_valid) { char buf[NAME_MAX+2]; - assert_se(!credential_glob_valid(NULL)); - assert_se(!credential_glob_valid("")); - assert_se(!credential_glob_valid(".")); - assert_se(!credential_glob_valid("..")); - assert_se(!credential_glob_valid("foo/bar")); - assert_se(credential_glob_valid("foo")); - assert_se(credential_glob_valid("foo*")); - assert_se(credential_glob_valid("x*")); - assert_se(credential_glob_valid("*")); - assert_se(!credential_glob_valid("?")); - assert_se(!credential_glob_valid("*a")); - assert_se(!credential_glob_valid("a?")); - assert_se(!credential_glob_valid("a[abc]")); - assert_se(!credential_glob_valid("a[abc]")); + ASSERT_FALSE(credential_glob_valid(NULL)); + ASSERT_FALSE(credential_glob_valid("")); + ASSERT_FALSE(credential_glob_valid(".")); + ASSERT_FALSE(credential_glob_valid("..")); + ASSERT_FALSE(credential_glob_valid("foo/bar")); + ASSERT_TRUE(credential_glob_valid("foo")); + ASSERT_TRUE(credential_glob_valid("foo*")); + ASSERT_TRUE(credential_glob_valid("x*")); + ASSERT_TRUE(credential_glob_valid("*")); + ASSERT_FALSE(credential_glob_valid("?")); + ASSERT_FALSE(credential_glob_valid("*a")); + ASSERT_FALSE(credential_glob_valid("a?")); + ASSERT_FALSE(credential_glob_valid("a[abc]")); + ASSERT_FALSE(credential_glob_valid("a[abc]")); memset(buf, 'x', sizeof(buf)-1); buf[sizeof(buf)-1] = 0; - assert_se(!credential_glob_valid(buf)); + ASSERT_FALSE(credential_glob_valid(buf)); buf[sizeof(buf)-2] = 0; - assert_se(credential_glob_valid(buf)); + ASSERT_TRUE(credential_glob_valid(buf)); buf[sizeof(buf)-2] = '*'; - assert_se(credential_glob_valid(buf)); + ASSERT_TRUE(credential_glob_valid(buf)); } static void test_encrypt_decrypt_with(sd_id128_t mode, uid_t uid) { @@ -152,7 +152,7 @@ static void test_encrypt_decrypt_with(sd_id128_t mode, uid_t uid) { return; } - assert_se(r >= 0); + ASSERT_OK(r); _cleanup_(iovec_done) struct iovec decrypted = {}; r = decrypt_credential_and_warn( @@ -164,7 +164,7 @@ static void test_encrypt_decrypt_with(sd_id128_t mode, uid_t uid) { &encrypted, CREDENTIAL_ALLOW_NULL, &decrypted); - assert_se(r == -EREMOTE); /* name didn't match */ + ASSERT_ERROR(r, EREMOTE); /* name didn't match */ r = decrypt_credential_and_warn( "foo", @@ -175,9 +175,9 @@ static void test_encrypt_decrypt_with(sd_id128_t mode, uid_t uid) { &encrypted, CREDENTIAL_ALLOW_NULL, &decrypted); - assert_se(r >= 0); + ASSERT_OK(r); - assert_se(iovec_memcmp(&plaintext, &decrypted) == 0); + ASSERT_EQ(iovec_memcmp(&plaintext, &decrypted), 0); } static bool try_tpm2(void) { @@ -203,17 +203,17 @@ TEST(credential_encrypt_decrypt) { test_encrypt_decrypt_with(CRED_AES256_GCM_BY_NULL, UID_INVALID); - assert_se(mkdtemp_malloc(NULL, &d) >= 0); + ASSERT_OK(mkdtemp_malloc(NULL, &d)); j = path_join(d, "secret"); - assert_se(j); + ASSERT_NOT_NULL(j); const char *e = getenv("SYSTEMD_CREDENTIAL_SECRET"); _cleanup_free_ char *ec = NULL; if (e) - assert_se(ec = strdup(e)); + ASSERT_NOT_NULL(ec = strdup(e)); - assert_se(setenv("SYSTEMD_CREDENTIAL_SECRET", j, true) >= 0); + ASSERT_OK_ERRNO(setenv("SYSTEMD_CREDENTIAL_SECRET", j, true)); test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST, UID_INVALID); test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST_SCOPED, 0); @@ -225,7 +225,7 @@ TEST(credential_encrypt_decrypt) { } if (ec) - assert_se(setenv("SYSTEMD_CREDENTIAL_SECRET", ec, true) >= 0); + ASSERT_OK_ERRNO(setenv("SYSTEMD_CREDENTIAL_SECRET", ec, true)); } TEST(mime_type_matches) { @@ -246,10 +246,10 @@ TEST(mime_type_matches) { FOREACH_ELEMENT(t, tags) { _cleanup_free_ char *encoded = NULL; - assert_se(base64mem(t, sizeof(sd_id128_t), &encoded) >= 0); + ASSERT_OK(base64mem(t, sizeof(sd_id128_t), &encoded)); /* Validate that the size matches expectations for the 4/3 factor size increase (rounding up) */ - assert_se(strlen(encoded) == DIV_ROUND_UP((128U / 8U), 3U) * 4U); + ASSERT_EQ(strlen(encoded), DIV_ROUND_UP((128U / 8U), 3U) * 4U); /* Cut off rounded string where the ID ends, but now round down to get rid of characters that might contain follow-up data */ encoded[128 / 6] = 0; diff --git a/src/test/test-execute.c b/src/test/test-execute.c index 95ccf5490d..de575ec1e6 100644 --- a/src/test/test-execute.c +++ b/src/test/test-execute.c @@ -1448,8 +1448,10 @@ static int prepare_ns(const char *process_name) { _cleanup_free_ char *unit_dir = NULL, *build_dir = NULL, *build_dir_mount = NULL; int ret; - /* Make "/" read-only. */ - ASSERT_OK(mount_nofollow_verbose(LOG_DEBUG, NULL, "/", NULL, MS_BIND|MS_REMOUNT|MS_RDONLY, NULL)); + const char *coverage = getenv("COVERAGE_BUILD_DIR"); + if (!coverage) + /* Make "/" read-only. */ + ASSERT_OK(mount_nofollow_verbose(LOG_DEBUG, NULL, "/", NULL, MS_BIND|MS_REMOUNT|MS_RDONLY, NULL)); /* Creating a new user namespace in the above means all MS_SHARED mounts become MS_SLAVE. * Let's put them back to MS_SHARED here, since that's what we want as defaults. (This will diff --git a/test/integration-test-wrapper.py b/test/integration-test-wrapper.py index a3f90dc9fa..09dcda92e1 100755 --- a/test/integration-test-wrapper.py +++ b/test/integration-test-wrapper.py @@ -4,12 +4,15 @@ """Test wrapper command for driving integration tests.""" import argparse +import base64 +import dataclasses import json import os import re import shlex import subprocess import sys +import tempfile import textwrap from pathlib import Path @@ -33,6 +36,47 @@ ExecStart=false """ +def sandbox(args: argparse.Namespace) -> list[str]: + return [ + args.mkosi, + '--directory', os.fspath(args.meson_source_dir), + '--extra-search-path', os.fspath(args.meson_build_dir), + 'sandbox', + ] # fmt: skip + + +@dataclasses.dataclass(frozen=True) +class Summary: + distribution: str + release: str + architecture: str + builddir: Path + environment: dict[str, str] + + @classmethod + def get(cls, args: argparse.Namespace) -> 'Summary': + j = json.loads( + subprocess.run( + [ + args.mkosi, + '--directory', os.fspath(args.meson_source_dir), + '--json', + 'summary', + ], + stdout=subprocess.PIPE, + text=True, + ).stdout + ) # fmt: skip + + return Summary( + distribution=j['Images'][-1]['Distribution'], + release=j['Images'][-1]['Release'], + architecture=j['Images'][-1]['Architecture'], + builddir=Path(j['Images'][-1]['BuildDirectory']), + environment=j['Images'][-1]['Environment'], + ) + + def process_coredumps(args: argparse.Namespace, journal_file: Path) -> bool: # Collect executable paths of all coredumps and filter out the expected ones. @@ -42,11 +86,7 @@ def process_coredumps(args: argparse.Namespace, journal_file: Path) -> bool: exclude_regex = None result = subprocess.run( - [ - args.mkosi, - '--directory', os.fspath(args.meson_source_dir), - '--extra-search-path', os.fspath(args.meson_build_dir), - 'sandbox', + sandbox(args) + [ 'coredumpctl', '--file', journal_file, '--json=short', @@ -69,11 +109,7 @@ def process_coredumps(args: argparse.Namespace, journal_file: Path) -> bool: return False subprocess.run( - [ - args.mkosi, - '--directory', os.fspath(args.meson_source_dir), - '--extra-search-path', os.fspath(args.meson_build_dir), - 'sandbox', + sandbox(args) + [ 'coredumpctl', '--file', journal_file, '--no-pager', @@ -86,6 +122,119 @@ def process_coredumps(args: argparse.Namespace, journal_file: Path) -> bool: return True +def process_coverage(args: argparse.Namespace, summary: Summary, name: str, journal_file: Path) -> None: + coverage = subprocess.run( + sandbox(args) + [ + 'journalctl', + '--file', journal_file, + '--field=COVERAGE_TAR', + ], + stdout=subprocess.PIPE, + text=True, + check=True, + ).stdout # fmt: skip + + (args.meson_build_dir / 'test/coverage').mkdir(exist_ok=True) + + initial = args.meson_build_dir / 'test/coverage/initial.coverage-info' + output = args.meson_build_dir / f'test/coverage/{name}.coverage-info' + + for b64 in coverage.splitlines(): + tarball = base64.b64decode(b64) + + with tempfile.TemporaryDirectory(prefix='coverage-') as tmp: + subprocess.run( + sandbox(args) + [ + 'tar', + '--extract', + '--file', '-', + '--directory', tmp, + '--keep-directory-symlink', + '--no-overwrite-dir', + '--zstd', + ], + input=tarball, + check=True, + ) # fmt: skip + + for p in Path(tmp).iterdir(): + if not p.name.startswith('#'): + continue + + dst = Path(tmp) / p.name.replace('#', '/').lstrip('/') + dst.parent.mkdir(parents=True, exist_ok=True) + p.rename(dst) + + subprocess.run( + sandbox(args) + [ + 'find', + tmp, + '-name', '*.gcda', + '-size', '0', + '-delete', + ], + input=tarball, + check=True, + ) # fmt: skip + + subprocess.run( + sandbox(args) + + [ + 'rsync', + '--archive', + '--prune-empty-dirs', + '--include=*/', + '--include=*.gcno', + '--exclude=*', + f'{os.fspath(args.meson_build_dir / summary.builddir)}/', + os.fspath(Path(tmp) / 'work/build'), + ], + check=True, + ) + + subprocess.run( + sandbox(args) + + [ + 'lcov', + *( + [ + '--gcov-tool', 'llvm-cov', + '--gcov-tool', 'gcov', + ] + if summary.environment.get('LLVM', '0') == '1' + else [] + ), + '--directory', tmp, + '--base-directory', 'src/', + '--capture', + '--exclude', '*.gperf', + '--output-file', f'{output}.new', + '--ignore-errors', 'inconsistent,inconsistent,source,negative', + '--substitute', 's#src/src#src#g', + '--no-external', + '--quiet', + ], + check=True, + ) # fmt: skip + + subprocess.run( + sandbox(args) + + [ + 'lcov', + '--ignore-errors', 'inconsistent,inconsistent,format,corrupt,empty', + '--add-tracefile', output if output.exists() else initial, + '--add-tracefile', f'{output}.new', + '--output-file', output, + '--quiet', + ], + check=True, + ) # fmt: skip + + Path(f'{output}.new').unlink() + + print(f'Wrote coverage report for {name} to {output}', file=sys.stderr) + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--mkosi', required=True) @@ -127,6 +276,7 @@ def main() -> None: keep_journal = os.getenv('TEST_SAVE_JOURNAL', 'fail') shell = bool(int(os.getenv('TEST_SHELL', '0'))) + summary = Summary.get(args) if shell and not sys.stderr.isatty(): print( @@ -250,6 +400,13 @@ def main() -> None: coredumps = process_coredumps(args, journal_file) + if ( + summary.environment.get('COVERAGE', '0') == '1' + and result.returncode in (args.exit_code, 77) + and not coredumps + ): + process_coverage(args, summary, name, journal_file) + if keep_journal == '0' or ( keep_journal == 'fail' and result.returncode in (args.exit_code, 77) and not coredumps ): @@ -262,22 +419,11 @@ def main() -> None: if os.getenv('GITHUB_ACTIONS'): id = os.environ['GITHUB_RUN_ID'] + workflow = os.environ['GITHUB_WORKFLOW'] iteration = os.environ['GITHUB_RUN_ATTEMPT'] - j = json.loads( - subprocess.run( - [ - args.mkosi, - '--directory', os.fspath(args.meson_source_dir), - '--json', - 'summary', - ], - stdout=subprocess.PIPE, - text=True, - ).stdout - ) # fmt: skip - distribution = j['Images'][-1]['Distribution'] - release = j['Images'][-1]['Release'] - artifact = f'ci-mkosi-{id}-{iteration}-{distribution}-{release}-failed-test-journals' + artifact = ( + f'ci-{workflow}-{id}-{iteration}-{summary.distribution}-{summary.release}-failed-test-journals' + ) ops += [f'gh run download {id} --name {artifact} -D ci/{artifact}'] journal_file = Path(f'ci/{artifact}/test/journal/{name}.journal') diff --git a/test/test-execute/exec-ambientcapabilities-dynuser.service b/test/test-execute/exec-ambientcapabilities-dynuser.service index ab815f39a3..b927c7dbca 100644 --- a/test/test-execute/exec-ambientcapabilities-dynuser.service +++ b/test/test-execute/exec-ambientcapabilities-dynuser.service @@ -9,3 +9,4 @@ AmbientCapabilities=CAP_CHOWN CAP_SETUID CAP_NET_RAW DynamicUser=yes PrivateUsers=yes EnvironmentFile=-/usr/lib/systemd/systemd-asan-env +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-fixeduser-adm.service b/test/test-execute/exec-dynamicuser-fixeduser-adm.service index 1b7f232cd1..3a7f8aef60 100644 --- a/test/test-execute/exec-dynamicuser-fixeduser-adm.service +++ b/test/test-execute/exec-dynamicuser-fixeduser-adm.service @@ -10,3 +10,4 @@ ExecStart=sh -x -c 'test "$$(id -nG)" = "adm" && test "$$(id -ng)" = "adm" && te ExecStart=sh -x -c 'test "$$(id -nG)" = "adm" && test "$$(id -ng)" = "adm" && test "$$(id -nu)" = "adm"' DynamicUser=yes User=adm +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-fixeduser-games.service b/test/test-execute/exec-dynamicuser-fixeduser-games.service index b13c23a74d..40048d27a8 100644 --- a/test/test-execute/exec-dynamicuser-fixeduser-games.service +++ b/test/test-execute/exec-dynamicuser-fixeduser-games.service @@ -10,3 +10,4 @@ ExecStart=sh -x -c 'test "$$(id -nG)" = "games" && test "$$(id -ng)" = "games" & ExecStart=sh -x -c 'test "$$(id -nG)" = "games" && test "$$(id -ng)" = "games" && test "$$(id -nu)" = "games"' DynamicUser=yes User=games +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-fixeduser-one-supplementarygroup.service b/test/test-execute/exec-dynamicuser-fixeduser-one-supplementarygroup.service index e494c33551..e58b524033 100644 --- a/test/test-execute/exec-dynamicuser-fixeduser-one-supplementarygroup.service +++ b/test/test-execute/exec-dynamicuser-fixeduser-one-supplementarygroup.service @@ -9,3 +9,4 @@ Type=oneshot User=1 DynamicUser=yes SupplementaryGroups=1 +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-fixeduser.service b/test/test-execute/exec-dynamicuser-fixeduser.service index 4ebfc20cde..8e5244d891 100644 --- a/test/test-execute/exec-dynamicuser-fixeduser.service +++ b/test/test-execute/exec-dynamicuser-fixeduser.service @@ -8,3 +8,4 @@ ExecStart=sh -x -c 'test "$$(id -g)" = "1" && test "$$(id -u)" = "1"' Type=oneshot User=1 DynamicUser=yes +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-runtimedirectory1.service b/test/test-execute/exec-dynamicuser-runtimedirectory1.service index 59d3bf0884..671b316736 100644 --- a/test/test-execute/exec-dynamicuser-runtimedirectory1.service +++ b/test/test-execute/exec-dynamicuser-runtimedirectory1.service @@ -11,3 +11,4 @@ RuntimeDirectory=test-exec_runtimedirectorypreserve RuntimeDirectoryPreserve=yes DynamicUser=yes EnvironmentFile=-/usr/lib/systemd/systemd-asan-env +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-runtimedirectory2.service b/test/test-execute/exec-dynamicuser-runtimedirectory2.service index 6ff9d7503a..cdb80848e3 100644 --- a/test/test-execute/exec-dynamicuser-runtimedirectory2.service +++ b/test/test-execute/exec-dynamicuser-runtimedirectory2.service @@ -12,3 +12,4 @@ RuntimeDirectory=test-exec_runtimedirectorypreserve RuntimeDirectoryPreserve=yes DynamicUser=yes EnvironmentFile=-/usr/lib/systemd/systemd-asan-env +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-runtimedirectory3.service b/test/test-execute/exec-dynamicuser-runtimedirectory3.service index cebb819476..51a9e44c6f 100644 --- a/test/test-execute/exec-dynamicuser-runtimedirectory3.service +++ b/test/test-execute/exec-dynamicuser-runtimedirectory3.service @@ -11,3 +11,4 @@ Type=oneshot RuntimeDirectory=test-exec_runtimedirectorypreserve DynamicUser=yes EnvironmentFile=-/usr/lib/systemd/systemd-asan-env +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-statedir-migrate-step2.service b/test/test-execute/exec-dynamicuser-statedir-migrate-step2.service index 7261f4a174..f22862378c 100644 --- a/test/test-execute/exec-dynamicuser-statedir-migrate-step2.service +++ b/test/test-execute/exec-dynamicuser-statedir-migrate-step2.service @@ -25,3 +25,4 @@ Type=oneshot DynamicUser=yes StateDirectory=test-dynamicuser-migrate test-dynamicuser-migrate2/hoge EnvironmentFile=-/usr/lib/systemd/systemd-asan-env +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-statedir.service b/test/test-execute/exec-dynamicuser-statedir.service index 636a70259c..1e4fe818ac 100644 --- a/test/test-execute/exec-dynamicuser-statedir.service +++ b/test/test-execute/exec-dynamicuser-statedir.service @@ -84,3 +84,4 @@ Type=oneshot DynamicUser=yes StateDirectory=waldo quux/pief aaa/bbb aaa aaa/ccc xxx/yyy:aaa/111 xxx:aaa/222 xxx/zzz:aaa/333 abc:d\:ef EnvironmentFile=-/usr/lib/systemd/systemd-asan-env +ReadWritePaths=-/coverage diff --git a/test/test-execute/exec-dynamicuser-supplementarygroups.service b/test/test-execute/exec-dynamicuser-supplementarygroups.service index be1b8f76f2..fd88a790e4 100644 --- a/test/test-execute/exec-dynamicuser-supplementarygroups.service +++ b/test/test-execute/exec-dynamicuser-supplementarygroups.service @@ -9,3 +9,4 @@ Type=oneshot DynamicUser=yes SupplementaryGroups=1 2 EnvironmentFile=-/usr/lib/systemd/systemd-asan-env +ReadWritePaths=-/coverage diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 215f3cb1cc..1fd1b2290f 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -8655,7 +8655,7 @@ if __name__ == '__main__': asan_options = ns.asan_options lsan_options = ns.lsan_options ubsan_options = ns.ubsan_options - with_coverage = ns.with_coverage + with_coverage = ns.with_coverage or "COVERAGE_BUILD_DIR" in os.environ show_journal = ns.show_journal if use_valgrind: diff --git a/test/units/TEST-38-FREEZER.sh b/test/units/TEST-38-FREEZER.sh index 07597843e2..4c483df46a 100755 --- a/test/units/TEST-38-FREEZER.sh +++ b/test/units/TEST-38-FREEZER.sh @@ -7,6 +7,11 @@ set -o pipefail # shellcheck source=test/units/test-control.sh . "$(dirname "$0")"/test-control.sh +if [[ -n "${COVERAGE_BUILD_DIR:-}" ]]; then + echo "TEST-38-FREEZER freezes when systemd is built with coverage enabled" >/skipped + exit 77 +fi + systemd-analyze log-level debug unit=TEST-38-FREEZER-sleep.service |