modularServiceCompliance
pkgs.testers.modularServiceCompliance
Functor
Docs pulled from | This Revision | 9 minutes ago
Nixpkgs manual
See https://nixos.org/manual/nixpkgs/unstable/#tester-modularServiceCompliance or doc/build-helpers/testers.chapter.md
Noogle detected
This is a Functor
Learn about functors
Implementation
The following is the current implementation of this function.
{
evalConfig,
mkTest,
sharedDir,
callReload,
namePrefix ? "modular-service-compliance",
}:
let
/**
A successful evaluation to be probed by evalTestDefs
*/
# Try use only few evalConfig calls for performance.
evalResult = evalConfig {
services = {
svc = {
process.argv = [ "${coreutils}/bin/true" ];
assertions = [
{
assertion = true;
message = "compliance test assertion";
}
];
warnings = [ "compliance test warning" ];
services.child = {
process.argv = [ "${coreutils}/bin/true" ];
assertions = [
{
assertion = true;
message = "compliance child assertion";
}
];
warnings = [ "compliance child warning" ];
};
# A sub-service exercising reload derivation from a reload signal.
services.reloadee = {
process.argv = [ "${coreutils}/bin/true" ];
process.reloadSignal = "HUP";
};
};
};
};
evalTestDefs =
let
c = evalResult.config.svc;
in
{
testProcessArgv = {
expr = c.process.argv;
expected = [ "${coreutils}/bin/true" ];
};
testSubServiceArgv = {
expr = c.services.child.process.argv;
expected = [ "${coreutils}/bin/true" ];
};
testAssertions = {
expr = lib.elem {
assertion = true;
message = "compliance test assertion";
} c.assertions;
expected = true;
};
testWarnings = {
expr = lib.elem "compliance test warning" c.warnings;
expected = true;
};
testSubServiceAssertions = {
expr = lib.elem {
assertion = true;
message = "compliance child assertion";
} c.services.child.assertions;
expected = true;
};
testSubServiceWarnings = {
expr = lib.elem "compliance child warning" c.services.child.warnings;
expected = true;
};
# The reload-conflict assertion must not fire (its `assertion` must hold) for a
# service that sets only reloadSignal (guards the inverted-assertion fix, and the
# priority-aware conflict detection: reloadSignal derives reloadCommand internally,
# which must not be mistaken for a user-set conflict).
testNoReloadConflict = {
expr = lib.any (
a:
a.message
== "reloadSignal conflicts with reloadCommand. Please either use reloadSignal or reloadCommand."
&& !a.assertion
) c.services.reloadee.assertions;
expected = false;
};
# Setting process.reloadSignal derives process.reloadCommand
# (guards the misplaced-paren mkIf fix).
testReloadSignalDerivesCommand = {
expr = c.services.reloadee.process.reloadCommand;
expected = "${coreutils}/bin/kill -HUP $MAINPID";
};
# notificationProtocol submodule bools default to false.
testNotificationProtocolSystemdDefault = {
expr = c.notificationProtocol.systemd;
expected = false;
};
testNotificationProtocolS6Default = {
expr = c.notificationProtocol.s6;
expected = false;
};
# Setting both reloadSignal and reloadCommand explicitly is a genuine conflict,
# so the assertion must fire. Separate eval: checkDrv would fail on this.
testReloadConflictFires = {
expr = lib.any (
a:
a.message
== "reloadSignal conflicts with reloadCommand. Please either use reloadSignal or reloadCommand."
&& !a.assertion
) conflictEval.config.conflict.assertions;
expected = true;
};
# Separate eval for a failing assertion — checkDrv would fail here,
# so we only access config.
testFailingAssertionValue = {
expr = lib.elem {
assertion = false;
message = "compliance failing assertion";
} failingEval.config.failing.assertions;
expected = true;
};
};
conflictEval = evalConfig {
services = {
conflict = {
process.argv = [ "${coreutils}/bin/true" ];
process.reloadSignal = "HUP";
process.reloadCommand = "${coreutils}/bin/kill -HUP $MAINPID";
};
};
};
failingEval = evalConfig {
services = {
failing = {
process.argv = [ "${coreutils}/bin/true" ];
assertions = [
{
assertion = false;
message = "compliance failing assertion";
}
];
};
};
};
/**
A service script that records its received arguments, then sleeps forever.
The first argument is a service identifier used to namespace its comms
subdirectory; the remaining arguments are recorded as the service's args.
*/
svc = writeShellScript "${namePrefix}-svc" ''
id="$1"; shift
dir="${sharedDir}/$id"
mkdir -p "$dir"
echo "$$" > "$dir/pid"
printf '%s\n' "$@" > "$dir/args"
exec "${coreutils}/bin/sleep" infinity
'';
/**
A reloadable service script. Like `svc`, it namespaces a comms subdirectory
by its first argument and records the remaining arguments. It traps SIGHUP
and appends a marker line to `$dir/reloads` on each reload, then stays alive
as the trapping shell itself (no `exec`, so the trap and MAINPID are kept).
*/
reloadableSvc = writeShellScript "${namePrefix}-reloadable-svc" ''
id="$1"; shift
dir="${sharedDir}/$id"
mkdir -p "$dir"
: > "$dir/reloads"
reload() { printf 'reloaded\n' >> "$dir/reloads"; }
trap reload HUP
echo "$$" > "$dir/pid"
printf '%s\n' "$@" > "$dir/args"
while true; do "${coreutils}/bin/sleep" 1; done
'';
mkArgv =
id: extraArgs:
[
svc
id
]
++ extraArgs;
/**
Shell snippet: wait for a service to write its pid file, verify the
process is still alive, then check that each expected argument appears
in the recorded args file.
*/
waitAndCheck =
id: expectedArgs:
''
echo "waiting for ${id}..."
timeout=30; elapsed=0
while [ ! -f "${sharedDir}/${id}/pid" ] && [ "$elapsed" -lt "$timeout" ]; do
sleep 1; elapsed=$((elapsed + 1))
done
test -f "${sharedDir}/${id}/pid" || { echo "${id}: no pid file after ''${timeout}s"; exit 1; }
pid=$(cat "${sharedDir}/${id}/pid")
kill -0 "$pid" || { echo "${id}: pid $pid is not running"; exit 1; }
echo "${id}: started (pid $pid)"
''
+ lib.concatMapStrings (arg: ''
grep -qxF -- ${lib.escapeShellArg arg} "${sharedDir}/${id}/args" \
|| { echo "${id}: expected arg ${lib.escapeShellArg arg} not found"; cat "${sharedDir}/${id}/args"; exit 1; }
'') expectedArgs;
mkTestScript =
name: text:
lib.getExe (writeShellApplication {
name = "${namePrefix}-${name}";
runtimeInputs = [
coreutils
gnugrep
];
inherit text;
});
# The reload runtime test's service tree. The reloadable unit is the *sub*-service,
# so its name path `[ "reload" "inner" ]` handed to `callReload` exercises the
# integration's nested unit naming (NixOS dash-joins to `reload-inner.service`).
reloadServices = {
reload = {
# Parent must run something; a bare keep-alive is enough.
process.argv = [
reloadableSvc
"reload-parent"
];
services.inner.process = {
argv = [
reloadableSvc
"reload-inner"
];
# The script traps SIGHUP; reloadSignal derives the manager's reload command.
reloadSignal = "HUP";
};
};
};
in
{
# Eval-level tests: config structure, evaluated in the integration's
# full context (one whole-system eval).
# TODO: generalize with
# - pkgs/test/buildenv.nix
# - pkgs/test/overriding.nix
eval = stdenvNoCC.mkDerivation (finalAttrs: {
__structuredAttrs = true;
name = "${namePrefix}-eval-report";
# Depend on the integration's representative derivation to prove that
# the system builds with these services.
representative = evalResult.checkDrv;
passthru = {
tests = evalTestDefs;
failures = lib.runTests finalAttrs.finalPackage.tests;
};
testResults = lib.mapAttrs (_: test: test.expr == test.expected) finalAttrs.finalPackage.tests;
buildCommand = ''
touch $out
for testName in "''${!testResults[@]}"; do
if [[ -n "''${testResults[$testName]}" ]]; then
echo "PASS $testName"
else
echo "FAIL $testName"
fi
done
''
+ lib.optionalString (lib.any (v: !v) (lib.attrValues finalAttrs.testResults)) ''
{
echo ""
echo "Eval-level compliance failures:"
for testName in "''${!testResults[@]}"; do
if [[ -z "''${testResults[$testName]}" ]]; then
echo "- $testName"
fi
done
echo ""
echo 'Inspect with: nix eval .#<path>.tests.''${testName}'
} >&2
exit 1
'';
});
# Integration tests: verify that services actually run.
basic-argv = mkTest {
name = "${namePrefix}-basic-argv";
services.test.process.argv = mkArgv "test" [
"--greeting"
"hello"
];
testExe = mkTestScript "basic-argv" (
waitAndCheck "test" [
"--greeting"
"hello"
]
);
};
sub-services = mkTest {
name = "${namePrefix}-sub-services";
services.a = {
process.argv = mkArgv "a" [ "--depth=0" ];
services.b = {
process.argv = mkArgv "b" [ "--depth=1" ];
services.c.process.argv = mkArgv "c" [ "--depth=2" ];
};
};
testExe = mkTestScript "sub-services" ''
${waitAndCheck "a" [ "--depth=0" ]}
${waitAndCheck "b" [ "--depth=1" ]}
${waitAndCheck "c" [ "--depth=2" ]}
'';
};
# Runtime reload: start a reloadable service, ask the integration to reload it,
# and assert the service observed the reload (recorded a SIGHUP marker).
reload = mkTest {
name = "${namePrefix}-reload";
services = reloadServices;
testExe = mkTestScript "reload" ''
${waitAndCheck "reload-inner" [ ]}
${callReload [
"reload"
"inner"
]}
timeout=30; elapsed=0
while ! grep -qx "reloaded" "${sharedDir}/reload-inner/reloads" && [ "$elapsed" -lt "$timeout" ]; do
sleep 1; elapsed=$((elapsed + 1))
done
grep -qx "reloaded" "${sharedDir}/reload-inner/reloads" \
|| { echo "reload: no reload recorded after ''${timeout}s"; cat "${sharedDir}/reload-inner/reloads"; exit 1; }
echo "reload: reload observed"
'';
};
# See also the manual compliance items in doc/build-helpers/testers.chapter.md.
}