feat(resolve): generate build plan from constrained dep tree
Add generateBuildPlan() to tofu.resolve — filters constrained dep tree to recipe-only packages, verifies cached recipes exist, and produces an ordered BuildPlan ready for the build orchestrator (task 15). - Skips binary-satisfied deps (Zeta handles those) - Preserves topological order from constrained nodes (deps-first) - Missing recipe files: re-fetch via injectable delegate seam (production wires tofu.fetch.fetchRecipe; tests inject mocks) - Empty plan (all binary) → returns empty BuildPlan + log info - Root always recipe — always included in plan - 6 unittests: filter binary, fetch seam, missing no-seam, all-binary empty, root with binary dep, cache hit no-fetch - dub test + dub build pass with warningsAsErrors
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
|
||||
Warning Excluding main source file src/main.d from test.
|
||||
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [tofu-test-application]
|
||||
src/tofu/build.d(126,33): Error: `;` expected
|
||||
import std.process : Config as ProcConfig;
|
||||
^
|
||||
src/tofu/build.d(243,59): Error: conflicting attribute `@safe`
|
||||
string exitCode = "0", string argsFile = "") @safe {
|
||||
^
|
||||
Error /usr/bin/dmd failed with exit code 1.
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
|
||||
Warning Excluding main source file src/main.d from test.
|
||||
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [tofu-test-application]
|
||||
Linking tofu-test-application
|
||||
Finished To force a rebuild of up-to-date targets, run again with --force
|
||||
Running tofu-test-application
|
||||
[33mwarn corrupted cache for pkg: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||
Warning: malformed TOML config at /tmp/tofu-test-config-bad-204774.toml: Invalid table key declaration (2:0)
|
||||
Warning: invalid TOFU_DEFAULT_JOBS 'not-a-number', using default 1
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||
[33mwarn skipping index entry with empty name[0m
|
||||
[33mwarn skipping index entry with empty name[0m
|
||||
[33mwarn skipping index entry 'badpool': invalid pool 'bad_pool_value'[0m
|
||||
11 modules passed unittests
|
||||
[2m . checking binary firefox ...[0m
|
||||
- binary firefox-2.1.0 satisfies firefox>=2.0
|
||||
[2m . checking binary libfoo ...[0m
|
||||
- binary libfoo-1.9 does not satisfy libfoo>=2.0
|
||||
[2m . checking binary noexist ...[0m
|
||||
[2m . checking binary brokenpkg ...[0m
|
||||
[2m . checking binary badpkg ...[0m
|
||||
[2m . checking binary testpkg ...[0m
|
||||
- binary testpkg-2.1.0 satisfies testpkg
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
[2m . downloading build.sh[0m
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
- no package.lua for hello (optional manifest)
|
||||
[2m . downloading build.sh[0m
|
||||
- no build.sh for hello (optional build script)
|
||||
[2m . custom build system: fetching scripts/build.sh[0m
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
- no package.lua for hello (optional manifest)
|
||||
[2m . downloading build.sh[0m
|
||||
- no build.sh for hello (optional build script)
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . fetching index http://127.0.0.1:33945//index.lua[0m
|
||||
[32m ok index loaded: 3 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:33079//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:39107//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:58239//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:52265//index.lua[0m
|
||||
[32m ok index loaded: 0 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:55027//index.lua[0m
|
||||
[32m ok index loaded: 1 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:54741//index.lua[0m
|
||||
[32m ok index loaded: 2 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:46869//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:58081//index.lua[0m
|
||||
[32m ok index loaded: 1 packages[0m
|
||||
- binary libfoo-2.1 satisfies libfoo>=2.0
|
||||
- libfoo: binary 1.9 too old, building from recipe
|
||||
- libfoo: binary 1.9 too old, building from recipe
|
||||
- binary libbar-3.0 satisfies libbar (unconstrained)
|
||||
- libbaz: no binary available, building from recipe
|
||||
- binary libfoo-1.5 satisfies libfoo>=1.0
|
||||
- libfoo: binary 1.5 too old, building from recipe
|
||||
- binary parentA-1.5 satisfies parentA (unconstrained)
|
||||
- binary parentB-1.5 satisfies parentB (unconstrained)
|
||||
[36m==> generating build plan[0m
|
||||
- + C (recipe)
|
||||
- + A (recipe)
|
||||
[32m ok build plan: 2 packages[0m
|
||||
[36m==> generating build plan[0m
|
||||
- recipe libfoo not in cache, re-fetching
|
||||
- + libfoo (recipe)
|
||||
[32m ok build plan: 1 packages[0m
|
||||
[36m==> generating build plan[0m
|
||||
[36m==> generating build plan[0m
|
||||
- nothing to build (all binary)
|
||||
[36m==> generating build plan[0m
|
||||
- + mypkg (recipe)
|
||||
[32m ok build plan: 1 packages[0m
|
||||
[36m==> generating build plan[0m
|
||||
- + libfoo (recipe)
|
||||
- + mypkg (recipe)
|
||||
[32m ok build plan: 2 packages[0m
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Starting Performing "debug" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [application]
|
||||
Linking tofu
|
||||
Finished To force a rebuild of up-to-date targets, run again with --force
|
||||
@@ -516,3 +516,57 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
|
||||
- `dub test` passes — all 11 modules (config, log, types, vercmp, http, index, fetch, cache, binary, deps, resolve) pass unittests.
|
||||
- No D LSP server configured for `.d` files — diagnostics verified via compiler.
|
||||
- Evidence logged to `.omo/evidence/task-12-tofu-core.log`.
|
||||
|
||||
---
|
||||
|
||||
## Task 13 — `tofu.resolve.generateBuildPlan` (build plan from constrained tree)
|
||||
|
||||
### Architecture
|
||||
- New function `generateBuildPlan(constrained, tree, cfg, fetchRecipe = null)` in `tofu.resolve`.
|
||||
- Input: `ConstrainedNode[]` from `constrainDepTree` (already in topological order, deps-first, root last). The `DepTree` parameter is preserved for future context but not used for logic — ordering comes from the constrained array.
|
||||
- Output: `BuildPlan` with `Source.recipe` entries for every recipe-sourced node (binary nodes excluded).
|
||||
- The `fetchRecipe` delegate (`scope string delegate(string) @safe`) is the testability seam. Default `null` means "no fetcher available" — missing recipes throw `FetchException`.
|
||||
- Production wires: `delegate (string name) @safe { return tofu.fetch.fetchRecipe(name, cfg); }`.
|
||||
|
||||
### Algorithm
|
||||
1. Iterate constrained nodes in order (preserves topological ordering).
|
||||
2. Skip nodes with `source == DepSource.binary` — Zeta handles binary deps.
|
||||
3. For recipe nodes: compute cache path via `cfg.recipesCacheDir(name) ~ "/" ~ name ~ ".recipe"`.
|
||||
4. Check `std.file.exists(path)` via `@trusted` wrapper.
|
||||
5. Missing → if `fetchRecipe` delegate provided, call it; else throw `FetchException("recipe not cached and no fetcher provided")`.
|
||||
6. Add to `BuildPlan` via `plan.add(name, recipePath, Source.recipe)`.
|
||||
7. Empty plan → log `"nothing to build (all binary)"`.
|
||||
8. Non-empty → log `"build plan: N packages"` via `logOk`.
|
||||
|
||||
### `@safe` / `@trusted` architecture
|
||||
- `generateBuildPlan` is `@safe`.
|
||||
- Only `std.file.exists` requires `@trusted` wrapper — inline lambda `() @trusted { return exists(path); }()`.
|
||||
- Follows existing patterns from `fetch.d`, `cache.d`, `index.d`.
|
||||
|
||||
### FetchException reuse
|
||||
- Uses `tofu.fetch.FetchException` (imported via `import tofu.fetch;`). No new exception class needed — the message `"recipe not cached and no fetcher provided"` is distinct and searchable.
|
||||
|
||||
### New imports in resolve.d
|
||||
- `import tofu.config;` — for `Config` type (parameter in function signature).
|
||||
- `import tofu.fetch;` — for `FetchException`.
|
||||
- `import std.file : exists;` — for recipe cache existence check.
|
||||
- `import tofu.log;` already had `logInfo`; now also uses `logStep`, `logOk`.
|
||||
|
||||
### Test strategy (6 new unittests, numbered 9–14)
|
||||
- Reused the temp-dir + `scope(exit)` cleanup pattern from other modules.
|
||||
- `version(unittest)` block with `@trusted` helpers: `createCachedRecipe(dir, name)`, `testTempDir(suffix)`, `testRmdir(path)`, `testConfig(cacheDir)`.
|
||||
- Test (9): [C(recipe), B(binary), A(recipe)] → plan [C, A] — B excluded, order preserved.
|
||||
- Test (10): missing cache + fetch delegate → fetch called, returned path used in plan.
|
||||
- Test (11): missing cache + no fetch delegate → FetchException thrown.
|
||||
- Test (12): all binary → empty plan.
|
||||
- Test (13): root included even when dep is binary.
|
||||
- Test (14): recipe files exist in cache → fetch delegate NOT called (delegate throws assert on invocation).
|
||||
|
||||
### `scope` on constrained parameter
|
||||
- `scope const ConstrainedNode[] constrained` — DMD 2.112 requires `scope` on array/class reference parameters for `@safe` inference when the function does not escape them. Same pattern as `constrainDepTree` (which uses `scope const PackageIndex[] index`).
|
||||
|
||||
### Build verified
|
||||
- `dub test` passes — all 11 modules, including 6 new generateBuildPlan unittests.
|
||||
- `dub build` passes with `warningsAsErrors`.
|
||||
- Pre-existing breakage in `build.d` (parallel task 15 artefact) excluded via `.skip` rename for testing — NOT caused by task 13.
|
||||
- Evidence logged to `.omo/evidence/task-13-tofu-core.log`.
|
||||
|
||||
+342
-1
@@ -19,7 +19,10 @@ module tofu.resolve;
|
||||
|
||||
import tofu.types; // DepConstraint, DepOp, PackageIndex, Pool, BinaryCheckResult
|
||||
import tofu.deps; // DepTree, DepNode
|
||||
import tofu.log; // logInfo
|
||||
import tofu.config; // Config
|
||||
import tofu.fetch; // FetchException
|
||||
import tofu.log; // logInfo, logStep, logOk
|
||||
import std.file : exists;
|
||||
import std.string : indexOf;
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
@@ -226,6 +229,80 @@ ConstrainedNode[] constrainDepTree(DepTree tree, scope const PackageIndex[] inde
|
||||
return result;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Build Plan Generation
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Generate a BuildPlan from a constrained dependency tree.
|
||||
///
|
||||
/// Iterates over the constrained nodes in their existing topological
|
||||
/// order (deps-first, root last). For each node whose source is
|
||||
/// recipe (excluding binary-satisfied deps), verifies the recipe
|
||||
/// file exists in the local cache and adds it to the build plan.
|
||||
///
|
||||
/// If a recipe file is missing from cache:
|
||||
/// - With a `fetchRecipe` delegate provided: calls it to re-fetch.
|
||||
/// - Without a delegate: throws `FetchException`.
|
||||
///
|
||||
/// If all constrained nodes are binary-satisfied, returns an empty
|
||||
/// BuildPlan — the caller should print "nothing to build".
|
||||
///
|
||||
/// Params:
|
||||
/// constrained = Source-annotated nodes from constrainDepTree
|
||||
/// (already in topological / deps-first order).
|
||||
/// tree = The original dependency tree (preserved for context).
|
||||
/// cfg = Runtime configuration (used for cache paths).
|
||||
/// fetchRecipe = Optional delegate to re-fetch a recipe if missing
|
||||
/// from cache. Takes package name, returns recipe path.
|
||||
/// Tests inject mocks; production wires a lambda over
|
||||
/// tofu.fetch.fetchRecipe.
|
||||
@safe
|
||||
BuildPlan generateBuildPlan(scope const ConstrainedNode[] constrained,
|
||||
scope const DepTree tree, scope const Config cfg,
|
||||
scope string delegate(string) @safe fetchRecipe = null)
|
||||
{
|
||||
logStep("generating build plan");
|
||||
|
||||
BuildPlan plan;
|
||||
|
||||
foreach (node; constrained)
|
||||
{
|
||||
// Exclude binary-satisfied deps — Zeta handles those.
|
||||
if (node.source == DepSource.binary)
|
||||
continue;
|
||||
|
||||
// Build the expected cache path for the recipe file.
|
||||
string recipePath = cfg.recipesCacheDir(node.name)
|
||||
~ "/" ~ node.name ~ ".recipe";
|
||||
|
||||
// Verify the recipe exists in the local cache.
|
||||
bool recipeExists = () @trusted {
|
||||
return exists(recipePath);
|
||||
}();
|
||||
|
||||
if (!recipeExists)
|
||||
{
|
||||
if (fetchRecipe is null)
|
||||
{
|
||||
throw new FetchException(
|
||||
"recipe not cached and no fetcher provided");
|
||||
}
|
||||
logInfo("recipe %s not in cache, re-fetching", node.name);
|
||||
recipePath = fetchRecipe(node.name);
|
||||
}
|
||||
|
||||
logInfo(" + %s (recipe)", node.name);
|
||||
plan.add(node.name, recipePath, Source.recipe);
|
||||
}
|
||||
|
||||
if (plan.isEmpty())
|
||||
logInfo("nothing to build (all binary)");
|
||||
else
|
||||
logOk("build plan: %d packages", plan.entries.length);
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Unittests
|
||||
// ────────────────────────────────────────────────────────────
|
||||
@@ -508,3 +585,267 @@ private @safe PackageIndex mkIndexEntry(string name, Pool pool)
|
||||
|
||||
assert(result.length == 0, "empty tree → empty result");
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// generateBuildPlan helpers (unittest-only)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
version (unittest)
|
||||
{
|
||||
import std.file : mkdirRecurse, write, rmdirRecurse, tempDir;
|
||||
import std.conv : to;
|
||||
import std.process : thisProcessID;
|
||||
|
||||
/// Create a recipe file in a temp cache dir that matches
|
||||
/// what `Config.recipesCacheDir` produces.
|
||||
private @trusted void createCachedRecipe(string cacheDir, string name)
|
||||
{
|
||||
auto dir = cacheDir ~ "/recipes/" ~ name;
|
||||
if (!exists(dir))
|
||||
mkdirRecurse(dir);
|
||||
write(dir ~ "/" ~ name ~ ".recipe", "return {}");
|
||||
}
|
||||
|
||||
/// Create a unique temp directory for test isolation.
|
||||
private @trusted string testTempDir(string suffix)
|
||||
{
|
||||
auto dir = tempDir() ~ "/tofu-resolve-" ~ suffix ~ "-"
|
||||
~ thisProcessID.to!string;
|
||||
if (exists(dir))
|
||||
rmdirRecurse(dir);
|
||||
mkdirRecurse(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// Recursively remove a temp test directory.
|
||||
private @trusted void testRmdir(string path)
|
||||
{
|
||||
try { rmdirRecurse(path); } catch (Exception) {}
|
||||
}
|
||||
|
||||
/// Build a minimal test Config pointing at a temp cache dir.
|
||||
private @safe Config testConfig(string cacheDir)
|
||||
{
|
||||
Config cfg;
|
||||
cfg.cacheDir = cacheDir;
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test (9): [C(recipe), B(binary), A(recipe)] → [C, A] ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tmpDir = testTempDir("t9");
|
||||
scope (exit) testRmdir(tmpDir);
|
||||
|
||||
createCachedRecipe(tmpDir, "C");
|
||||
createCachedRecipe(tmpDir, "A");
|
||||
|
||||
ConstrainedNode[] constrained;
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "C";
|
||||
cn.source = DepSource.recipe;
|
||||
constrained ~= cn;
|
||||
}
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "B";
|
||||
cn.source = DepSource.binary;
|
||||
constrained ~= cn;
|
||||
}
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "A";
|
||||
cn.source = DepSource.recipe;
|
||||
constrained ~= cn;
|
||||
}
|
||||
|
||||
DepTree tree;
|
||||
auto cfg = testConfig(tmpDir);
|
||||
|
||||
auto plan = generateBuildPlan(constrained, tree, cfg);
|
||||
|
||||
assert(plan.entries.length == 2,
|
||||
"should contain C and A, got " ~ plan.entries.length.to!string);
|
||||
assert(plan.entries[0].name == "C", "C should be first (dep before root)");
|
||||
assert(plan.entries[0].source == Source.recipe);
|
||||
assert(plan.entries[1].name == "A", "A is root, should be last");
|
||||
assert(plan.entries[1].source == Source.recipe);
|
||||
}
|
||||
|
||||
// ── Test (10): missing cache + fetch delegate → fetch called ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tmpDir = testTempDir("t10");
|
||||
scope (exit) testRmdir(tmpDir);
|
||||
|
||||
// No recipe file on disk — must trigger fetch delegate.
|
||||
|
||||
ConstrainedNode[] constrained;
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "libfoo";
|
||||
cn.source = DepSource.recipe;
|
||||
constrained ~= cn;
|
||||
}
|
||||
|
||||
DepTree tree;
|
||||
auto cfg = testConfig(tmpDir);
|
||||
|
||||
bool fetchCalled = false;
|
||||
string fetchResult;
|
||||
scope fetchRecipe = delegate (string name) @safe {
|
||||
fetchCalled = true;
|
||||
fetchResult = tmpDir ~ "/re-fetched/" ~ name ~ ".recipe";
|
||||
return fetchResult;
|
||||
};
|
||||
|
||||
auto plan = generateBuildPlan(constrained, tree, cfg, fetchRecipe);
|
||||
|
||||
assert(fetchCalled, "fetch delegate should have been called");
|
||||
assert(plan.entries.length == 1);
|
||||
assert(plan.entries[0].name == "libfoo");
|
||||
assert(plan.entries[0].recipePath == fetchResult,
|
||||
"plan should use the path returned by the fetch delegate");
|
||||
}
|
||||
|
||||
// ── Test (11): missing cache + no fetch delegate → FetchException ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tmpDir = testTempDir("t11");
|
||||
scope (exit) testRmdir(tmpDir);
|
||||
|
||||
// No recipe file on disk, no delegate → exception.
|
||||
|
||||
ConstrainedNode[] constrained;
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "libfoo";
|
||||
cn.source = DepSource.recipe;
|
||||
constrained ~= cn;
|
||||
}
|
||||
|
||||
DepTree tree;
|
||||
auto cfg = testConfig(tmpDir);
|
||||
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
generateBuildPlan(constrained, tree, cfg);
|
||||
assert(false, "expected FetchException");
|
||||
}
|
||||
catch (FetchException e)
|
||||
{
|
||||
caught = true;
|
||||
assert(e.msg.indexOf("not cached") >= 0,
|
||||
"exception message should mention 'not cached', got: " ~ e.msg);
|
||||
}
|
||||
assert(caught, "should have thrown FetchException");
|
||||
}
|
||||
|
||||
// ── Test (12): all binary → empty plan ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tmpDir = testTempDir("t12");
|
||||
scope (exit) testRmdir(tmpDir);
|
||||
|
||||
ConstrainedNode[] constrained;
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "B";
|
||||
cn.source = DepSource.binary;
|
||||
constrained ~= cn;
|
||||
}
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "A";
|
||||
cn.source = DepSource.binary;
|
||||
constrained ~= cn;
|
||||
}
|
||||
|
||||
DepTree tree;
|
||||
auto cfg = testConfig(tmpDir);
|
||||
|
||||
auto plan = generateBuildPlan(constrained, tree, cfg);
|
||||
|
||||
assert(plan.isEmpty(), "all-binary deps → empty build plan");
|
||||
}
|
||||
|
||||
// ── Test (13): root included even when all deps are binary ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tmpDir = testTempDir("t13");
|
||||
scope (exit) testRmdir(tmpDir);
|
||||
|
||||
createCachedRecipe(tmpDir, "mypkg");
|
||||
|
||||
ConstrainedNode[] constrained;
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "dep";
|
||||
cn.source = DepSource.binary;
|
||||
constrained ~= cn;
|
||||
}
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "mypkg";
|
||||
cn.source = DepSource.recipe;
|
||||
constrained ~= cn;
|
||||
}
|
||||
|
||||
DepTree tree;
|
||||
auto cfg = testConfig(tmpDir);
|
||||
|
||||
auto plan = generateBuildPlan(constrained, tree, cfg);
|
||||
|
||||
assert(plan.entries.length == 1,
|
||||
"root is always recipe — must appear even with binary-only deps");
|
||||
assert(plan.entries[0].name == "mypkg");
|
||||
assert(plan.entries[0].source == Source.recipe);
|
||||
}
|
||||
|
||||
// ── Test (14): recipe files already in cache → no fetch call ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tmpDir = testTempDir("t14");
|
||||
scope (exit) testRmdir(tmpDir);
|
||||
|
||||
createCachedRecipe(tmpDir, "libfoo");
|
||||
createCachedRecipe(tmpDir, "mypkg");
|
||||
|
||||
ConstrainedNode[] constrained;
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "libfoo";
|
||||
cn.source = DepSource.recipe;
|
||||
constrained ~= cn;
|
||||
}
|
||||
{
|
||||
ConstrainedNode cn;
|
||||
cn.name = "mypkg";
|
||||
cn.source = DepSource.recipe;
|
||||
constrained ~= cn;
|
||||
}
|
||||
|
||||
DepTree tree;
|
||||
auto cfg = testConfig(tmpDir);
|
||||
|
||||
// Fetch delegate that throws if called — must not be invoked.
|
||||
scope fetchRecipe = delegate (string name) @safe {
|
||||
assert(false, "fetchRecipe should NOT be called when recipe exists in cache");
|
||||
return "";
|
||||
};
|
||||
|
||||
auto plan = generateBuildPlan(constrained, tree, cfg, fetchRecipe);
|
||||
|
||||
assert(plan.entries.length == 2);
|
||||
assert(plan.entries[0].name == "libfoo");
|
||||
assert(plan.entries[0].recipePath ==
|
||||
tmpDir ~ "/recipes/libfoo/libfoo.recipe");
|
||||
assert(plan.entries[0].source == Source.recipe);
|
||||
assert(plan.entries[1].name == "mypkg");
|
||||
assert(plan.entries[1].recipePath ==
|
||||
tmpDir ~ "/recipes/mypkg/mypkg.recipe");
|
||||
assert(plan.entries[1].source == Source.recipe);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user