ci: Implement coverage on top of mkosi (#35407)

This commit is contained in:
Daan De Meyer
2024-12-05 10:47:45 +01:00
committed by GitHub
32 changed files with 513 additions and 85 deletions
+145
View File
@@ -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
+1 -1
View File
@@ -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
+2
View File
@@ -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
+3
View File
@@ -4,5 +4,8 @@
ToolsTreePackages=
gcc
gperf
lcov
llvm
meson
pkgconf
rsync
@@ -10,6 +10,7 @@ ToolsTreePackages=
libcap
libmicrohttpd
mypy
perl-json-xs
python-jinja
python-pytest
ruff
+9
View File
@@ -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
+56
View File
@@ -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"
+9
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() {
@@ -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"
+2
View File
@@ -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
+2 -1
View File
@@ -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;
+55 -55
View File
@@ -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;
+4 -2
View File
@@ -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
+171 -25
View File
@@ -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')
@@ -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
@@ -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
@@ -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
@@ -9,3 +9,4 @@ Type=oneshot
User=1
DynamicUser=yes
SupplementaryGroups=1
ReadWritePaths=-/coverage
@@ -8,3 +8,4 @@ ExecStart=sh -x -c 'test "$$(id -g)" = "1" && test "$$(id -u)" = "1"'
Type=oneshot
User=1
DynamicUser=yes
ReadWritePaths=-/coverage
@@ -11,3 +11,4 @@ RuntimeDirectory=test-exec_runtimedirectorypreserve
RuntimeDirectoryPreserve=yes
DynamicUser=yes
EnvironmentFile=-/usr/lib/systemd/systemd-asan-env
ReadWritePaths=-/coverage
@@ -12,3 +12,4 @@ RuntimeDirectory=test-exec_runtimedirectorypreserve
RuntimeDirectoryPreserve=yes
DynamicUser=yes
EnvironmentFile=-/usr/lib/systemd/systemd-asan-env
ReadWritePaths=-/coverage
@@ -11,3 +11,4 @@ Type=oneshot
RuntimeDirectory=test-exec_runtimedirectorypreserve
DynamicUser=yes
EnvironmentFile=-/usr/lib/systemd/systemd-asan-env
ReadWritePaths=-/coverage
@@ -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
@@ -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
@@ -9,3 +9,4 @@ Type=oneshot
DynamicUser=yes
SupplementaryGroups=1 2
EnvironmentFile=-/usr/lib/systemd/systemd-asan-env
ReadWritePaths=-/coverage
+1 -1
View File
@@ -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:
+5
View File
@@ -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