import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; // Regression for the concurrent dependency-reset deadlock (issue #16). // // Build() resets each Module/ModulePartition's per-build `compiled` /`checked` // flags so a reused Configuration re-evaluates mtimes. That reset MUST stay // scoped to the configuration being built. It used to recurse into // cfg.dependencies — but dependency Configurations are shared across the build // DAG and each is compiled concurrently by its own Build() call. A parent's // recursive reset could therefore clear a dependency's module `compiled` flag // *after* that dependency's module-compile thread had set it true and exited, // but before an intra-config waiter (its impl / a dependent partition) ran // `compiled.wait(false)`. The waiter then blocked forever on a flag nothing // re-signalled: the build froze mid-compile, idle, with no compiler process // alive. // // This test pins the invariant deterministically, without depending on thread // timing: build a static-lib dependency fully (its module ends up compiled), // then build a consumer that depends on it while the dependency is already // cached in depResults (so it is NOT rebuilt). Before the fix, the consumer's // recursive reset flipped the dependency's module `compiled` flag back to false // and, because the dependency never rebuilds, it stayed false. After the fix // the consumer resets only its own modules, so the dependency's flag is left // intact. We assert it is still set once the consumer build completes. int main() { fs::path fixtureRoot = fs::current_path() / "tests" / "ConcurrentDependencyReset" / "fixture"; // Cold output: a stale archive/PCM from a previous run would let the // dependency build short-circuit and skip the module compile we rely on. std::error_code ec; fs::remove_all(fixtureRoot / "deplib" / "build", ec); fs::remove_all(fixtureRoot / "deplib" / "bin", ec); fs::remove_all(fixtureRoot / "app" / "build", ec); fs::remove_all(fixtureRoot / "app" / "bin", ec); Configuration dep; dep.path = fixtureRoot / "deplib"; dep.name = "depmod"; dep.outputName = "depmod"; dep.target = HostTarget(); dep.type = ConfigurationType::LibraryStatic; { std::array ifaces = { "DepMod" }; std::array impls = { "DepMod-impl" }; dep.GetInterfacesAndImplementations(ifaces, impls); } if (dep.interfaces.size() != 1) { std::println(std::cerr, "expected 1 dependency interface, got {}", dep.interfaces.size()); return 1; } { std::unordered_map> depResults; std::mutex depMutex; BuildResult r = Build(dep, depResults, depMutex); if (!r.result.empty()) { std::println(std::cerr, "dependency build failed: {}", r.result); return 1; } if (!dep.interfaces[0]->compiled.load()) { std::println(std::cerr, "dependency module not marked compiled after its own build"); return 1; } // Seed a consumer-facing cache so the consumer reuses this build instead // of rebuilding the dependency — mirrors how a shared dep is built once // and consumed by multiple parents through depResults. std::promise promise; promise.set_value(std::move(r)); std::shared_future ready = promise.get_future().share(); std::unordered_map> consumerDeps; consumerDeps.emplace(dep.PcmDir(), ready); Configuration app; app.path = fixtureRoot / "app"; app.name = "resetapp"; app.outputName = "resetapp"; app.target = HostTarget(); app.type = ConfigurationType::Executable; app.dependencies = { &dep }; { std::array appIfaces = {}; std::array appImpls = { "main" }; app.GetInterfacesAndImplementations(appIfaces, appImpls); } BuildResult ar = Build(app, consumerDeps, depMutex); if (!ar.result.empty()) { std::println(std::cerr, "consumer build failed: {}", ar.result); return 1; } // The core regression assertion: building the consumer must not have // reset the (cached, never-rebuilt) dependency's module flag. Before the // fix this was false and an intra-config waiter would have deadlocked. if (!dep.interfaces[0]->compiled.load()) { std::println(std::cerr, "FAIL: consumer build reset a cached dependency's module 'compiled' " "flag (issue #16 regression) — this is the state that deadlocks a " "concurrent build"); return 1; } fs::path bin = app.BinDir() / "resetapp"; if (!fs::exists(bin)) { std::println(std::cerr, "consumer binary not produced at {}", bin.string()); return 1; } auto run = RunCommandWithTimeout(bin.string(), std::chrono::seconds(10)); if (run.exitCode != 0 || run.timedOut || run.crashed) { std::println(std::cerr, "consumer exe did not exit cleanly: exit={} output={}", run.exitCode, run.output); return 1; } if (run.output != "42") { std::println(std::cerr, "expected '42', got '{}'", run.output); return 1; } } return 0; }