feat(resolve): add version-constraint-aware dep resolution with binary fallback
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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.
|
||||
Up-to-date tofu ~main: target for configuration [tofu-test-application] is up to date.
|
||||
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-201829.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:53:05] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:06] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:06] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:06] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:06] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:53:06] "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:37645//index.lua[0m
|
||||
[32m ok index loaded: 3 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:36355//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:52041//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:53177//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:55829//index.lua[0m
|
||||
[32m ok index loaded: 0 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:53801//index.lua[0m
|
||||
[32m ok index loaded: 1 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:44861//index.lua[0m
|
||||
[32m ok index loaded: 2 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:35139//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:41715//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)
|
||||
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
|
||||
@@ -21,4 +21,7 @@ One exception class for the entire types module — no separate subclasses per e
|
||||
### Task 4: Manual char scanning for dep spec parsing
|
||||
Rather than pulling in `std.regex`, the parser uses hand-written `isNameChar()` and `isWhite()` helpers with a simple position cursor. This keeps the module dependency-light (only `std.ascii`). The logic is a direct 1:1 port of the Lua reference patterns: `[A-Za-z0-9_.+-]` for names/versions, whitespace `[ \t]` for separators.
|
||||
|
||||
### Task 12: `delegate` keyword in test lambdas for delegate-typed parameters
|
||||
D non-capturing lambdas infer as `function` pointers, which cannot implicitly convert to `delegate`-typed parameters. The explicit `delegate` keyword (`scope f = delegate (...) { ... };`) forces the correct type. This pattern is needed for resolution seam delegates that production code will instantiate with captures (e.g. closing over `Config cfg`).
|
||||
|
||||
---
|
||||
|
||||
@@ -463,3 +463,56 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
|
||||
- `dub build` passes with `warningsAsErrors`.
|
||||
- `dub test` (with deps.d.skip) — 9 modules passed unittests.
|
||||
- Evidence logged to `.omo/evidence/task-9-tofu-core.log`.
|
||||
|
||||
---
|
||||
|
||||
## Task 12 — `tofu.resolve` (version-constraint-aware dep resolution)
|
||||
|
||||
### Architecture
|
||||
- Module `tofu.resolve` depends on: `tofu.types` (DepConstraint, DepOp, PackageIndex, Pool, BinaryCheckResult), `tofu.deps` (DepTree, DepNode), `tofu.log` (logInfo), `std.string` (indexOf).
|
||||
- `constrainDepTree(tree, index, binaryCheck)` annotates a dep tree with source decisions: binary (pre-built zuur binary satisfied) vs recipe (build from source).
|
||||
- The `binaryCheck` delegate is the testability seam — production wires `tofu.binary.checkBinaryVersion`; tests inject mocks.
|
||||
|
||||
### Algorithm — three-pass resolution
|
||||
1. **Pass 1 (aggregate)**: Walk all nodes, collect all `DepConstraint[]` per dep name into `allConstraints` AA. Same dep constrained by multiple parents → all constraints aggregated.
|
||||
2. **Pass 2 (resolve)**: Walk nodes in topological order. First time a dep name is encountered, resolve it:
|
||||
- Test ALL aggregated constraints via `binaryCheck(depName, constraint)`.
|
||||
- If ANY constraint returns `!satisfies` → dep goes recipe (after verifying recipe exists in index).
|
||||
- If ALL constraints return `satisfies` → dep goes binary.
|
||||
- Root (last node) is always recipe (it's what the user asked to build from ZUUR recipes).
|
||||
- Neither binary nor recipe in index → `ResolveException("dependency '<name>' not found in ZUUR (neither binary nor recipe)")`.
|
||||
3. **Pass 3 (output)**: Build `ConstrainedNode[]` array — one entry per unique node name in tree order, source from resolved map (fallback recipe).
|
||||
|
||||
### Deduplication semantics
|
||||
- Same dep appearing under multiple parents → resolved ONCE. All constraints tested; if ANY fails → recipe. This implements the "strictest wins" rule from the plan.
|
||||
- Unconstrained dep (`DepOp.none`) with binary → binary (satisfies always true). Without binary → recipe (if in index).
|
||||
|
||||
### Log format
|
||||
- `"binary libfoo-2.1 satisfies libfoo>=2.0"` — binary satisfied
|
||||
- `"libfoo: binary 1.9 too old, building from recipe"` — binary exists but too old
|
||||
- `"libfoo: no binary available, building from recipe"` — binary 404/skip
|
||||
|
||||
### `delegate` vs `function` in testability seams
|
||||
- **CRITICAL gotcha**: Non-capturing D lambdas in `@safe unittest` blocks are inferred as `function` pointers with inferred attributes (`pure nothrow @nogc @safe`). A parameter typed as `delegate` cannot accept a `function` even if attributes match.
|
||||
- **Fix**: Use the explicit `delegate` keyword: `scope binaryCheck = delegate (string name, DepConstraint c) @safe { ... };`. This forces delegate type regardless of captures.
|
||||
- **Why delegate, not function?** The production caller (install command) must capture `cfg` to call `checkBinaryVersion(name, constraint, cfg)`. A function pointer cannot carry captured state. The `delegate` keyword in tests matches the production usage pattern.
|
||||
- Contrast with `deps.d` where `resolveDepTree` uses `function` — that works because recipe fetching doesn't need captured state (the production caller uses module-level functions).
|
||||
|
||||
### `std.string.indexOf` import
|
||||
- `indexOf` is NOT in the default namespace. Must explicitly `import std.string : indexOf;` to use on string in assertions. Same pattern as `deps.d`.
|
||||
|
||||
### Test cases (8/8 pass)
|
||||
1. dep libfoo>=2.0, binary 2.1 → binary
|
||||
2. dep libfoo>=2.0, binary 1.9, recipe in index → recipe
|
||||
3. dep libfoo>=2.0, binary 1.9, NOT in index → ResolveException ("neither binary nor recipe")
|
||||
4. unconstrained dep with binary → binary
|
||||
5. unconstrained dep without binary but in index → recipe
|
||||
6. multiple constraints on same dep, one unsatisfied → recipe (dedup + strictest wins)
|
||||
7. root marked recipe (single-node tree)
|
||||
8. empty tree → empty result
|
||||
|
||||
### Build verified
|
||||
- `dub build` passes with `warningsAsErrors`.
|
||||
- `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`.
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
/// tofu.resolve — Version-constraint-aware dependency source resolution.
|
||||
///
|
||||
/// Annotates a dependency tree with source decisions (binary vs recipe)
|
||||
/// by checking zuur/binary package versions against dependency constraints
|
||||
/// via an injectable delegate. Falls back to recipe builds when binaries
|
||||
/// are missing or too old.
|
||||
///
|
||||
/// Design:
|
||||
/// - `constrainDepTree` returns one `ConstrainedNode` per unique package
|
||||
/// name in the tree, including the root (which is always recipe).
|
||||
/// - The `binaryCheck` delegate is the testability seam — production
|
||||
/// wires `tofu.binary.checkBinaryVersion`; tests inject mocks.
|
||||
/// - Deduplication: the same dep name appearing in multiple parent
|
||||
/// constraints is resolved ONCE. If ANY constraint fails binary
|
||||
/// satisfaction, the dep is marked recipe.
|
||||
/// - The `PackageIndex[]` parameter enables recipe-existence checks
|
||||
/// without network I/O — the caller passes the already-fetched index.
|
||||
module tofu.resolve;
|
||||
|
||||
import tofu.types; // DepConstraint, DepOp, PackageIndex, Pool, BinaryCheckResult
|
||||
import tofu.deps; // DepTree, DepNode
|
||||
import tofu.log; // logInfo
|
||||
import std.string : indexOf;
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Exception
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Thrown when a dependency cannot be resolved from either
|
||||
/// binary repository or recipe index.
|
||||
class ResolveException : Exception
|
||||
{
|
||||
@safe this(string msg)
|
||||
{
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Data structures
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Where a resolved dependency comes from.
|
||||
enum DepSource
|
||||
{
|
||||
binary,
|
||||
recipe,
|
||||
}
|
||||
|
||||
/// A dependency tree node annotated with its resolved source.
|
||||
struct ConstrainedNode
|
||||
{
|
||||
string name = "";
|
||||
DepSource source = DepSource.recipe;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Check whether a package name appears in the index with a
|
||||
/// pool that includes recipes (`recipes` or `both`).
|
||||
private @safe bool hasRecipe(string name, scope const PackageIndex[] index)
|
||||
{
|
||||
foreach (entry; index)
|
||||
{
|
||||
if (entry.name == name
|
||||
&& (entry.pool == Pool.recipes || entry.pool == Pool.both))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Format a dependency constraint for human-readable log messages.
|
||||
private @safe string formatConstraint(DepConstraint c)
|
||||
{
|
||||
final switch (c.op)
|
||||
{
|
||||
case DepOp.none:
|
||||
return c.name ~ " (unconstrained)";
|
||||
case DepOp.ge:
|
||||
return c.name ~ ">=" ~ c.ver;
|
||||
case DepOp.le:
|
||||
return c.name ~ "<=" ~ c.ver;
|
||||
case DepOp.eq:
|
||||
return c.name ~ "==" ~ c.ver;
|
||||
case DepOp.ne:
|
||||
return c.name ~ "~=" ~ c.ver;
|
||||
case DepOp.gt:
|
||||
return c.name ~ ">" ~ c.ver;
|
||||
case DepOp.lt:
|
||||
return c.name ~ "<" ~ c.ver;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Public API
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Annotate a dependency tree with source decisions (binary vs recipe).
|
||||
///
|
||||
/// For each unique package name in the tree, all applicable constraints
|
||||
/// (aggregated from every parent node that depends on it) are tested
|
||||
/// against the binary repository via the `binaryCheck` delegate. If ANY
|
||||
/// constraint is unsatisfied, the dep is marked recipe. If ALL constraints
|
||||
/// are satisfied, the dep is marked binary.
|
||||
///
|
||||
/// The root (last node in the topological order) is always recipe — it is
|
||||
/// the target the user asked to build from ZUUR recipes.
|
||||
///
|
||||
/// If a dep cannot be resolved as binary AND has no recipe in the index,
|
||||
/// throws `ResolveException`.
|
||||
///
|
||||
/// Params:
|
||||
/// tree = Topologically ordered dependency tree (from deps.resolveDepTree).
|
||||
/// index = ZUUR package index for recipe existence checks.
|
||||
/// binaryCheck = Delegate that checks binary version against a constraint
|
||||
/// (testability seam — production wires tofu.binary.checkBinaryVersion).
|
||||
///
|
||||
/// Returns:
|
||||
/// `ConstrainedNode[]` with one entry per unique package name in the tree,
|
||||
/// ordered by first appearance in the tree's topological order.
|
||||
@safe
|
||||
ConstrainedNode[] constrainDepTree(DepTree tree, scope const PackageIndex[] index,
|
||||
scope BinaryCheckResult delegate(string, DepConstraint) @safe binaryCheck)
|
||||
{
|
||||
if (tree.nodes.length == 0)
|
||||
return [];
|
||||
|
||||
// Root is the target package — always recipe.
|
||||
string rootName = tree.nodes[$ - 1].name;
|
||||
|
||||
// ── Pass 1: Aggregate all constraints per dep name ──
|
||||
// For each dep name, collect every DepConstraint from every
|
||||
// parent node that lists it as a dependency.
|
||||
DepConstraint[][string] allConstraints;
|
||||
|
||||
foreach (node; tree.nodes)
|
||||
{
|
||||
foreach (constraint; node.constraints)
|
||||
{
|
||||
allConstraints[constraint.name] ~= constraint;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 2: Resolve source for each unique dep name ──
|
||||
DepSource[string] resolved;
|
||||
|
||||
// Root is always recipe.
|
||||
resolved[rootName] = DepSource.recipe;
|
||||
|
||||
// Walk the tree in order. The first time we encounter a dep name
|
||||
// (via some parent's constraint), resolve it using ALL accumulated
|
||||
// constraints from ALL parents.
|
||||
foreach (node; tree.nodes)
|
||||
{
|
||||
foreach (constraint; node.constraints)
|
||||
{
|
||||
string depName = constraint.name;
|
||||
|
||||
// Already resolved — skip.
|
||||
if (depName in resolved)
|
||||
continue;
|
||||
|
||||
// Retrieve the full constraint set for this dep.
|
||||
auto constraints = depName in allConstraints;
|
||||
assert(constraints !is null, "dep in tree must have constraints");
|
||||
|
||||
bool allSatisfied = true;
|
||||
foreach (c; *constraints)
|
||||
{
|
||||
auto result = binaryCheck(depName, c);
|
||||
|
||||
if (!result.satisfies)
|
||||
{
|
||||
if (result.exists)
|
||||
logInfo("%s: binary %s too old, building from recipe",
|
||||
depName, result.ver);
|
||||
else
|
||||
logInfo("%s: no binary available, building from recipe",
|
||||
depName);
|
||||
allSatisfied = false;
|
||||
break;
|
||||
}
|
||||
|
||||
logInfo("binary %s-%s satisfies %s",
|
||||
depName, result.ver, formatConstraint(c));
|
||||
}
|
||||
|
||||
if (allSatisfied)
|
||||
{
|
||||
resolved[depName] = DepSource.binary;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recipe fallback — must exist in the index.
|
||||
if (!hasRecipe(depName, index))
|
||||
{
|
||||
throw new ResolveException(
|
||||
"dependency '" ~ depName
|
||||
~ "' not found in ZUUR (neither binary nor recipe)");
|
||||
}
|
||||
resolved[depName] = DepSource.recipe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 3: Build output array in tree order ──
|
||||
// Include every distinct node (including root).
|
||||
ConstrainedNode[] result;
|
||||
bool[string] seen;
|
||||
|
||||
foreach (node; tree.nodes)
|
||||
{
|
||||
if (node.name in seen)
|
||||
continue;
|
||||
seen[node.name] = true;
|
||||
|
||||
ConstrainedNode cn;
|
||||
cn.name = node.name;
|
||||
// Use resolved map; fall back to recipe for safety.
|
||||
cn.source = resolved.get(node.name, DepSource.recipe);
|
||||
result ~= cn;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Unittests
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
// Helper: build a DepConstraint with explicit fields.
|
||||
private @safe DepConstraint mkConstraint(string name,
|
||||
DepOp op = DepOp.none,
|
||||
string ver = "")
|
||||
{
|
||||
DepConstraint c;
|
||||
c.name = name;
|
||||
c.op = op;
|
||||
c.ver = ver;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Helper: build a simple two-node DepTree (dep → root).
|
||||
private @safe DepTree mkTree2(string depName, DepConstraint depConstraint,
|
||||
string rootName)
|
||||
{
|
||||
DepNode depNode;
|
||||
depNode.name = depName;
|
||||
|
||||
DepNode rootNode;
|
||||
rootNode.name = rootName;
|
||||
rootNode.constraints ~= depConstraint;
|
||||
|
||||
DepTree t;
|
||||
t.nodes = [depNode, rootNode]; // dep before root (topo order)
|
||||
return t;
|
||||
}
|
||||
|
||||
// Helper: build an index entry with a given name and pool.
|
||||
private @safe PackageIndex mkIndexEntry(string name, Pool pool)
|
||||
{
|
||||
PackageIndex e;
|
||||
e.name = name;
|
||||
e.pool = pool;
|
||||
return e;
|
||||
}
|
||||
|
||||
// ── Test (1): dep libfoo>=2.0, binary 2.1 → binary ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tree = mkTree2("libfoo",
|
||||
mkConstraint("libfoo", DepOp.ge, "2.0"),
|
||||
"mypkg");
|
||||
|
||||
PackageIndex[] index = [mkIndexEntry("libfoo", Pool.recipes)];
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
assert(name == "libfoo");
|
||||
BinaryCheckResult r;
|
||||
r.exists = true;
|
||||
r.ver = "2.1";
|
||||
r.satisfies = true;
|
||||
return r;
|
||||
};
|
||||
|
||||
auto result = constrainDepTree(tree, index, binaryCheck);
|
||||
|
||||
assert(result.length == 2);
|
||||
// libfoo first (dep), mypkg second (root)
|
||||
assert(result[0].name == "libfoo");
|
||||
assert(result[0].source == DepSource.binary,
|
||||
"libfoo-2.1 satisfies >=2.0 → should be binary");
|
||||
assert(result[1].name == "mypkg");
|
||||
assert(result[1].source == DepSource.recipe,
|
||||
"root is always recipe");
|
||||
}
|
||||
|
||||
// ── Test (2): dep libfoo>=2.0, binary 1.9, recipe in index → recipe ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tree = mkTree2("libfoo",
|
||||
mkConstraint("libfoo", DepOp.ge, "2.0"),
|
||||
"mypkg");
|
||||
|
||||
PackageIndex[] index = [mkIndexEntry("libfoo", Pool.recipes)];
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
BinaryCheckResult r;
|
||||
r.exists = true;
|
||||
r.ver = "1.9";
|
||||
r.satisfies = false; // 1.9 < 2.0
|
||||
return r;
|
||||
};
|
||||
|
||||
auto result = constrainDepTree(tree, index, binaryCheck);
|
||||
|
||||
assert(result.length == 2);
|
||||
assert(result[0].name == "libfoo");
|
||||
assert(result[0].source == DepSource.recipe,
|
||||
"binary too old, recipe in index → recipe");
|
||||
assert(result[1].source == DepSource.recipe);
|
||||
}
|
||||
|
||||
// ── Test (3): dep libfoo>=2.0, binary 1.9, NOT in index → ResolveException ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tree = mkTree2("libfoo",
|
||||
mkConstraint("libfoo", DepOp.ge, "2.0"),
|
||||
"mypkg");
|
||||
|
||||
PackageIndex[] index; // empty — no recipe available
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
BinaryCheckResult r;
|
||||
r.exists = true;
|
||||
r.ver = "1.9";
|
||||
r.satisfies = false;
|
||||
return r;
|
||||
};
|
||||
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
constrainDepTree(tree, index, binaryCheck);
|
||||
assert(false, "expected ResolveException");
|
||||
}
|
||||
catch (ResolveException e)
|
||||
{
|
||||
caught = true;
|
||||
assert(e.msg.indexOf("neither binary nor recipe") >= 0,
|
||||
"message should mention 'neither binary nor recipe', got: " ~ e.msg);
|
||||
assert(e.msg.indexOf("libfoo") >= 0,
|
||||
"message should name the missing dep, got: " ~ e.msg);
|
||||
}
|
||||
assert(caught, "should have thrown ResolveException");
|
||||
}
|
||||
|
||||
// ── Test (4): unconstrained dep with binary → binary ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tree = mkTree2("libbar",
|
||||
mkConstraint("libbar", DepOp.none, ""),
|
||||
"mypkg");
|
||||
|
||||
PackageIndex[] index = [mkIndexEntry("libbar", Pool.recipes)];
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
BinaryCheckResult r;
|
||||
r.exists = true;
|
||||
r.ver = "3.0";
|
||||
r.satisfies = true; // unconstrained always satisfies
|
||||
return r;
|
||||
};
|
||||
|
||||
auto result = constrainDepTree(tree, index, binaryCheck);
|
||||
|
||||
assert(result.length == 2);
|
||||
assert(result[0].name == "libbar");
|
||||
assert(result[0].source == DepSource.binary,
|
||||
"unconstrained dep with binary → binary");
|
||||
}
|
||||
|
||||
// ── Test (5): unconstrained dep without binary but in index → recipe ──
|
||||
@safe unittest
|
||||
{
|
||||
auto tree = mkTree2("libbaz",
|
||||
mkConstraint("libbaz", DepOp.none, ""),
|
||||
"mypkg");
|
||||
|
||||
PackageIndex[] index = [mkIndexEntry("libbaz", Pool.both)];
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
BinaryCheckResult r;
|
||||
r.exists = false; // 404
|
||||
r.ver = "";
|
||||
r.satisfies = false;
|
||||
return r;
|
||||
};
|
||||
|
||||
auto result = constrainDepTree(tree, index, binaryCheck);
|
||||
|
||||
assert(result.length == 2);
|
||||
assert(result[0].name == "libbaz");
|
||||
assert(result[0].source == DepSource.recipe,
|
||||
"unconstrained dep no binary, in index → recipe");
|
||||
}
|
||||
|
||||
// ── Test (6): multiple constraints on same dep, one unsatisfied → recipe ──
|
||||
@safe unittest
|
||||
{
|
||||
// parentA deps=[libfoo>=1.0]
|
||||
// parentB deps=[libfoo>=2.0]
|
||||
// root deps=[parentA, parentB]
|
||||
// Binary version: 1.5
|
||||
// >=1.0 → satisfies, >=2.0 → !satisfies → libfoo goes recipe.
|
||||
|
||||
DepNode libfooNode;
|
||||
libfooNode.name = "libfoo";
|
||||
|
||||
DepNode parentANode;
|
||||
parentANode.name = "parentA";
|
||||
parentANode.constraints ~= mkConstraint("libfoo", DepOp.ge, "1.0");
|
||||
|
||||
DepNode parentBNode;
|
||||
parentBNode.name = "parentB";
|
||||
parentBNode.constraints ~= mkConstraint("libfoo", DepOp.ge, "2.0");
|
||||
|
||||
DepNode rootNode;
|
||||
rootNode.name = "mypkg";
|
||||
rootNode.constraints ~= mkConstraint("parentA", DepOp.none, "");
|
||||
rootNode.constraints ~= mkConstraint("parentB", DepOp.none, "");
|
||||
|
||||
DepTree tree;
|
||||
tree.nodes = [libfooNode, parentANode, parentBNode, rootNode];
|
||||
|
||||
PackageIndex[] index = [mkIndexEntry("libfoo", Pool.recipes)];
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
BinaryCheckResult r;
|
||||
r.exists = true;
|
||||
r.ver = "1.5";
|
||||
if (name == "libfoo" && c.op == DepOp.ge && c.ver == "2.0")
|
||||
r.satisfies = false;
|
||||
else
|
||||
r.satisfies = true;
|
||||
return r;
|
||||
};
|
||||
|
||||
auto result = constrainDepTree(tree, index, binaryCheck);
|
||||
|
||||
// libfoo must be recipe because >=2.0 fails.
|
||||
auto libfooEntry = result[0];
|
||||
assert(libfooEntry.name == "libfoo");
|
||||
assert(libfooEntry.source == DepSource.recipe,
|
||||
"one constraint unsatisfied → recipe");
|
||||
|
||||
// parentA and parentB: unconstrained, binary exists → binary.
|
||||
assert(result[1].name == "parentA");
|
||||
assert(result[1].source == DepSource.binary);
|
||||
assert(result[2].name == "parentB");
|
||||
assert(result[2].source == DepSource.binary);
|
||||
|
||||
// root → recipe.
|
||||
assert(result[3].name == "mypkg");
|
||||
assert(result[3].source == DepSource.recipe);
|
||||
}
|
||||
|
||||
// ── Test (7): root marked recipe ──
|
||||
@safe unittest
|
||||
{
|
||||
// Single-node tree: root with no deps.
|
||||
DepNode rootNode;
|
||||
rootNode.name = "solopkg";
|
||||
|
||||
DepTree tree;
|
||||
tree.nodes = [rootNode];
|
||||
|
||||
PackageIndex[] index;
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
assert(false, "binaryCheck should not be called for root with no deps");
|
||||
return BinaryCheckResult();
|
||||
};
|
||||
|
||||
auto result = constrainDepTree(tree, index, binaryCheck);
|
||||
|
||||
assert(result.length == 1);
|
||||
assert(result[0].name == "solopkg");
|
||||
assert(result[0].source == DepSource.recipe,
|
||||
"root is always recipe");
|
||||
}
|
||||
|
||||
// ── Test (8): empty tree → empty result ──
|
||||
@safe unittest
|
||||
{
|
||||
DepTree tree; // nodes.length == 0
|
||||
|
||||
PackageIndex[] index;
|
||||
|
||||
scope binaryCheck = delegate (string name, DepConstraint c) @safe {
|
||||
assert(false, "binaryCheck should never be called on empty tree");
|
||||
return BinaryCheckResult();
|
||||
};
|
||||
|
||||
auto result = constrainDepTree(tree, index, binaryCheck);
|
||||
|
||||
assert(result.length == 0, "empty tree → empty result");
|
||||
}
|
||||
Reference in New Issue
Block a user