From bb74677b0831438cdf87cbfd34d6b3fcf5e5892f Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 15 Dec 2025 01:14:10 -0500 Subject: [PATCH] Create basic substitution unit tests - substitute single store object - substitute single store object with single dep --- .../single/substituter.json | 31 +++ .../worker-substitution/with-dep/store.json | 55 ++++++ .../with-dep/substituter.json | 55 ++++++ src/libstore-tests/meson.build | 1 + src/libstore-tests/worker-substitution.cc | 176 ++++++++++++++++++ .../nix/util/tests/json-characterization.hh | 25 +++ 6 files changed, 343 insertions(+) create mode 100644 src/libstore-tests/data/worker-substitution/single/substituter.json create mode 100644 src/libstore-tests/data/worker-substitution/with-dep/store.json create mode 100644 src/libstore-tests/data/worker-substitution/with-dep/substituter.json create mode 100644 src/libstore-tests/worker-substitution.cc diff --git a/src/libstore-tests/data/worker-substitution/single/substituter.json b/src/libstore-tests/data/worker-substitution/single/substituter.json new file mode 100644 index 000000000..f22d4c7df --- /dev/null +++ b/src/libstore-tests/data/worker-substitution/single/substituter.json @@ -0,0 +1,31 @@ +{ + "buildTrace": {}, + "config": { + "store": "/nix/store" + }, + "contents": { + "axqic2q30v0sqvcpiqxs139q8w6zd4n8-hello": { + "contents": { + "contents": "Hello, world!", + "executable": false, + "type": "regular" + }, + "info": { + "ca": { + "hash": "sha256-KShIJtIwWG1gMSpvPMt5drppc1h5WMwHWzVpNJiVqGI=", + "method": "nar" + }, + "deriver": null, + "narHash": "sha256-KShIJtIwWG1gMSpvPMt5drppc1h5WMwHWzVpNJiVqGI=", + "narSize": 128, + "references": [], + "registrationTime": null, + "signatures": [], + "storeDir": "/nix/store", + "ultimate": false, + "version": 2 + } + } + }, + "derivations": {} +} diff --git a/src/libstore-tests/data/worker-substitution/with-dep/store.json b/src/libstore-tests/data/worker-substitution/with-dep/store.json new file mode 100644 index 000000000..3f2994dfb --- /dev/null +++ b/src/libstore-tests/data/worker-substitution/with-dep/store.json @@ -0,0 +1,55 @@ +{ + "buildTrace": {}, + "config": { + "store": "/nix/store" + }, + "contents": { + "4k79i02avcckr96r97lqnswn75fi1gv7-dependency": { + "contents": { + "contents": "I am a dependency", + "executable": false, + "type": "regular" + }, + "info": { + "ca": { + "hash": "sha256-miJnClL0Ai/HAmX1G/pz7P2TIaeFjP5D/VN1rhYf354=", + "method": "nar" + }, + "deriver": null, + "narHash": "sha256-miJnClL0Ai/HAmX1G/pz7P2TIaeFjP5D/VN1rhYf354=", + "narSize": 136, + "references": [], + "registrationTime": null, + "signatures": [], + "storeDir": "/nix/store", + "ultimate": false, + "version": 2 + } + }, + "k09ldq9fvxb6vfwq0cmv6j1jgqx08y1n-main": { + "contents": { + "contents": "I depend on /nix/store/4k79i02avcckr96r97lqnswn75fi1gv7-dependency", + "executable": false, + "type": "regular" + }, + "info": { + "ca": { + "hash": "sha256-CBfMK3HkqiXjpI8HNL1spWD/US4RnQHwI67Ojl50XoQ=", + "method": "nar" + }, + "deriver": null, + "narHash": "sha256-CBfMK3HkqiXjpI8HNL1spWD/US4RnQHwI67Ojl50XoQ=", + "narSize": 184, + "references": [ + "4k79i02avcckr96r97lqnswn75fi1gv7-dependency" + ], + "registrationTime": null, + "signatures": [], + "storeDir": "/nix/store", + "ultimate": false, + "version": 2 + } + } + }, + "derivations": {} +} diff --git a/src/libstore-tests/data/worker-substitution/with-dep/substituter.json b/src/libstore-tests/data/worker-substitution/with-dep/substituter.json new file mode 100644 index 000000000..3f2994dfb --- /dev/null +++ b/src/libstore-tests/data/worker-substitution/with-dep/substituter.json @@ -0,0 +1,55 @@ +{ + "buildTrace": {}, + "config": { + "store": "/nix/store" + }, + "contents": { + "4k79i02avcckr96r97lqnswn75fi1gv7-dependency": { + "contents": { + "contents": "I am a dependency", + "executable": false, + "type": "regular" + }, + "info": { + "ca": { + "hash": "sha256-miJnClL0Ai/HAmX1G/pz7P2TIaeFjP5D/VN1rhYf354=", + "method": "nar" + }, + "deriver": null, + "narHash": "sha256-miJnClL0Ai/HAmX1G/pz7P2TIaeFjP5D/VN1rhYf354=", + "narSize": 136, + "references": [], + "registrationTime": null, + "signatures": [], + "storeDir": "/nix/store", + "ultimate": false, + "version": 2 + } + }, + "k09ldq9fvxb6vfwq0cmv6j1jgqx08y1n-main": { + "contents": { + "contents": "I depend on /nix/store/4k79i02avcckr96r97lqnswn75fi1gv7-dependency", + "executable": false, + "type": "regular" + }, + "info": { + "ca": { + "hash": "sha256-CBfMK3HkqiXjpI8HNL1spWD/US4RnQHwI67Ojl50XoQ=", + "method": "nar" + }, + "deriver": null, + "narHash": "sha256-CBfMK3HkqiXjpI8HNL1spWD/US4RnQHwI67Ojl50XoQ=", + "narSize": 184, + "references": [ + "4k79i02avcckr96r97lqnswn75fi1gv7-dependency" + ], + "registrationTime": null, + "signatures": [], + "storeDir": "/nix/store", + "ultimate": false, + "version": 2 + } + } + }, + "derivations": {} +} diff --git a/src/libstore-tests/meson.build b/src/libstore-tests/meson.build index 58f624611..123bccb90 100644 --- a/src/libstore-tests/meson.build +++ b/src/libstore-tests/meson.build @@ -85,6 +85,7 @@ sources = files( 'store-reference.cc', 'uds-remote-store.cc', 'worker-protocol.cc', + 'worker-substitution.cc', 'write-derivation.cc', ) diff --git a/src/libstore-tests/worker-substitution.cc b/src/libstore-tests/worker-substitution.cc new file mode 100644 index 000000000..7e94cc951 --- /dev/null +++ b/src/libstore-tests/worker-substitution.cc @@ -0,0 +1,176 @@ +#include +#include + +#include "nix/store/build/worker.hh" +#include "nix/store/dummy-store-impl.hh" +#include "nix/store/globals.hh" +#include "nix/util/memory-source-accessor.hh" + +#include "nix/store/tests/libstore.hh" +#include "nix/util/tests/json-characterization.hh" + +namespace nix { + +class WorkerSubstitutionTest : public LibStoreTest, public JsonCharacterizationTest> +{ + std::filesystem::path unitTestData = getUnitTestData() / "worker-substitution"; + +protected: + ref dummyStore; + ref substituter; + + WorkerSubstitutionTest() + : LibStoreTest([] { + auto config = make_ref(DummyStoreConfig::Params{}); + config->readOnly = false; + return config->openDummyStore(); + }()) + , dummyStore(store.dynamic_pointer_cast()) + , substituter([] { + auto config = make_ref(DummyStoreConfig::Params{}); + config->readOnly = false; + config->isTrusted = true; + return config->openDummyStore(); + }()) + { + } + +public: + std::filesystem::path goldenMaster(std::string_view testStem) const override + { + return unitTestData / testStem; + } + + static void SetUpTestSuite() + { + initLibStore(false); + } +}; + +TEST_F(WorkerSubstitutionTest, singleStoreObject) +{ + // Add a store path to the substituter + auto pathInSubstituter = substituter->addToStore( + "hello", + SourcePath{ + [] { + auto sc = make_ref(); + sc->root = MemorySourceAccessor::File{MemorySourceAccessor::File::Regular{ + .executable = false, + .contents = "Hello, world!", + }}; + return sc; + }(), + }, + ContentAddressMethod::Raw::NixArchive, + HashAlgorithm::SHA256); + + // Snapshot the substituter (has one store object) + checkpointJson("single/substituter", substituter); + + // Snapshot the destination store before (should be empty) + checkpointJson("../dummy-store/empty", dummyStore); + + // The path should not exist in the destination store yet + ASSERT_FALSE(dummyStore->isValidPath(pathInSubstituter)); + + // Create a worker with our custom substituter + Worker worker{*dummyStore, *dummyStore}; + + // Override the substituters to use our dummy store substituter + ref substituerAsStore = substituter; + worker.getSubstituters = [substituerAsStore]() -> std::list> { return {substituerAsStore}; }; + + // Create a substitution goal for the path + auto goal = worker.makePathSubstitutionGoal(pathInSubstituter); + + // Run the worker with -j0 semantics (no local builds, only substitution) + // The worker.run() takes a set of goals + Goals goals; + goals.insert(upcast_goal(goal)); + worker.run(goals); + + // Snapshot the destination store after (should match the substituter) + checkpointJson("single/substituter", dummyStore); + + // The path should now exist in the destination store + ASSERT_TRUE(dummyStore->isValidPath(pathInSubstituter)); + + // Verify the goal succeeded + ASSERT_EQ(goal->exitCode, Goal::ecSuccess); +} + +TEST_F(WorkerSubstitutionTest, singleRootStoreObjectWithSingleDepStoreObject) +{ + // First, add a dependency store path to the substituter + auto dependencyPath = substituter->addToStore( + "dependency", + SourcePath{ + [] { + auto sc = make_ref(); + sc->root = MemorySourceAccessor::File{MemorySourceAccessor::File::Regular{ + .executable = false, + .contents = "I am a dependency", + }}; + return sc; + }(), + }, + ContentAddressMethod::Raw::NixArchive, + HashAlgorithm::SHA256); + + // Now add a store path that references the dependency + auto mainPath = substituter->addToStore( + "main", + SourcePath{ + [&] { + auto sc = make_ref(); + // Include a reference to the dependency path in the contents + sc->root = MemorySourceAccessor::File{MemorySourceAccessor::File::Regular{ + .executable = false, + .contents = "I depend on " + substituter->printStorePath(dependencyPath), + }}; + return sc; + }(), + }, + ContentAddressMethod::Raw::NixArchive, + HashAlgorithm::SHA256, + StorePathSet{dependencyPath}); + + // Snapshot the substituter (has two store objects) + checkpointJson("with-dep/substituter", substituter); + + // Snapshot the destination store before (should be empty) + checkpointJson("../dummy-store/empty", dummyStore); + + // Neither path should exist in the destination store yet + ASSERT_FALSE(dummyStore->isValidPath(dependencyPath)); + ASSERT_FALSE(dummyStore->isValidPath(mainPath)); + + // Create a worker with our custom substituter + Worker worker{*dummyStore, *dummyStore}; + + // Override the substituters to use our dummy store substituter + ref substituterAsStore = substituter; + worker.getSubstituters = [substituterAsStore]() -> std::list> { return {substituterAsStore}; }; + + // Create a substitution goal for the main path only + // The worker should automatically substitute the dependency as well + auto goal = worker.makePathSubstitutionGoal(mainPath); + + // Run the worker + Goals goals; + goals.insert(upcast_goal(goal)); + worker.run(goals); + + // Snapshot the destination store after (should match the substituter) + checkpointJson("with-dep/substituter", dummyStore); + + // Both paths should now exist in the destination store + ASSERT_TRUE(dummyStore->isValidPath(dependencyPath)); + ASSERT_TRUE(dummyStore->isValidPath(mainPath)); + + // Verify the goal succeeded + ASSERT_EQ(goal->exitCode, Goal::ecSuccess); +} + +} // namespace nix diff --git a/src/libutil-test-support/include/nix/util/tests/json-characterization.hh b/src/libutil-test-support/include/nix/util/tests/json-characterization.hh index 6db32c4b6..744880f38 100644 --- a/src/libutil-test-support/include/nix/util/tests/json-characterization.hh +++ b/src/libutil-test-support/include/nix/util/tests/json-characterization.hh @@ -83,6 +83,31 @@ void checkpointJson(CharacterizationTest & test, PathView testStem, const T & go } } +/** + * Specialization for when we need to do "JSON -> `ref`" in one + * direction, but "`const T &` -> JSON" in the other direction. + */ +template +void checkpointJson(CharacterizationTest & test, PathView testStem, const ref & got) +{ + using namespace nlohmann; + + auto file = test.goldenMaster(Path{testStem} + ".json"); + + json gotJson = static_cast(*got); + + if (testAccept()) { + std::filesystem::create_directories(file.parent_path()); + writeFile(file, gotJson.dump(2) + "\n"); + ADD_FAILURE() << "Updating golden master " << file; + } else { + json expectedJson = json::parse(readFile(file)); + ASSERT_EQ(gotJson, expectedJson); + ref expected = adl_serializer>::from_json(expectedJson); + ASSERT_EQ(*got, *expected); + } +} + /** * Mixin class for writing characterization tests for `nlohmann::json` * conversions for a given type.