@@ -613,6 +613,155 @@ TEST_F(NixApiStoreTestWithRealisedPath, nix_store_get_fs_closure_include_deriver
613613 ASSERT_EQ (closure_paths.count (drvPathName), 1 ) << " Derivation should be in closure when include_derivers=true" ;
614614}
615615
616+ TEST_F (NixApiStoreTestWithRealisedPath, nix_store_realise_output_ordering)
617+ {
618+ // Test that nix_store_realise returns outputs in alphabetical order by output name.
619+ // This test uses a CA derivation with 10 outputs in randomized input order
620+ // to verify that the callback order is deterministic and alphabetical.
621+ nix::experimentalFeatureSettings.set (" extra-experimental-features" , " ca-derivations" );
622+ nix::settings.substituters = {};
623+
624+ auto * store = open_local_store ();
625+
626+ // Create a CA derivation with 10 outputs using proper placeholders
627+ auto outa_ph = nix::hashPlaceholder (" outa" );
628+ auto outb_ph = nix::hashPlaceholder (" outb" );
629+ auto outc_ph = nix::hashPlaceholder (" outc" );
630+ auto outd_ph = nix::hashPlaceholder (" outd" );
631+ auto oute_ph = nix::hashPlaceholder (" oute" );
632+ auto outf_ph = nix::hashPlaceholder (" outf" );
633+ auto outg_ph = nix::hashPlaceholder (" outg" );
634+ auto outh_ph = nix::hashPlaceholder (" outh" );
635+ auto outi_ph = nix::hashPlaceholder (" outi" );
636+ auto outj_ph = nix::hashPlaceholder (" outj" );
637+
638+ std::string drvJson = R"( {
639+ "version": 3,
640+ "name": "multi-output-test",
641+ "system": ")" + nix::settings.thisSystem .get ()
642+ + R"( ",
643+ "builder": "/bin/sh",
644+ "args": ["-c", "echo a > $outa; echo b > $outb; echo c > $outc; echo d > $outd; echo e > $oute; echo f > $outf; echo g > $outg; echo h > $outh; echo i > $outi; echo j > $outj"],
645+ "env": {
646+ "builder": "/bin/sh",
647+ "name": "multi-output-test",
648+ "system": ")" + nix::settings.thisSystem .get ()
649+ + R"( ",
650+ "outf": ")" + outf_ph
651+ + R"( ",
652+ "outd": ")" + outd_ph
653+ + R"( ",
654+ "outi": ")" + outi_ph
655+ + R"( ",
656+ "oute": ")" + oute_ph
657+ + R"( ",
658+ "outh": ")" + outh_ph
659+ + R"( ",
660+ "outc": ")" + outc_ph
661+ + R"( ",
662+ "outb": ")" + outb_ph
663+ + R"( ",
664+ "outg": ")" + outg_ph
665+ + R"( ",
666+ "outj": ")" + outj_ph
667+ + R"( ",
668+ "outa": ")" + outa_ph
669+ + R"( "
670+ },
671+ "inputDrvs": {},
672+ "inputSrcs": [],
673+ "outputs": {
674+ "outd": { "hashAlgo": "sha256", "method": "nar" },
675+ "outf": { "hashAlgo": "sha256", "method": "nar" },
676+ "outg": { "hashAlgo": "sha256", "method": "nar" },
677+ "outb": { "hashAlgo": "sha256", "method": "nar" },
678+ "outc": { "hashAlgo": "sha256", "method": "nar" },
679+ "outi": { "hashAlgo": "sha256", "method": "nar" },
680+ "outj": { "hashAlgo": "sha256", "method": "nar" },
681+ "outh": { "hashAlgo": "sha256", "method": "nar" },
682+ "outa": { "hashAlgo": "sha256", "method": "nar" },
683+ "oute": { "hashAlgo": "sha256", "method": "nar" }
684+ }
685+ })" ;
686+
687+ auto * drv = nix_derivation_from_json (ctx, store, drvJson.c_str ());
688+ assert_ctx_ok ();
689+ ASSERT_NE (drv, nullptr );
690+
691+ auto * drvPath = nix_add_derivation (ctx, store, drv);
692+ assert_ctx_ok ();
693+ ASSERT_NE (drvPath, nullptr );
694+
695+ // Realise the derivation - capture the order outputs are returned
696+ std::map<std::string, nix::StorePath> outputs;
697+ std::vector<std::string> output_order;
698+ auto cb = LambdaAdapter{.fun = [&](const char * outname, const StorePath * outPath) {
699+ ASSERT_NE (outname, nullptr );
700+ ASSERT_NE (outPath, nullptr );
701+ output_order.push_back (outname);
702+ outputs.emplace (outname, outPath->path );
703+ }};
704+
705+ auto ret = nix_store_realise (
706+ ctx, store, drvPath, static_cast <void *>(&cb), decltype (cb)::call_void<const char *, const StorePath *>);
707+ assert_ctx_ok ();
708+ ASSERT_EQ (ret, NIX_OK);
709+ ASSERT_EQ (outputs.size (), 10 );
710+
711+ // Verify outputs are returned in alphabetical order by output name
712+ std::vector<std::string> expected_order = {
713+ " outa" , " outb" , " outc" , " outd" , " oute" , " outf" , " outg" , " outh" , " outi" , " outj" };
714+ ASSERT_EQ (output_order, expected_order) << " Outputs should be returned in alphabetical order by output name" ;
715+
716+ // Now compute closure with include_outputs and collect paths in order
717+ struct CallbackData
718+ {
719+ std::vector<std::string> * paths;
720+ };
721+
722+ std::vector<std::string> closure_paths;
723+ CallbackData data{&closure_paths};
724+
725+ ret = nix_store_get_fs_closure (
726+ ctx,
727+ store,
728+ drvPath,
729+ false , // flip_direction
730+ true , // include_outputs - include the outputs in the closure
731+ false , // include_derivers
732+ &data,
733+ [](nix_c_context * context, void * userdata, const StorePath * path) {
734+ auto * data = static_cast <CallbackData *>(userdata);
735+ std::string path_str;
736+ nix_store_path_name (path, OBSERVE_STRING (path_str));
737+ data->paths ->push_back (path_str);
738+ });
739+ assert_ctx_ok ();
740+ ASSERT_EQ (ret, NIX_OK);
741+
742+ // Should contain at least the derivation and 10 outputs
743+ ASSERT_GE (closure_paths.size (), 11 );
744+
745+ // Verify all outputs are present in the closure
746+ for (const auto & [outname, outPath] : outputs) {
747+ std::string outPathName = store->ptr ->printStorePath (outPath);
748+
749+ bool found = false ;
750+ for (const auto & p : closure_paths) {
751+ // nix_store_path_name returns just the name part, so match against full path name
752+ if (outPathName.find (p) != std::string::npos) {
753+ found = true ;
754+ break ;
755+ }
756+ }
757+ ASSERT_TRUE (found) << " Output " << outname << " (" << outPathName << " ) not found in closure" ;
758+ }
759+
760+ nix_store_path_free (drvPath);
761+ nix_derivation_free (drv);
762+ nix_store_free (store);
763+ }
764+
616765TEST_F (NixApiStoreTestWithRealisedPath, nix_store_get_fs_closure_error_propagation)
617766{
618767 // Test that errors in the callback abort the closure computation
0 commit comments