From 294acfd807ec046d73c58e948c195cc466935aa7 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 13 Nov 2025 02:27:19 -0500 Subject: [PATCH 1/3] Create infrastructure for "checkpoint" characterization tests These do a read/write test in the middles of some computation. They are an imperative way to test intermediate values rather than functionally testing end outputs. --- .../nix/util/tests/characterization.hh | 4 +-- .../nix/util/tests/json-characterization.hh | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/libutil-test-support/include/nix/util/tests/characterization.hh b/src/libutil-test-support/include/nix/util/tests/characterization.hh index 0434590f799..d8fad1df925 100644 --- a/src/libutil-test-support/include/nix/util/tests/characterization.hh +++ b/src/libutil-test-support/include/nix/util/tests/characterization.hh @@ -31,16 +31,14 @@ static inline bool testAccept() /** * Mixin class for writing characterization tests */ -class CharacterizationTest : public virtual ::testing::Test +struct CharacterizationTest : virtual ::testing::Test { -protected: /** * While the "golden master" for this characterization test is * located. It should not be shared with any other test. */ virtual std::filesystem::path goldenMaster(PathView testStem) const = 0; -public: /** * Golden test for reading * 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 0ee6fd2fda6..bd70ba8301b 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 @@ -39,6 +39,30 @@ void writeJsonTest(CharacterizationTest & test, PathView testStem, const T & val [](const auto & file, const auto & got) { return writeFile(file, got.dump(2) + "\n"); }); } +/** + * Golden test in the middle of something + */ +template +void checkpointJson(CharacterizationTest & test, PathView testStem, const T & 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); + T expected = adl_serializer::from_json(expectedJson); + ASSERT_EQ(got, expected); + } +} + /** * Mixin class for writing characterization tests for `nlohmann::json` * conversions for a given type. @@ -67,6 +91,11 @@ struct JsonCharacterizationTest : virtual CharacterizationTest { nix::writeJsonTest(*this, testStem, value); } + + void checkpointJson(PathView testStem, const T & value) + { + nix::checkpointJson(*this, testStem, value); + } }; } // namespace nix From 620a6947ab80795ae28dc1ffe2857f2c08d7ce9a Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 13 Nov 2025 02:27:19 -0500 Subject: [PATCH 2/3] Dedup some derivation initialization logic, and test `nix derivation add`, and its C API counterpart, now works a bit closer to `builtins.derivation` in that they don't require the user to fill-in input addressed paths correctly ahead of time. The logic for this is carefully deduplicated, between all 3 entry points, and also between the existing `checkInvariants` function. There are some more functional tests, and there are also many more unit tests. Co-authored-by: Sergei Zimmerman Co-authored-by: edef --- src/libexpr/primops.cc | 23 +- src/libstore-c/nix_api_store.cc | 8 +- .../invariants/bad-depends-on-drv-pre.json | 27 ++ .../derivation/invariants/bad-env-var.json | 18 ++ .../data/derivation/invariants/bad-path.json | 20 ++ .../invariants/depends-on-drv-pre.json | 25 ++ ...filled-in-deferred-empty-env-var-post.json | 20 ++ .../filled-in-deferred-empty-env-var-pre.json | 18 ++ .../filled-in-deferred-no-env-var-post.json | 20 ++ .../filled-in-deferred-no-env-var-pre.json | 17 ++ .../invariants/filled-in-idempotent.json | 20 ++ .../external-formats.cc} | 45 +-- src/libstore-tests/derivation/invariants.cc | 264 ++++++++++++++++++ src/libstore-tests/derivation/test-support.hh | 52 ++++ src/libstore-tests/meson.build | 3 +- src/libstore/derivations.cc | 214 ++++++++++---- src/libstore/include/nix/store/derivations.hh | 62 ++++ src/nix/derivation-add.cc | 4 +- tests/functional/derivation-json.sh | 17 +- 19 files changed, 742 insertions(+), 135 deletions(-) create mode 100644 src/libstore-tests/data/derivation/invariants/bad-depends-on-drv-pre.json create mode 100644 src/libstore-tests/data/derivation/invariants/bad-env-var.json create mode 100644 src/libstore-tests/data/derivation/invariants/bad-path.json create mode 100644 src/libstore-tests/data/derivation/invariants/depends-on-drv-pre.json create mode 100644 src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-post.json create mode 100644 src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-pre.json create mode 100644 src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-post.json create mode 100644 src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-pre.json create mode 100644 src/libstore-tests/data/derivation/invariants/filled-in-idempotent.json rename src/libstore-tests/{derivation.cc => derivation/external-formats.cc} (91%) create mode 100644 src/libstore-tests/derivation/invariants.cc create mode 100644 src/libstore-tests/derivation/test-support.hh diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 35f16a68de0..05ba7475e34 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1774,28 +1774,7 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName drv.outputs.insert_or_assign(i, DerivationOutput::Deferred{}); } - auto hashModulo = hashDerivationModulo(*state.store, Derivation(drv), true); - switch (hashModulo.kind) { - case DrvHash::Kind::Regular: - for (auto & i : outputs) { - auto h = get(hashModulo.hashes, i); - if (!h) - state.error("derivation produced no hash for output '%s'", i).atPos(v).debugThrow(); - auto outPath = state.store->makeOutputPath(i, *h, drvName); - drv.env[i] = state.store->printStorePath(outPath); - drv.outputs.insert_or_assign( - i, - DerivationOutput::InputAddressed{ - .path = std::move(outPath), - }); - } - break; - ; - case DrvHash::Kind::Deferred: - for (auto & i : outputs) { - drv.outputs.insert_or_assign(i, DerivationOutput::Deferred{}); - } - } + drv.fillInOutputPaths(*state.store); } /* Write the resulting term into the Nix store directory. */ diff --git a/src/libstore-c/nix_api_store.cc b/src/libstore-c/nix_api_store.cc index 024ed97858a..b95e5b749f5 100644 --- a/src/libstore-c/nix_api_store.cc +++ b/src/libstore-c/nix_api_store.cc @@ -223,13 +223,7 @@ nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store if (context) context->last_err_code = NIX_OK; try { - auto drv = static_cast(nlohmann::json::parse(json)); - - auto drvPath = nix::writeDerivation(*store->ptr, drv, nix::NoRepair, /* read only */ true); - - drv.checkInvariants(*store->ptr, drvPath); - - return new nix_derivation{drv}; + return new nix_derivation{nix::Derivation::parseJsonAndValidate(*store->ptr, nlohmann::json::parse(json))}; } NIXC_CATCH_ERRS_NULL } diff --git a/src/libstore-tests/data/derivation/invariants/bad-depends-on-drv-pre.json b/src/libstore-tests/data/derivation/invariants/bad-depends-on-drv-pre.json new file mode 100644 index 00000000000..8454cf548fc --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/bad-depends-on-drv-pre.json @@ -0,0 +1,27 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "InputAddressed throws when should be deferred", + "out": "" + }, + "inputs": { + "drvs": { + "lg4c4b8r9hlczwprl6kgnzfd9mc1xmkk-dependency.drv": { + "dynamicOutputs": {}, + "outputs": [ + "out" + ] + } + }, + "srcs": [] + }, + "name": "depends-on-drv", + "outputs": { + "out": { + "path": "c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-wrong-name" + } + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/bad-env-var.json b/src/libstore-tests/data/derivation/invariants/bad-env-var.json new file mode 100644 index 00000000000..cb0c9492fe8 --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/bad-env-var.json @@ -0,0 +1,18 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Wrong env var value throws error", + "out": "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-wrong-name" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "bad-env-var", + "outputs": { + "out": {} + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/bad-path.json b/src/libstore-tests/data/derivation/invariants/bad-path.json new file mode 100644 index 00000000000..688f2d4e696 --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/bad-path.json @@ -0,0 +1,20 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Wrong InputAddressed path throws error", + "out": "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-wrong-name" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "bad-path", + "outputs": { + "out": { + "path": "c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-wrong-name" + } + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/depends-on-drv-pre.json b/src/libstore-tests/data/derivation/invariants/depends-on-drv-pre.json new file mode 100644 index 00000000000..d782cc756c5 --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/depends-on-drv-pre.json @@ -0,0 +1,25 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Deferred stays deferred with CA dependencies", + "out": "" + }, + "inputs": { + "drvs": { + "lg4c4b8r9hlczwprl6kgnzfd9mc1xmkk-dependency.drv": { + "dynamicOutputs": {}, + "outputs": [ + "out" + ] + } + }, + "srcs": [] + }, + "name": "depends-on-drv", + "outputs": { + "out": {} + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-post.json b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-post.json new file mode 100644 index 00000000000..c5abdf69298 --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-post.json @@ -0,0 +1,20 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Fill in deferred output with empty env var", + "out": "/nix/store/bilpz1nq8qi9r3bzsp72n34yjgqg43ws-filled-in-deferred-empty-env-var" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "filled-in-deferred-empty-env-var", + "outputs": { + "out": { + "path": "bilpz1nq8qi9r3bzsp72n34yjgqg43ws-filled-in-deferred-empty-env-var" + } + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-pre.json b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-pre.json new file mode 100644 index 00000000000..bc5338925ea --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-empty-env-var-pre.json @@ -0,0 +1,18 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Fill in deferred output with empty env var", + "out": "" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "filled-in-deferred-empty-env-var", + "outputs": { + "out": {} + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-post.json b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-post.json new file mode 100644 index 00000000000..709d7bca0de --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-post.json @@ -0,0 +1,20 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Fill in deferred with missing env var", + "out": "/nix/store/wpk9qrgg77fyswhailap0gicgw98izx9-filled-in-deferred-no-env-var" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "filled-in-deferred-no-env-var", + "outputs": { + "out": { + "path": "wpk9qrgg77fyswhailap0gicgw98izx9-filled-in-deferred-no-env-var" + } + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-pre.json b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-pre.json new file mode 100644 index 00000000000..194e33086a0 --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/filled-in-deferred-no-env-var-pre.json @@ -0,0 +1,17 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Fill in deferred with missing env var" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "filled-in-deferred-no-env-var", + "outputs": { + "out": {} + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/data/derivation/invariants/filled-in-idempotent.json b/src/libstore-tests/data/derivation/invariants/filled-in-idempotent.json new file mode 100644 index 00000000000..9b99fb81216 --- /dev/null +++ b/src/libstore-tests/data/derivation/invariants/filled-in-idempotent.json @@ -0,0 +1,20 @@ +{ + "args": [], + "builder": "/bin/sh", + "env": { + "__doc": "Correct path stays unchanged", + "out": "/nix/store/w4bk7hpyxzgy2gx8fsa8f952435pll3i-filled-in-already" + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "name": "filled-in-already", + "outputs": { + "out": { + "path": "w4bk7hpyxzgy2gx8fsa8f952435pll3i-filled-in-already" + } + }, + "system": "x86_64-linux", + "version": 4 +} diff --git a/src/libstore-tests/derivation.cc b/src/libstore-tests/derivation/external-formats.cc similarity index 91% rename from src/libstore-tests/derivation.cc rename to src/libstore-tests/derivation/external-formats.cc index 6b33e5442f1..056eeaa8a96 100644 --- a/src/libstore-tests/derivation.cc +++ b/src/libstore-tests/derivation/external-formats.cc @@ -1,57 +1,14 @@ #include #include -#include "nix/util/experimental-features.hh" #include "nix/store/derivations.hh" - -#include "nix/store/tests/libstore.hh" +#include "derivation/test-support.hh" #include "nix/util/tests/json-characterization.hh" namespace nix { using nlohmann::json; -class DerivationTest : public virtual CharacterizationTest, public LibStoreTest -{ - std::filesystem::path unitTestData = getUnitTestData() / "derivation"; - -public: - std::filesystem::path goldenMaster(std::string_view testStem) const override - { - return unitTestData / testStem; - } - - /** - * We set these in tests rather than the regular globals so we don't have - * to worry about race conditions if the tests run concurrently. - */ - ExperimentalFeatureSettings mockXpSettings; -}; - -class CaDerivationTest : public DerivationTest -{ - void SetUp() override - { - mockXpSettings.set("experimental-features", "ca-derivations"); - } -}; - -class DynDerivationTest : public DerivationTest -{ - void SetUp() override - { - mockXpSettings.set("experimental-features", "dynamic-derivations ca-derivations"); - } -}; - -class ImpureDerivationTest : public DerivationTest -{ - void SetUp() override - { - mockXpSettings.set("experimental-features", "impure-derivations"); - } -}; - TEST_F(DerivationTest, BadATerm_version) { ASSERT_THROW( diff --git a/src/libstore-tests/derivation/invariants.cc b/src/libstore-tests/derivation/invariants.cc new file mode 100644 index 00000000000..6d7cee968d1 --- /dev/null +++ b/src/libstore-tests/derivation/invariants.cc @@ -0,0 +1,264 @@ +#include +#include + +#include "nix/store/derivations.hh" +#include "nix/store/tests/libstore.hh" +#include "nix/store/dummy-store-impl.hh" +#include "nix/util/tests/json-characterization.hh" + +#include "derivation/test-support.hh" + +namespace nix { + +class FillInOutputPathsTest : public LibStoreTest, public JsonCharacterizationTest +{ + std::filesystem::path unitTestData = getUnitTestData() / "derivation" / "invariants"; + +protected: + FillInOutputPathsTest() + : LibStoreTest([]() { + auto config = make_ref(DummyStoreConfig::Params{}); + config->readOnly = false; + return config->openDummyStore(); + }()) + { + } + + /** + * Create a CA floating output derivation and write it to the store. + * This is useful for creating dependencies that will cause downstream + * derivations to remain deferred. + */ + StorePath makeCAFloatingDependency(std::string_view name) + { + Derivation depDrv; + depDrv.name = name; + depDrv.platform = "x86_64-linux"; + depDrv.builder = "/bin/sh"; + depDrv.outputs = { + { + "out", + // will ensure that downstream is deferred + DerivationOutput{DerivationOutput::CAFloating{ + .method = ContentAddressMethod::Raw::NixArchive, + .hashAlgo = HashAlgorithm::SHA256, + }}, + }, + }; + depDrv.env = {{"out", ""}}; + + // Fill in the dependency derivation's output paths + depDrv.fillInOutputPaths(*store); + + // Write the dependency to the store + return writeDerivation(*store, depDrv, NoRepair); + } + +public: + std::filesystem::path goldenMaster(std::string_view testStem) const override + { + return unitTestData / testStem; + } +}; + +TEST_F(FillInOutputPathsTest, fillsDeferredOutputs_emptyStringEnvVar) +{ + using nlohmann::json; + + // Before: Derivation with deferred output + Derivation drv; + drv.name = "filled-in-deferred-empty-env-var"; + drv.platform = "x86_64-linux"; + drv.builder = "/bin/sh"; + drv.outputs = { + {"out", DerivationOutput{DerivationOutput::Deferred{}}}, + }; + drv.env = {{"__doc", "Fill in deferred output with empty env var"}, {"out", ""}}; + + // Serialize before state + checkpointJson("filled-in-deferred-empty-env-var-pre", drv); + + drv.fillInOutputPaths(*store); + + // Serialize after state + checkpointJson("filled-in-deferred-empty-env-var-post", drv); + + // After: Should have been converted to InputAddressed + auto * outputP = std::get_if(&drv.outputs.at("out").raw); + ASSERT_TRUE(outputP); + auto & output = *outputP; + + // Environment variable should be filled in + EXPECT_EQ(drv.env.at("out"), store->printStorePath(output.path)); +} + +TEST_F(FillInOutputPathsTest, fillsDeferredOutputs_empty_string_var) +{ + using nlohmann::json; + + // Before: Derivation with deferred output + Derivation drv; + drv.name = "filled-in-deferred-no-env-var"; + drv.platform = "x86_64-linux"; + drv.builder = "/bin/sh"; + drv.outputs = { + {"out", DerivationOutput{DerivationOutput::Deferred{}}}, + }; + drv.env = { + {"__doc", "Fill in deferred with missing env var"}, + }; + + // Serialize before state + checkpointJson("filled-in-deferred-no-env-var-pre", drv); + + drv.fillInOutputPaths(*store); + + // Serialize after state + checkpointJson("filled-in-deferred-no-env-var-post", drv); + + // After: Should have been converted to InputAddressed + auto * outputP = std::get_if(&drv.outputs.at("out").raw); + ASSERT_TRUE(outputP); + auto & output = *outputP; + + // Environment variable should be filled in + EXPECT_EQ(drv.env.at("out"), store->printStorePath(output.path)); +} + +TEST_F(FillInOutputPathsTest, preservesInputAddressedOutputs) +{ + auto expectedPath = StorePath{"w4bk7hpyxzgy2gx8fsa8f952435pll3i-filled-in-already"}; + + Derivation drv; + drv.name = "filled-in-already"; + drv.platform = "x86_64-linux"; + drv.builder = "/bin/sh"; + drv.outputs = { + {"out", DerivationOutput{DerivationOutput::InputAddressed{.path = expectedPath}}}, + }; + drv.env = { + {"__doc", "Correct path stays unchanged"}, + {"out", store->printStorePath(expectedPath)}, + }; + + // Serialize before state + checkpointJson("filled-in-idempotent", drv); + + auto drvBefore = drv; + + drv.fillInOutputPaths(*store); + + // Should still be no change + EXPECT_EQ(drv, drvBefore); +} + +TEST_F(FillInOutputPathsTest, throwsOnIncorrectInputAddressedPath) +{ + auto wrongPath = StorePath{"c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-wrong-name"}; + + Derivation drv; + drv.name = "bad-path"; + drv.platform = "x86_64-linux"; + drv.builder = "/bin/sh"; + drv.outputs = { + {"out", DerivationOutput{DerivationOutput::InputAddressed{.path = wrongPath}}}, + }; + drv.env = { + {"__doc", "Wrong InputAddressed path throws error"}, + {"out", store->printStorePath(wrongPath)}, + }; + + // Serialize before state + checkpointJson("bad-path", drv); + + ASSERT_THROW(drv.fillInOutputPaths(*store), Error); +} + +TEST_F(FillInOutputPathsTest, throwsOnIncorrectEnvVar) +{ + auto wrongPath = StorePath{"c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-wrong-name"}; + + Derivation drv; + drv.name = "bad-env-var"; + drv.platform = "x86_64-linux"; + drv.builder = "/bin/sh"; + drv.outputs = { + {"out", DerivationOutput{DerivationOutput::Deferred{}}}, + }; + drv.env = { + {"__doc", "Wrong env var value throws error"}, + {"out", store->printStorePath(wrongPath)}, + }; + + // Serialize before state + checkpointJson("bad-env-var", drv); + + ASSERT_THROW(drv.fillInOutputPaths(*store), Error); +} + +TEST_F(FillInOutputPathsTest, preservesDeferredWithInputDrvs) +{ + using nlohmann::json; + + // Create a CA floating dependency derivation + auto depDrvPath = makeCAFloatingDependency("dependency"); + + // Create a derivation that depends on the dependency + Derivation drv; + drv.name = "depends-on-drv"; + drv.platform = "x86_64-linux"; + drv.builder = "/bin/sh"; + drv.outputs = { + {"out", DerivationOutput{DerivationOutput::Deferred{}}}, + }; + drv.env = { + {"__doc", "Deferred stays deferred with CA dependencies"}, + {"out", ""}, + }; + // Add the real input derivation dependency + drv.inputDrvs = {.map = {{depDrvPath, {.value = {"out"}}}}}; + + // Serialize before state + checkpointJson("depends-on-drv-pre", drv); + + auto drvBefore = drv; + + // Apply fillInOutputPaths + drv.fillInOutputPaths(*store); + + // Derivation should be unchanged + EXPECT_EQ(drv, drvBefore); +} + +TEST_F(FillInOutputPathsTest, throwsOnPatWhenShouldBeDeffered) +{ + using nlohmann::json; + + // Create a CA floating dependency derivation + auto depDrvPath = makeCAFloatingDependency("dependency"); + + auto wrongPath = StorePath{"c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-wrong-name"}; + + // Create a derivation that depends on the dependency + Derivation drv; + drv.name = "depends-on-drv"; + drv.platform = "x86_64-linux"; + drv.builder = "/bin/sh"; + drv.outputs = { + {"out", DerivationOutput{DerivationOutput::InputAddressed{.path = wrongPath}}}, + }; + drv.env = { + {"__doc", "InputAddressed throws when should be deferred"}, + {"out", ""}, + }; + // Add the real input derivation dependency + drv.inputDrvs = {.map = {{depDrvPath, {.value = {"out"}}}}}; + + // Serialize before state + checkpointJson("bad-depends-on-drv-pre", drv); + + // Apply fillInOutputPaths + ASSERT_THROW(drv.fillInOutputPaths(*store), Error); +} + +} // namespace nix diff --git a/src/libstore-tests/derivation/test-support.hh b/src/libstore-tests/derivation/test-support.hh new file mode 100644 index 00000000000..f48e6caef58 --- /dev/null +++ b/src/libstore-tests/derivation/test-support.hh @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include "nix/util/experimental-features.hh" +#include "nix/store/tests/libstore.hh" +#include "nix/util/tests/characterization.hh" + +namespace nix { + +class DerivationTest : public virtual CharacterizationTest, public LibStoreTest +{ + std::filesystem::path unitTestData = getUnitTestData() / "derivation"; + +public: + std::filesystem::path goldenMaster(std::string_view testStem) const override + { + return unitTestData / testStem; + } + + /** + * We set these in tests rather than the regular globals so we don't have + * to worry about race conditions if the tests run concurrently. + */ + ExperimentalFeatureSettings mockXpSettings; +}; + +class CaDerivationTest : public DerivationTest +{ + void SetUp() override + { + mockXpSettings.set("experimental-features", "ca-derivations"); + } +}; + +class DynDerivationTest : public DerivationTest +{ + void SetUp() override + { + mockXpSettings.set("experimental-features", "dynamic-derivations ca-derivations"); + } +}; + +class ImpureDerivationTest : public DerivationTest +{ + void SetUp() override + { + mockXpSettings.set("experimental-features", "impure-derivations"); + } +}; + +} // namespace nix diff --git a/src/libstore-tests/meson.build b/src/libstore-tests/meson.build index f76df8bcbb3..58f624611a4 100644 --- a/src/libstore-tests/meson.build +++ b/src/libstore-tests/meson.build @@ -58,7 +58,8 @@ sources = files( 'common-protocol.cc', 'content-address.cc', 'derivation-advanced-attrs.cc', - 'derivation.cc', + 'derivation/external-formats.cc', + 'derivation/invariants.cc', 'derived-path.cc', 'downstream-placeholder.cc', 'dummy-store.cc', diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index f96109a6ced..66e25732ee5 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -1210,6 +1210,136 @@ std::optional Derivation::tryResolve( return resolved; } +/** + * Process `InputAddressed`, `Deferred`, and `CAFixed` outputs. + * + * For `InputAddressed` outputs or `Deferred` outputs: + * + * - with `Regular` hash kind, validate `InputAddressed` outputs have + * the correct path (throws if mismatch). For `Deferred` outputs: + * - if `fillIn` is true, fill in the output path to make `InputAddressed` + * - if `fillIn` is false, throw an error + * Then validate or fill in the environment variable with the path. + * + * - with `Deferred` hash kind, validate that the output is either + * `InputAddressed` (error) or `Deferred` (correct). + * + * For `CAFixed` outputs, validate or fill in the environment variable + * with the computed path. + * + * @tparam fillIn If true, fill in missing output paths and environment + * variables. If false, validate that all paths are correct (throws on + * mismatch). + */ +template +static void processDerivationOutputPaths(Store & store, auto && drv, std::string_view drvName) +{ + std::optional hashesModulo; + + for (auto & [outputName, output] : drv.outputs) { + auto envHasRightPath = [&](const StorePath & actual) { + if constexpr (fillIn) { + auto j = drv.env.find(outputName); + /* Fill in mode: fill in missing or empty environment + variables */ + if (j == drv.env.end()) + drv.env.insert(j, {outputName, store.printStorePath(actual)}); + else if (j->second == "") + j->second = store.printStorePath(actual); + /* We know validation will succeed after fill-in, but + just to be extra sure, validate unconditionally */ + } + auto j = drv.env.find(outputName); + if (j == drv.env.end()) + throw Error( + "derivation has missing environment variable '%s', should be '%s' but is not present", + outputName, + store.printStorePath(actual)); + if (j->second != store.printStorePath(actual)) + throw Error( + "derivation has incorrect environment variable '%s', should be '%s' but is actually '%s'", + outputName, + store.printStorePath(actual), + j->second); + }; + auto hash = [&](const Output & outputVariant) { + if (!hashesModulo) { + // somewhat expensive so we do lazily + hashesModulo = hashDerivationModulo(store, drv, true); + } + switch (hashesModulo->kind) { + case DrvHash::Kind::Regular: { + auto h = get(hashesModulo->hashes, outputName); + if (!h) + throw Error("derivation produced no hash for output '%s'", outputName); + auto outPath = store.makeOutputPath(outputName, *h, drvName); + + if constexpr (std::is_same_v) { + if (outputVariant.path == outPath) { + return; // Correct case + } + /* Error case, an explicitly wrong path is + always an error. */ + throw Error( + "derivation has incorrect output '%s', should be '%s'", + store.printStorePath(outputVariant.path), + store.printStorePath(outPath)); + } else if constexpr (std::is_same_v) { + if constexpr (fillIn) + /* Fill in output path for Deferred + outputs */ + output = DerivationOutput::InputAddressed{ + .path = outPath, + }; + else + /* Validation mode: deferred outputs + should have been filled in */ + throw Error( + "derivation has incorrect deferred output, should be '%s'", store.printStorePath(outPath)); + } else { + /* Will never happen, based on where + `hash` is called. */ + static_assert(false); + } + envHasRightPath(outPath); + break; + } + case DrvHash::Kind::Deferred: + if constexpr (std::is_same_v) { + /* Error case, an explicitly wrong path is + always an error. */ + throw Error( + "derivation has incorrect output '%s', should be deferred", + store.printStorePath(outputVariant.path)); + } else if constexpr (std::is_same_v) { + /* Correct: Deferred output with Deferred + hash kind. */ + } else { + /* Will never happen, based on where + `hash` is called. */ + static_assert(false); + } + break; + } + }; + std::visit( + overloaded{ + [&](const DerivationOutput::InputAddressed & o) { hash(o); }, + [&](const DerivationOutput::Deferred & o) { hash(o); }, + [&](const DerivationOutput::CAFixed & dof) { envHasRightPath(dof.path(store, drvName, outputName)); }, + [&](const auto &) { + // Nothing to do for other output types + }, + }, + output.raw); + } + + /* Don't need the answer, but do this anyways to assert is proper + combination. The code above is more general and naturally allows + combinations that are currently prohibited. */ + drv.type(); +} + void Derivation::checkInvariants(Store & store, const StorePath & drvPath) const { assert(drvPath.isDerivation()); @@ -1217,65 +1347,41 @@ void Derivation::checkInvariants(Store & store, const StorePath & drvPath) const drvName = drvName.substr(0, drvName.size() - drvExtension.size()); if (drvName != name) { - throw Error("Derivation '%s' has name '%s' which does not match its path", store.printStorePath(drvPath), name); + throw Error("derivation '%s' has name '%s' which does not match its path", store.printStorePath(drvPath), name); } - auto envHasRightPath = [&](const StorePath & actual, const std::string & varName) { - auto j = env.find(varName); - if (j == env.end() || store.parseStorePath(j->second) != actual) - throw Error( - "derivation '%s' has incorrect environment variable '%s', should be '%s'", - store.printStorePath(drvPath), - varName, - store.printStorePath(actual)); - }; + try { + checkInvariants(store); + } catch (Error & e) { + e.addTrace({}, "while checking derivation '%s'", store.printStorePath(drvPath)); + throw; + } +} - // Don't need the answer, but do this anyways to assert is proper - // combination. The code below is more general and naturally allows - // combinations that are currently prohibited. - type(); +void Derivation::checkInvariants(Store & store) const +{ + processDerivationOutputPaths(store, *this, name); +} - std::optional hashesModulo; - for (auto & i : outputs) { - std::visit( - overloaded{ - [&](const DerivationOutput::InputAddressed & doia) { - if (!hashesModulo) { - // somewhat expensive so we do lazily - hashesModulo = hashDerivationModulo(store, *this, true); - } - auto currentOutputHash = get(hashesModulo->hashes, i.first); - if (!currentOutputHash) - throw Error( - "derivation '%s' has unexpected output '%s' (local-store / hashesModulo) named '%s'", - store.printStorePath(drvPath), - store.printStorePath(doia.path), - i.first); - StorePath recomputed = store.makeOutputPath(i.first, *currentOutputHash, drvName); - if (doia.path != recomputed) - throw Error( - "derivation '%s' has incorrect output '%s', should be '%s'", - store.printStorePath(drvPath), - store.printStorePath(doia.path), - store.printStorePath(recomputed)); - envHasRightPath(doia.path, i.first); - }, - [&](const DerivationOutput::CAFixed & dof) { - auto path = dof.path(store, drvName, i.first); - envHasRightPath(path, i.first); - }, - [&](const DerivationOutput::CAFloating &) { - /* Nothing to check */ - }, - [&](const DerivationOutput::Deferred &) { - /* Nothing to check */ - }, - [&](const DerivationOutput::Impure &) { - /* Nothing to check */ - }, - }, - i.second.raw); +void Derivation::fillInOutputPaths(Store & store) +{ + processDerivationOutputPaths(store, *this, name); +} + +Derivation Derivation::parseJsonAndValidate(Store & store, const nlohmann::json & json) +{ + auto drv = static_cast(json); + + drv.fillInOutputPaths(store); + + try { + drv.checkInvariants(store); + } catch (Error & e) { + e.addTrace({}, "while checking derivation from JSON with name '%s'", drv.name); + throw; } + + return drv; } const Hash impureOutputHash = hashString(HashAlgorithm::SHA256, "impure"); diff --git a/src/libstore/include/nix/store/derivations.hh b/src/libstore/include/nix/store/derivations.hh index 259314d3fcf..aa33245e31f 100644 --- a/src/libstore/include/nix/store/derivations.hh +++ b/src/libstore/include/nix/store/derivations.hh @@ -368,9 +368,48 @@ struct Derivation : BasicDerivation * This is mainly a matter of checking the outputs, where our C++ * representation supports all sorts of combinations we do not yet * allow. + * + * This overload does not validate the derivation name or add path + * context to errors. Use this when you don't have a `StorePath` or + * when you want to handle error context yourself. + * + * @param store The store to use for validation + */ + void checkInvariants(Store & store) const; + + /** + * This overload does everything the base `checkInvariants` does, + * but also validates that the derivation name matches the path, and + * improves any error messages that occur using the derivation path. + * + * @param store The store to use for validation + * @param drvPath The path to this derivation */ void checkInvariants(Store & store, const StorePath & drvPath) const; + /** + * Fill in output paths as needed. + * + * For input-addressed derivations (ready or deferred), it computes + * the derivation hash modulo and based on the result: + * + * - If `Regular`: converts `Deferred` outputs to `InputAddressed`, + * and ensures all `InputAddressed` outputs (whether preexisting + * or newly computed) have the right computed paths. Likewise + * defines (if absent or the empty string) or checks (if + * preexisting and non-empty) environment variables for each + * output with their path. + * + * - If `Deferred`: converts `InputAddressed` to `Deferred`. + * + * Also for fixed-output content-addressed derivations, likewise + * updates output paths in env vars. + * + * @param store The store to use for path computation + * @param drvName The derivation name (without .drv extension) + */ + void fillInOutputPaths(Store & store); + Derivation() = default; Derivation(const BasicDerivation & bd) @@ -383,6 +422,29 @@ struct Derivation : BasicDerivation { } + /** + * Parse a derivation from JSON, and also perform various + * conveniences such as: + * + * 1. Filling in output paths in as needed/required. + * + * 2. Checking invariants in general. + * + * In the future it might also do things like: + * + * - assist with the migration from older JSON formats. + * + * - (a somewhat example of the above) initialize + * `DerivationOptions` from their traditional encoding inside the + * `env` and `structuredAttrs`. + * + * @param store The store to use for path computation and validation + * @param json The JSON representation of the derivation + * @return A validated derivation with output paths filled in + * @throws Error if parsing fails, output paths can't be computed, or validation fails + */ + static Derivation parseJsonAndValidate(Store & store, const nlohmann::json & json); + bool operator==(const Derivation &) const = default; // TODO libc++ 16 (used by darwin) missing `std::map::operator <=>`, can't do yet. // auto operator <=> (const Derivation &) const = default; diff --git a/src/nix/derivation-add.cc b/src/nix/derivation-add.cc index 48e935092f2..bbaa8759715 100644 --- a/src/nix/derivation-add.cc +++ b/src/nix/derivation-add.cc @@ -33,12 +33,10 @@ struct CmdAddDerivation : MixDryRun, StoreCommand { auto json = nlohmann::json::parse(drainFD(STDIN_FILENO)); - auto drv = static_cast(json); + auto drv = Derivation::parseJsonAndValidate(*store, json); auto drvPath = writeDerivation(*store, drv, NoRepair, /* read only */ dryRun); - drv.checkInvariants(*store, drvPath); - writeDerivation(*store, drv, NoRepair, dryRun); logger->cout("%s", store->printStorePath(drvPath)); diff --git a/tests/functional/derivation-json.sh b/tests/functional/derivation-json.sh index 06f934cfe0a..63c11440394 100755 --- a/tests/functional/derivation-json.sh +++ b/tests/functional/derivation-json.sh @@ -4,11 +4,20 @@ source common.sh drvPath=$(nix-instantiate simple.nix) -nix derivation show "$drvPath" | jq .[] > "$TEST_HOME"/simple.json - -drvPath2=$(nix derivation add < "$TEST_HOME"/simple.json) +nix derivation show "$drvPath" | jq '.[]' > "$TEST_HOME/simple.json" +# Round tripping to JSON works +drvPath2=$(nix derivation add < "$TEST_HOME/simple.json") [[ "$drvPath" = "$drvPath2" ]] +# Derivation is input addressed, all outputs have a path +jq -e '.outputs | .[] | has("path")' < "$TEST_HOME/simple.json" + # Input addressed derivations cannot be renamed. -jq '.name = "foo"' < "$TEST_HOME"/simple.json | expectStderr 1 nix derivation add | grepQuiet "has incorrect output" +jq '.name = "foo"' < "$TEST_HOME/simple.json" | expectStderr 1 nix derivation add | grepQuiet "has incorrect output" + +# If we remove the input addressed to make it a deferred derivation, we +# still get the same result because Nix will see that need not be +# deferred and fill in the right input address for us. +drvPath3=$(jq '.outputs |= map_values(del(.path))' < "$TEST_HOME/simple.json" | nix derivation add) +[[ "$drvPath" = "$drvPath3" ]] From 5831567232fa3192f682c0d84323a4ba7f240c83 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 17 Nov 2025 12:14:39 -0500 Subject: [PATCH 3/3] libstore-c: Add new derivation and store path functions Add several new functions to the C API: StorePath operations: - nix_store_path_hash: Extract the hash part from a store path - nix_store_create_from_parts: Construct a store path from hash and name Derivation operations: - nix_derivation_clone: Clone a derivation - nix_derivation_to_json: Serialize a derivation to JSON Store operations: - nix_store_drv_from_store_path: Load a derivation from a store path Test the new functions, and improve documentation of some existing functions to better distinguish them, also. Co-authored-by: Tristan Ross Co-authored-by: Robert Hensing --- src/libstore-c/nix_api_store.cc | 69 ++++++++++ src/libstore-c/nix_api_store.h | 18 ++- src/libstore-c/nix_api_store/derivation.h | 19 +++ src/libstore-c/nix_api_store/store_path.h | 42 ++++++ src/libstore-tests/nix_api_store.cc | 158 ++++++++++++++++++++++ 5 files changed, 305 insertions(+), 1 deletion(-) diff --git a/src/libstore-c/nix_api_store.cc b/src/libstore-c/nix_api_store.cc index b95e5b749f5..fd5287a1b99 100644 --- a/src/libstore-c/nix_api_store.cc +++ b/src/libstore-c/nix_api_store.cc @@ -10,6 +10,10 @@ #include "nix/store/local-fs-store.hh" #include "nix/store/globals.hh" +#include "nix/util/base-nix-32.hh" + +#include +#include extern "C" { @@ -218,6 +222,47 @@ StorePath * nix_store_path_clone(const StorePath * p) return new StorePath{p->path}; } +nix_err +nix_store_path_hash(nix_c_context * context, const StorePath * store_path, nix_store_path_hash_part * hash_part_out) +{ + try { + auto hashPart = store_path->path.hashPart(); + // Decode from Nix32 (base32) encoding to raw bytes + auto decoded = nix::BaseNix32::decode(hashPart); + + assert(decoded.size() == 20); + std::memcpy(hash_part_out->bytes, decoded.data(), 20); + return NIX_OK; + } + NIXC_CATCH_ERRS +} + +StorePath * nix_store_create_from_parts( + nix_c_context * context, const nix_store_path_hash_part * hash, const char * name, size_t name_len) +{ + if (context) + context->last_err_code = NIX_OK; + try { + // Encode the 20 raw bytes to Nix32 (base32) format + auto hashStr = + nix::BaseNix32::encode(std::span(reinterpret_cast(hash->bytes), 20)); + + // Construct the store path basename: - + std::string baseName; + baseName += hashStr; + baseName += "-"; + baseName += std::string_view{name, name_len}; + + return new StorePath{nix::StorePath(std::move(baseName))}; + } + NIXC_CATCH_ERRS_NULL +} + +nix_derivation * nix_derivation_clone(const nix_derivation * d) +{ + return new nix_derivation{d->drv}; +} + nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store, const char * json) { if (context) @@ -228,6 +273,20 @@ nix_derivation * nix_derivation_from_json(nix_c_context * context, Store * store NIXC_CATCH_ERRS_NULL } +nix_err nix_derivation_to_json( + nix_c_context * context, const nix_derivation * drv, nix_get_string_callback callback, void * userdata) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto result = static_cast(drv->drv).dump(); + if (callback) { + callback(result.data(), result.size(), userdata); + } + } + NIXC_CATCH_ERRS +} + StorePath * nix_add_derivation(nix_c_context * context, Store * store, nix_derivation * derivation) { if (context) @@ -252,4 +311,14 @@ nix_err nix_store_copy_closure(nix_c_context * context, Store * srcStore, Store NIXC_CATCH_ERRS } +nix_derivation * nix_store_drv_from_store_path(nix_c_context * context, Store * store, const StorePath * path) +{ + if (context) + context->last_err_code = NIX_OK; + try { + return new nix_derivation{store->ptr->derivationFromPath(path->path)}; + } + NIXC_CATCH_ERRS_NULL +} + } // extern "C" diff --git a/src/libstore-c/nix_api_store.h b/src/libstore-c/nix_api_store.h index 9eaa61a920f..ade8b0e192a 100644 --- a/src/libstore-c/nix_api_store.h +++ b/src/libstore-c/nix_api_store.h @@ -106,7 +106,7 @@ nix_err nix_store_get_storedir(nix_c_context * context, Store * store, nix_get_string_callback callback, void * user_data); /** - * @brief Parse a Nix store path into a StorePath + * @brief Parse a Nix store path that includes the store dir into a StorePath * * @note Don't forget to free this path using nix_store_path_free()! * @param[out] context Optional, stores error information @@ -188,6 +188,12 @@ nix_store_get_version(nix_c_context * context, Store * store, nix_get_string_cal /** * @brief Create a `nix_derivation` from a JSON representation of that derivation. * + * @note Unlike `nix_derivation_to_json`, this needs a `Store`. This is because + * over time we expect the internal representation of derivations in Nix to + * differ from accepted derivation formats. The store argument is here to help + * any logic needed to convert from JSON to the internal representation, in + * excess of just parsing. + * * @param[out] context Optional, stores error information. * @param[in] store nix store reference. * @param[in] json JSON of the derivation as a string. @@ -242,6 +248,16 @@ nix_err nix_store_get_fs_closure( void * userdata, void (*callback)(nix_c_context * context, void * userdata, const StorePath * store_path)); +/** + * @brief Returns the derivation associated with the store path + * + * @param[out] context Optional, stores error information + * @param[in] store The nix store + * @param[in] path The nix store path + * @return A new derivation, or NULL on error + */ +nix_derivation * nix_store_drv_from_store_path(nix_c_context * context, Store * store, const StorePath * path); + // cffi end #ifdef __cplusplus } diff --git a/src/libstore-c/nix_api_store/derivation.h b/src/libstore-c/nix_api_store/derivation.h index 9c42cfd60cb..239ffd52f8b 100644 --- a/src/libstore-c/nix_api_store/derivation.h +++ b/src/libstore-c/nix_api_store/derivation.h @@ -20,6 +20,14 @@ extern "C" { /** @brief Nix Derivation */ typedef struct nix_derivation nix_derivation; +/** + * @brief Copy a `nix_derivation` + * + * @param[in] d the derivation to copy + * @return a new `nix_derivation` + */ +nix_derivation * nix_derivation_clone(const nix_derivation * d); + /** * @brief Deallocate a `nix_derivation` * @@ -28,6 +36,17 @@ typedef struct nix_derivation nix_derivation; */ void nix_derivation_free(nix_derivation * drv); +/** + * @brief Gets the derivation as a JSON string + * + * @param[out] context Optional, stores error information + * @param[in] drv The derivation + * @param[in] callback Called with the JSON string + * @param[in] userdata Arbitrary data passed to the callback + */ +nix_err nix_derivation_to_json( + nix_c_context * context, const nix_derivation * drv, nix_get_string_callback callback, void * userdata); + // cffi end #ifdef __cplusplus } diff --git a/src/libstore-c/nix_api_store/store_path.h b/src/libstore-c/nix_api_store/store_path.h index 9f3717aeadf..1aa9bcac7e4 100644 --- a/src/libstore-c/nix_api_store/store_path.h +++ b/src/libstore-c/nix_api_store/store_path.h @@ -10,6 +10,9 @@ * @brief Store path operations */ +#include +#include + #include "nix_api_util.h" #ifdef __cplusplus @@ -44,6 +47,45 @@ void nix_store_path_free(StorePath * p); */ void nix_store_path_name(const StorePath * store_path, nix_get_string_callback callback, void * user_data); +/** + * @brief A store path hash + * + * Once decoded from "nix32" encoding, a store path hash is 20 raw bytes. + */ +typedef struct nix_store_path_hash_part +{ + uint8_t bytes[20]; +} nix_store_path_hash_part; + +/** + * @brief Get the path hash (e.g. "" in /nix/store/-) + * + * The hash is returned as raw bytes, decoded from "nix32" encoding. + * + * @param[out] context Optional, stores error information + * @param[in] store_path the path to get the hash from + * @param[out] hash_part_out the decoded hash as 20 raw bytes + * @return NIX_OK on success, error code on failure + */ +nix_err +nix_store_path_hash(nix_c_context * context, const StorePath * store_path, nix_store_path_hash_part * hash_part_out); + +/** + * @brief Create a StorePath from its constituent parts (hash and name) + * + * This function constructs a store path from a hash and name, without needing + * a Store reference or the store directory prefix. + * + * @note Don't forget to free this path using nix_store_path_free()! + * @param[out] context Optional, stores error information + * @param[in] hash The store path hash (20 raw bytes) + * @param[in] name The store path name (the part after the hash) + * @param[in] name_len Length of the name string + * @return owned store path, NULL on error + */ +StorePath * nix_store_create_from_parts( + nix_c_context * context, const nix_store_path_hash_part * hash, const char name[/*name_len*/], size_t name_len); + // cffi end #ifdef __cplusplus } diff --git a/src/libstore-tests/nix_api_store.cc b/src/libstore-tests/nix_api_store.cc index bf411053acc..ecf0913e63d 100644 --- a/src/libstore-tests/nix_api_store.cc +++ b/src/libstore-tests/nix_api_store.cc @@ -1,5 +1,7 @@ #include +#include + #include "nix_api_util.h" #include "nix_api_store.h" @@ -92,6 +94,69 @@ TEST_F(nix_api_store_test, DoesNotCrashWhenContextIsNull) nix_store_path_free(path); } +TEST_F(nix_api_store_test, nix_store_path_hash) +{ + StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + ASSERT_NE(path, nullptr); + + nix_store_path_hash_part hash; + auto ret = nix_store_path_hash(ctx, path, &hash); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + + // Verify it's 20 bytes + static_assert(sizeof(hash.bytes) == 20); + + // The hash should be non-zero + bool allZero = true; + for (int i = 0; i < 20; i++) { + if (hash.bytes[i] != 0) { + allZero = false; + break; + } + } + ASSERT_FALSE(allZero); + + nix_store_path_free(path); +} + +TEST_F(nix_api_store_test, nix_store_create_from_parts_roundtrip) +{ + // Parse a path + StorePath * original = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + EXPECT_NE(original, nullptr); + + // Get its hash + nix_store_path_hash_part hash; + auto ret = nix_store_path_hash(ctx, original, &hash); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + + // Get its name + std::string name; + nix_store_path_name(original, OBSERVE_STRING(name)); + + // Reconstruct from parts + StorePath * reconstructed = nix_store_create_from_parts(ctx, &hash, name.c_str(), name.size()); + assert_ctx_ok(); + ASSERT_NE(reconstructed, nullptr); + + // Should be equal + EXPECT_EQ(original->path, reconstructed->path); + + nix_store_path_free(original); + nix_store_path_free(reconstructed); +} + +TEST_F(nix_api_store_test, nix_store_create_from_parts_invalid_name) +{ + nix_store_path_hash_part hash = {}; + // Invalid name with spaces + StorePath * path = nix_store_create_from_parts(ctx, &hash, "invalid name", 12); + ASSERT_EQ(path, nullptr); + ASSERT_EQ(nix_err_code(ctx), NIX_ERR_NIX_ERROR); +} + TEST_F(nix_api_store_test, get_version) { std::string str; @@ -795,4 +860,97 @@ TEST_F(NixApiStoreTestWithRealisedPath, nix_store_get_fs_closure_error_propagati ASSERT_EQ(call_count, 1); // Should have been called exactly once, then aborted } +/** + * @brief Helper function to load JSON from a test data file + * + * @param filename Relative path from _NIX_TEST_UNIT_DATA + * @return JSON string contents of the file + */ +static std::string load_json_from_test_data(const char * filename) +{ + std::filesystem::path unitTestData{getenv("_NIX_TEST_UNIT_DATA")}; + std::ifstream t{unitTestData / filename}; + std::stringstream buffer; + buffer << t.rdbuf(); + return buffer.str(); +} + +TEST_F(nix_api_store_test, nix_derivation_to_json_roundtrip) +{ + // Load JSON from test data + auto originalJson = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json"); + + // Parse to derivation + auto * drv = nix_derivation_from_json(ctx, store, originalJson.c_str()); + assert_ctx_ok(); + ASSERT_NE(drv, nullptr); + + // Convert back to JSON + std::string convertedJson; + auto ret = nix_derivation_to_json(ctx, drv, OBSERVE_STRING(convertedJson)); + assert_ctx_ok(); + ASSERT_EQ(ret, NIX_OK); + ASSERT_FALSE(convertedJson.empty()); + + // Parse both JSON strings to compare (ignoring whitespace differences) + auto originalParsed = nlohmann::json::parse(originalJson); + auto convertedParsed = nlohmann::json::parse(convertedJson); + + // Remove parts that will be different due to filling-in. + originalParsed.at("outputs").erase("out"); + originalParsed.at("env").erase("out"); + convertedParsed.at("outputs").erase("out"); + convertedParsed.at("env").erase("out"); + + // They should be equivalent + ASSERT_EQ(originalParsed, convertedParsed); + + nix_derivation_free(drv); +} + +TEST_F(nix_api_store_test, nix_derivation_store_round_trip) +{ + // Load a derivation from JSON + auto json = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json"); + auto * drv = nix_derivation_from_json(ctx, store, json.c_str()); + assert_ctx_ok(); + ASSERT_NE(drv, nullptr); + + // Add to store + auto * drvPath = nix_add_derivation(ctx, store, drv); + assert_ctx_ok(); + ASSERT_NE(drvPath, nullptr); + + // Retrieve from store + auto * drv2 = nix_store_drv_from_store_path(ctx, store, drvPath); + assert_ctx_ok(); + ASSERT_NE(drv2, nullptr); + + // The round trip should make the same derivation + ASSERT_EQ(drv->drv, drv2->drv); + + nix_store_path_free(drvPath); + nix_derivation_free(drv); + nix_derivation_free(drv2); +} + +TEST_F(nix_api_store_test, nix_derivation_clone) +{ + // Load a derivation from JSON + auto json = load_json_from_test_data("derivation/invariants/filled-in-deferred-empty-env-var-pre.json"); + auto * drv = nix_derivation_from_json(ctx, store, json.c_str()); + assert_ctx_ok(); + ASSERT_NE(drv, nullptr); + + // Clone the derivation + auto * drv2 = nix_derivation_clone(drv); + ASSERT_NE(drv2, nullptr); + + // The clone should be equal + ASSERT_EQ(drv->drv, drv2->drv); + + nix_derivation_free(drv); + nix_derivation_free(drv2); +} + } // namespace nixC