aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--flake.nix2
-rw-r--r--nixos/tests/all-tests.nix66
-rw-r--r--nixos/tests/dsl.nix106
3 files changed, 96 insertions, 78 deletions
diff --git a/flake.nix b/flake.nix
index 10314f9..5014f4d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -34,6 +34,8 @@
clang-tools # clang-format
flamegraph
linuxKernel.packages.linux_6_6.perf
+ hyperfine
+ nix-eval-jobs
];
shellHook = ''
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index f03fc20..c6b66f1 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -23,24 +23,24 @@ let
in
{
a = {
- request = true;
+ goal = true;
inputs = abcInputs;
};
b = {
- request = true;
+ goal = true;
inputs = abcInputs;
};
c = {
- request = true;
+ goal = true;
inputs = abcInputs;
};
d = {
- request = true;
+ goal = true;
inputs.w = { };
};
e = {
- request = true;
+ goal = true;
inputs.x = { };
};
@@ -73,8 +73,8 @@ builtins.mapAttrs
diamond-unbuilt-0 = {
imports = [
{
- dag.needBuilds = 0;
- dag.needDownloads = 0;
+ dag.test.unconstrained.builds = 0;
+ dag.test.unconstrained.downloads = 0;
}
diamond
];
@@ -84,9 +84,9 @@ builtins.mapAttrs
{
dag.nodes.a.cache = "remote";
dag.nodes.b.cache = "remote";
- dag.nodes.d.request = true;
- dag.needBuilds = 2;
- dag.needDownloads = 2;
+ dag.nodes.d.goal = true;
+ dag.test.unconstrained.builds = 2;
+ dag.test.unconstrained.downloads = 2;
}
diamond
];
@@ -94,9 +94,9 @@ builtins.mapAttrs
diamond-unbuilt-4 = {
imports = [
{
- dag.nodes.d.request = true;
- dag.needBuilds = 4;
- dag.needDownloads = 0;
+ dag.nodes.d.goal = true;
+ dag.test.unconstrained.builds = 4;
+ dag.test.unconstrained.downloads = 0;
}
diamond
];
@@ -106,33 +106,31 @@ builtins.mapAttrs
imports = [
{
dag = {
- # all builds
- needBuilds = 9;
- # all builds allowed
- allowBuilds = 5;
- # chosen builds requested
- choseBuilds = 3;
+ test.unconstrained.builds = 9;
+
+ constraints.builds = 5;
+ test.constrained.builds = 3;
nodes = {
- a = {
- assertChosen = true;
- assertNeeded = true;
+ a.test = {
+ chosen = true;
+ needed = true;
};
- b = {
- assertChosen = true;
- assertNeeded = true;
+ b.test = {
+ chosen = true;
+ needed = true;
};
- c = {
- assertChosen = true;
- assertNeeded = true;
+ c.test = {
+ chosen = true;
+ needed = true;
};
- d.assertNeeded = true;
- e.assertNeeded = true;
- u.assertNeeded = true;
- v.assertNeeded = true;
- w.assertNeeded = true;
- x.assertNeeded = true;
+ d.test.needed = true;
+ e.test.needed = true;
+ u.test.needed = true;
+ v.test.needed = true;
+ w.test.needed = true;
+ x.test.needed = true;
};
};
}
diff --git a/nixos/tests/dsl.nix b/nixos/tests/dsl.nix
index b0dd006..acf15c0 100644
--- a/nixos/tests/dsl.nix
+++ b/nixos/tests/dsl.nix
@@ -17,13 +17,13 @@ let
type = lib.types.str;
default = name;
};
- options.request = lib.mkEnableOption "Whether to mark the node for building";
- options.assertNeeded = lib.mkOption {
+ options.goal = lib.mkEnableOption ''Mark for building (node is a "goal", "target")'';
+ options.test.needed = lib.mkOption {
type = with lib.types; nullOr bool;
default = null;
- description = "Whether the node must be built to satisfy all requests (either a requested node or a transitive dependency)";
+ description = "Verify `nix-build --dry-run` reports node as any of to-be built or to-be fetched";
};
- options.assertChosen = lib.mkOption {
+ options.test.chosen = lib.mkOption {
type = with lib.types; nullOr bool;
default = null;
description = "Whether the node is included in the build plan (i.t. it's `needed` and fitted into budget)";
@@ -57,32 +57,32 @@ in
type = Nodes;
description = "Derivation DAG, including cache status and references.";
};
- needBuilds = lib.mkOption {
+ test.unconstrained.builds = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
description = "How many builds are required to satisfy all targets. Null disables the test";
};
- needDownloads = lib.mkOption {
+ test.unconstrained.downloads = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
description = "How many downloads are required to satisfy all targets. Null disables the test";
};
- choseBuilds = lib.mkOption {
+ test.constrained.builds = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
description = "How many builds we expect evanix to choose to satisfy the maximum number of targets within the given budget. Null disables the test";
};
- choseDownloads = lib.mkOption {
+ test.constrained.downloads = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
description = "How many downloads we expect evanix to choose to satisfy the maximum number of targets within the given budget. Null disables the test";
};
- allowBuilds = lib.mkOption {
+ constraints.builds = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
description = "How many builds evanix is allowed to choose. Null means no constraint";
};
- allowDownloads = lib.mkOption {
+ constraints.downloads = lib.mkOption {
type = with lib.types; nullOr int;
default = null;
description = "How many downloads evanix is allowed to choose. Null means no constraint";
@@ -105,11 +105,16 @@ in
'';
requestExpressions = pkgs.writeText "guest-request-scope.nix" ''
let
+ inherit (pkgs) lib;
pkgs = import ${pkgs.path} { };
config = builtins.fromJSON (builtins.readFile ${configJson});
- testPkgs = import ${expressions};
+ all = import ${expressions};
+ subset = lib.pipe all [
+ (lib.filterAttrs (name: node: lib.isDerivation node))
+ (lib.filterAttrs (name: node: config.nodes.''${name}.goal))
+ ];
in
- pkgs.lib.attrsets.filterAttrs (name: node: !(config.nodes ? ''${name}) || (config.nodes.''${name} ? request && config.nodes.''${name}.request)) testPkgs
+ subset
'';
tester = pkgs.writers.writePython3Bin "dag-test" { } ''
@@ -120,9 +125,11 @@ in
import subprocess
import sys
+
with open("${configJson}", "r") as f:
config = json.load(f)
+
nodes = config["nodes"]
print(f"config={config}", file=sys.stderr)
@@ -173,13 +180,11 @@ in
raise RuntimeError("nix-build --dry-run produced invalid output", line)
return to_fetch, to_build
- nix_build_needed = set()
drv_to_schedule = {}
for name, node in nodes.items():
p = subprocess.run(["nix-build", "${expressions}", "--dry-run", "--show-trace", "-A", name], check=True, stderr=subprocess.PIPE)
output = p.stderr.decode("utf-8")
to_fetch, to_build = parse_dry_run(output)
- nix_build_needed.update(to_build)
drv_to_schedule[name] = (to_fetch, to_build)
drv_to_action = {}
@@ -210,52 +215,65 @@ in
else:
raise AssertionError('cache is not in [ "local", "remote", "unbuilt" ]')
- need_dls, need_builds = set(), set()
+ need_builds: set[str] = set()
+ need_dls: set[str] = set()
for name, node in nodes.items():
- if node["request"]:
+ if node["goal"]:
need_dls.update(drv_to_schedule[name][0])
need_builds.update(drv_to_schedule[name][1])
- if (expected_need_dls := config.get("needDownloads", None)) is not None:
+ if (expected_need_dls := config["test"]["unconstrained"]["downloads"]) is not None:
assert len(need_dls) == expected_need_dls, f"{len(need_dls)} != {expected_need_dls}; building {need_dls}"
print("Verified `needDownloads`", file=sys.stderr)
- if (expected_need_builds := config.get("needBuilds", None)) is not None:
+ if (expected_need_builds := config["test"]["unconstrained"]["builds"]) is not None:
assert len(need_builds) == expected_need_builds, f"{len(need_builds)} != {expected_need_builds}; building {need_builds}"
print("Verified `needBuilds`", file=sys.stderr)
- assertNeededNodes = set()
- for name, node in nodes.items():
- if "assertNeeded" in node and node["assertNeeded"]:
- assertNeededNodes.add(name)
- if assertNeededNodes:
- for name in assertNeededNodes:
- assert name in nix_build_needed, f"{name}.assertNeeded failed"
- print("Verified `assertNeededNodes`", file=sys.stderr)
-
- assertChosenNodes = set()
for name, node in nodes.items():
- if "assertChosen" in node and node["assertChosen"]:
- assertChosenNodes.add(name)
+ if node["test"]["needed"]:
+ assert name in need_builds or name in need_dls, f"{name}.test.needed violated"
- evanix_args = ["evanix", "${requestExpressions}", "--dry-run", "--close-unused-fd", "false"]
- if config.get("allowBuilds", None) is not None:
- evanix_args.extend(["--solver=highs", "--max-build", str(config["allowBuilds"])])
- if assertChosenNodes or config.get("choseBuilds", None) is not None:
- evanix = subprocess.run(evanix_args, check=True, stdout=subprocess.PIPE)
- evanix_output = evanix.stdout.decode("utf-8")
- evanix_builds = parse_evanix_dry_run(evanix_output)
+ evanix_args = ["evanix", "${requestExpressions}", "--dry-run", "--close-unused-fd", "false"]
+ if (allow_builds := config["constraints"]["builds"]) is not None:
+ evanix_args.extend(["--solver=highs", "--max-build", str(allow_builds)])
- if config.get("choseBuilds", None) is not None:
- assert len(evanix_builds) == config["choseBuilds"], f"len({evanix_builds}) != choseBuilds"
- print("Verified `choseBuilds`", file=sys.stderr)
+ expect_chosen_nodes = set(name for name, node in nodes.items() if node["test"]["chosen"])
+ expect_n_chosen_builds = config["test"]["constrained"]["builds"]
+ expect_n_chosen_downloads = config["test"]["constrained"]["downloads"]
- if assertChosenNodes:
- for name in assertChosenNodes:
- assert name in evanix_builds and name in nix_build_needed, f"{name}.assertChosen failed"
- print("Verified `assertChosenNodes`", file=sys.stderr)
+ # TODO: Add option
+ if expect_n_chosen_downloads is not None and expect_n_chosen_builds is not None:
+ expect_n_chosen_nodes = expect_n_chosen_builds + expect_n_chosen_downloads
+ else:
+ expect_n_chosen_nodes = None
+ if expect_chosen_nodes or expect_n_chosen_nodes is not None:
+ evanix = subprocess.run(evanix_args, check=True, stdout=subprocess.PIPE)
+ evanix_output = evanix.stdout.decode("utf-8")
+ evanix_choices = parse_evanix_dry_run(evanix_output)
+ else:
+ evanix_choices = set()
+
+ evanix_builds, evanix_downloads = [], []
+ for choice in evanix_choices:
+ if drv_to_action[choice] == "build":
+ evanix_builds.append(choice)
+ elif drv_to_action[choice] == "fetch":
+ evanix_downloads.append(choice)
+
+ if expect_n_chosen_nodes is not None:
+ assert len(evanix_choices) == expect_n_chosen_nodes, f"len({evanix_builds}) != choseNodes"
+ print("Verified `choseNodes`", file=sys.stderr)
+
+ if expect_chosen_nodes:
+ for name in expect_chosen_nodes:
+ assert name in evanix_choices, f"{name}.test.chosen failed; choices: {evanix_choices}"
+ print("Verified `expect_chosen_nodes`", file=sys.stderr)
+
+ assert expect_n_chosen_builds is None or len(evanix_builds) == expect_n_chosen_builds, f"{expect_n_chosen_builds=} {len(evanix_builds)=}"
+ assert expect_n_chosen_downloads is None or len(evanix_downloads) == expect_n_chosen_downloads, f"{expect_n_chosen_downloads=} {len(evanix_downloads)=}"
'';
in
{