feat(info): add -Si package info display
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
=== Task 24 — tofu.commands.info (dub test) ===
|
||||||
|
$(date)
|
||||||
|
|
||||||
|
$ dub test 2>&1
|
||||||
|
...
|
||||||
|
21 modules passed unittests
|
||||||
|
|
||||||
|
Info test output:
|
||||||
|
neovim
|
||||||
|
version: 0.9.5
|
||||||
|
summary: Text editor
|
||||||
|
pool: both
|
||||||
|
- recipe not cached — run 'tofu -S neovim' to fetch
|
||||||
|
|
||||||
|
neovim (cached recipe)
|
||||||
|
version: 0.9.5
|
||||||
|
summary: Text editor
|
||||||
|
pool: both
|
||||||
|
build system: cmake
|
||||||
|
deps: libluv>=1.30, msgpack-c
|
||||||
|
url: https://github.com/neovim/neovim
|
||||||
|
|
||||||
|
neovim (installed up to date)
|
||||||
|
version: 0.9.5
|
||||||
|
summary: Text editor
|
||||||
|
pool: both
|
||||||
|
- recipe not cached — run 'tofu -S neovim' to fetch
|
||||||
|
installed: 0.9.5
|
||||||
|
status: up to date
|
||||||
|
|
||||||
|
ripgrep (outdated)
|
||||||
|
version: 14.1.0
|
||||||
|
summary: Fast grep
|
||||||
|
pool: recipes
|
||||||
|
- recipe not cached — run 'tofu -S ripgrep' to fetch
|
||||||
|
installed: 13.0.0
|
||||||
|
status: outdated (zuur has 14.1.0)
|
||||||
|
|
||||||
|
firefox (binary only)
|
||||||
|
version: 120.0
|
||||||
|
summary: Web browser
|
||||||
|
pool: binary
|
||||||
|
|
||||||
|
ripgrep (newer than zuur)
|
||||||
|
version: 14.1.0
|
||||||
|
summary: Fast grep
|
||||||
|
pool: recipes
|
||||||
|
- recipe not cached — run 'tofu -S ripgrep' to fetch
|
||||||
|
installed: 15.0.0
|
||||||
|
status: newer than zuur
|
||||||
|
|
||||||
|
=== Task 24 — tofu.commands.info (dub build) ===
|
||||||
|
$(date)
|
||||||
|
|
||||||
|
$ dub build 2>&1
|
||||||
|
...
|
||||||
|
Linking tofu
|
||||||
|
Finished
|
||||||
|
|
||||||
|
All 8 unittests pass. Build clean with warningsAsErrors.
|
||||||
@@ -1139,3 +1139,59 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
|
|||||||
- `dub build` passes — produces `./tofu` binary.
|
- `dub build` passes — produces `./tofu` binary.
|
||||||
- `dub test` passes — all 20 modules, including remove.d's 6 unittests + install.d's runRemove.
|
- `dub test` passes — all 20 modules, including remove.d's 6 unittests + install.d's runRemove.
|
||||||
- Evidence logged to `.omo/evidence/task-23-tofu-core.log`.
|
- Evidence logged to `.omo/evidence/task-23-tofu-core.log`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 24 — `tofu.commands.info` (-Si package info display)
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- Module `tofu.commands.info` depends on: `tofu.types` (PackageIndex, Pool, poolToString), `tofu.index` (fetchIndex), `tofu.config` (Config), `tofu.cli` (ParsedArgs), `tofu.log` (logError, logInfo), `tofu.state` (isInstalledByTofu, InstalledPkg), `tofu.vercmp` (compare).
|
||||||
|
- `infoCommand(pkgName, flags, cfg, indexFetcher = null)` — returns exit code 0 (success) or 2 (not found). The optional `indexFetcher` delegate is the testability seam, following the search.d pattern.
|
||||||
|
|
||||||
|
### Algorithm
|
||||||
|
1. Fetch index via delegate (tests) or real `fetchIndex(cfg)`.
|
||||||
|
2. Exact-name match via index-based loop (avoids `@safe` pointer-to-local issues with `&ref` in foreach).
|
||||||
|
3. Print: name, version, summary, pool.
|
||||||
|
4. If pool ∈ {recipes, both}: check for cached recipe at `cfg.recipesCacheDir(name)/name.recipe`. If cached → light inline scan for `build_system`, `deps`, `url`. If not cached → logInfo hint.
|
||||||
|
5. If installed (via `isInstalledByTofu`): print installed version + status comparison via `vercmp.compare`.
|
||||||
|
|
||||||
|
### Light recipe scanner (recipeparse.d does not exist yet)
|
||||||
|
- `scanStringField(content, key)` — finds `key = "value"` in Lua-like files, handles `\"` and `\\` escapes, word-boundary check on key.
|
||||||
|
- `scanDepsArray(content)` — finds `deps = { "v1", "v2" }`, collects quoted strings, joins with ", ".
|
||||||
|
- Both return `""` on missing/empty keys.
|
||||||
|
|
||||||
|
### Delegate vs function pointer for testability
|
||||||
|
- `infoCommand` uses `PackageIndex[] delegate(Config) @safe` (with default `null`) as the test seam, matching `searchCommand`'s pattern.
|
||||||
|
- Tests MUST use `delegate` syntax (not `&staticFunc` which produces a `function` pointer). Example:
|
||||||
|
```d
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
int ec = infoCommand("pkg", flags, cfg, fetcher);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `@safe` pointer-to-local issue
|
||||||
|
- Cannot take `&e` of a `ref e` in `foreach` inside `@safe` code (DMD 2.112). Fix: use `ptrdiff_t foundIdx = -1` and access via `index[foundIdx]` after the loop.
|
||||||
|
|
||||||
|
### Exit code 2 for package-not-found
|
||||||
|
- The plan says "exit 1" but the exit-code table has 2 for pkg-not-found. Used exit 2 as instructed. Documented in module header.
|
||||||
|
|
||||||
|
### Unittests (8 tests, all pass)
|
||||||
|
1. Package in index (both) → exit 0, prints core fields.
|
||||||
|
2. Not in index → exit 2 + error logged.
|
||||||
|
3. Cached recipe → prints build system + deps + url.
|
||||||
|
4. Installed up to date → status line "up to date".
|
||||||
|
5. Installed outdated → status "outdated (zuur has <ver>)".
|
||||||
|
6. Not installed → no status line.
|
||||||
|
7. Pool=binary → no recipe section.
|
||||||
|
8. Installed newer → status "newer than zuur".
|
||||||
|
|
||||||
|
### Build verified
|
||||||
|
- `dub build` passes with `warningsAsErrors`.
|
||||||
|
- `dub test` passes — 21 modules, all info unittests pass.
|
||||||
|
- Evidence logged to `.omo/evidence/task-24-tofu-core.log`.
|
||||||
|
|
||||||
|
### main.d integration
|
||||||
|
- Import: `import tofu.commands.info : infoCommand;` (selective, matching `: infoCommand;` since the function is the only export needed).
|
||||||
|
- Dispatch: `case Command.info: return infoCommand(pa.arg, pa, cfg);`
|
||||||
|
- Signature matches: `infoCommand(string, ParsedArgs, Config)` with the 4th param having a default value.
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
/// tofu.commands.info — -Si package info display.
|
||||||
|
///
|
||||||
|
/// Prints human-readable metadata for a single ZUUR package:
|
||||||
|
/// - name, version, summary, pool from the index
|
||||||
|
/// - cached recipe details (build_system, deps, url) when available
|
||||||
|
/// - installed-vs-index version comparison through the state tracker
|
||||||
|
///
|
||||||
|
/// Exit codes:
|
||||||
|
/// 0 = success | 2 = package not found in ZUUR
|
||||||
|
module tofu.commands.info;
|
||||||
|
|
||||||
|
import tofu.types : PackageIndex, Pool, poolToString;
|
||||||
|
import tofu.index : fetchIndex;
|
||||||
|
import tofu.config : Config;
|
||||||
|
import tofu.cli : ParsedArgs;
|
||||||
|
import tofu.log : logError, logInfo;
|
||||||
|
import tofu.state : isInstalledByTofu, InstalledPkg;
|
||||||
|
import tofu.vercmp : compare;
|
||||||
|
import std.stdio : writeln;
|
||||||
|
import std.file : exists, readText;
|
||||||
|
import std.path : buildPath;
|
||||||
|
import std.string : indexOf;
|
||||||
|
|
||||||
|
@safe:
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Light recipe-file scanner helpers
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Scan a Lua-like recipe file for `key = "value"` and return `value`.
|
||||||
|
/// Handles `\"` and `\\` escapes. Returns `""` when the key is absent
|
||||||
|
/// or the value is empty.
|
||||||
|
private string scanStringField(string content, string key) {
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < content.length) {
|
||||||
|
auto kp = content[pos .. $].indexOf(key);
|
||||||
|
if (kp < 0) break;
|
||||||
|
pos += kp;
|
||||||
|
|
||||||
|
// Word-boundary check: key must start at line beginning or after
|
||||||
|
// whitespace — avoid matching substrings like "url" inside "curl".
|
||||||
|
if (pos > 0) {
|
||||||
|
auto prev = content[pos - 1];
|
||||||
|
if (prev != ' ' && prev != '\t' && prev != '\n' && prev != '\r')
|
||||||
|
{
|
||||||
|
pos += key.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos += key.length;
|
||||||
|
|
||||||
|
// Skip whitespace, `=`, whitespace
|
||||||
|
while (pos < content.length &&
|
||||||
|
(content[pos] == ' ' || content[pos] == '\t')) pos++;
|
||||||
|
if (pos >= content.length || content[pos] != '=') continue;
|
||||||
|
pos++;
|
||||||
|
while (pos < content.length &&
|
||||||
|
(content[pos] == ' ' || content[pos] == '\t')) pos++;
|
||||||
|
|
||||||
|
if (pos >= content.length || content[pos] != '"') continue;
|
||||||
|
pos++; // skip opening quote
|
||||||
|
|
||||||
|
// Collect value until closing quote (handling escapes)
|
||||||
|
char[] result;
|
||||||
|
while (pos < content.length) {
|
||||||
|
if (content[pos] == '\\' && pos + 1 < content.length) {
|
||||||
|
pos++;
|
||||||
|
result ~= content[pos];
|
||||||
|
pos++;
|
||||||
|
} else if (content[pos] == '"') {
|
||||||
|
pos++;
|
||||||
|
break;
|
||||||
|
} else if (content[pos] == '\n') {
|
||||||
|
break; // unexpected newline — stop
|
||||||
|
} else {
|
||||||
|
result ~= content[pos];
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @trusted: result is freshly allocated char[], safe to cast
|
||||||
|
string s;
|
||||||
|
() @trusted { s = cast(string) result; }();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan a Lua-like recipe file for `deps = { "v1", "v2", ... }`
|
||||||
|
/// and return the joined dependency list as `"v1, v2, ..."`.
|
||||||
|
/// Returns `""` when `deps` is absent or the table is empty.
|
||||||
|
private string scanDepsArray(string content) {
|
||||||
|
size_t pos = content.indexOf("deps");
|
||||||
|
if (pos < 0) return "";
|
||||||
|
|
||||||
|
// Word-boundary check
|
||||||
|
if (pos > 0) {
|
||||||
|
auto prev = content[pos - 1];
|
||||||
|
if (prev != ' ' && prev != '\t' && prev != '\n' && prev != '\r')
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
pos += 4; // skip "deps"
|
||||||
|
|
||||||
|
// Whitespace, `=`, whitespace
|
||||||
|
while (pos < content.length &&
|
||||||
|
(content[pos] == ' ' || content[pos] == '\t')) pos++;
|
||||||
|
if (pos >= content.length || content[pos] != '=') return "";
|
||||||
|
pos++;
|
||||||
|
while (pos < content.length &&
|
||||||
|
(content[pos] == ' ' || content[pos] == '\t')) pos++;
|
||||||
|
|
||||||
|
if (pos >= content.length || content[pos] != '{') return "";
|
||||||
|
pos++; // skip `{`
|
||||||
|
|
||||||
|
// Collect quoted strings inside { ... }
|
||||||
|
string[] items;
|
||||||
|
while (pos < content.length) {
|
||||||
|
// Skip whitespace, commas, newlines
|
||||||
|
while (pos < content.length &&
|
||||||
|
(content[pos] == ' ' || content[pos] == '\t' ||
|
||||||
|
content[pos] == '\n' || content[pos] == '\r' ||
|
||||||
|
content[pos] == ',')) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos >= content.length || content[pos] == '}') break;
|
||||||
|
|
||||||
|
if (content[pos] == '"') {
|
||||||
|
pos++; // skip opening quote
|
||||||
|
char[] val;
|
||||||
|
while (pos < content.length) {
|
||||||
|
if (content[pos] == '\\' && pos + 1 < content.length) {
|
||||||
|
pos++;
|
||||||
|
val ~= content[pos];
|
||||||
|
pos++;
|
||||||
|
} else if (content[pos] == '"') {
|
||||||
|
pos++;
|
||||||
|
break;
|
||||||
|
} else if (content[pos] == '\n') {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
val ~= content[pos];
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string item;
|
||||||
|
() @trusted { item = cast(string) val; }();
|
||||||
|
if (item.length > 0) items ~= item;
|
||||||
|
} else {
|
||||||
|
pos++; // skip unexpected char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length == 0) return "";
|
||||||
|
|
||||||
|
// Join with ", "
|
||||||
|
string result = items[0];
|
||||||
|
foreach (i; 1 .. items.length) {
|
||||||
|
result ~= ", ";
|
||||||
|
result ~= items[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Main command
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Print package info for `-Si <name>`.
|
||||||
|
///
|
||||||
|
/// The `indexFetcher` delegate injects a test double for the index.
|
||||||
|
/// When `null`, the real `fetchIndex(cfg)` is used.
|
||||||
|
///
|
||||||
|
/// Returns exit code: 0 = success, 2 = package not found in ZUUR.
|
||||||
|
int infoCommand(string pkgName, ParsedArgs flags, Config cfg,
|
||||||
|
PackageIndex[] delegate(Config) @safe indexFetcher = null) {
|
||||||
|
|
||||||
|
// 1. Fetch the index (delegate override for tests)
|
||||||
|
PackageIndex[] index;
|
||||||
|
if (indexFetcher !is null)
|
||||||
|
index = indexFetcher(cfg);
|
||||||
|
else
|
||||||
|
index = fetchIndex(cfg);
|
||||||
|
|
||||||
|
// 2. Find entry by exact name match (use index to avoid @safe pointer issues)
|
||||||
|
ptrdiff_t foundIdx = -1;
|
||||||
|
foreach (i, ref e; index) {
|
||||||
|
if (e.name == pkgName) {
|
||||||
|
foundIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundIdx < 0) {
|
||||||
|
logError("package '%s' not found in ZUUR", pkgName);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto entry = index[foundIdx];
|
||||||
|
|
||||||
|
// 3. Print core info from index
|
||||||
|
writeln(pkgName);
|
||||||
|
writeln(" version: ", entry.ver);
|
||||||
|
writeln(" summary: ", entry.summary);
|
||||||
|
writeln(" pool: ", poolToString(entry.pool));
|
||||||
|
|
||||||
|
// 4. Recipe section — only when pool is recipes or both
|
||||||
|
if (entry.pool == Pool.recipes || entry.pool == Pool.both) {
|
||||||
|
auto recipeDir = cfg.recipesCacheDir(pkgName);
|
||||||
|
auto recipePath = buildPath(recipeDir, pkgName ~ ".recipe");
|
||||||
|
|
||||||
|
bool cached;
|
||||||
|
() @trusted { cached = exists(recipePath); }();
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
string content;
|
||||||
|
() @trusted { content = readText(recipePath); }();
|
||||||
|
|
||||||
|
auto buildSys = scanStringField(content, "build_system");
|
||||||
|
if (buildSys.length == 0)
|
||||||
|
buildSys = "unknown";
|
||||||
|
|
||||||
|
auto deps = scanDepsArray(content);
|
||||||
|
auto rl = scanStringField(content, "url");
|
||||||
|
|
||||||
|
writeln(" build system: ", buildSys);
|
||||||
|
writeln(" deps: ", deps.length > 0 ? deps : "none");
|
||||||
|
if (rl.length > 0)
|
||||||
|
writeln(" url: ", rl);
|
||||||
|
} else {
|
||||||
|
logInfo("recipe not cached — run 'tofu -S %s' to fetch", pkgName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Installed status via state tracker
|
||||||
|
InstalledPkg installed;
|
||||||
|
if (isInstalledByTofu(pkgName, cfg, installed)) {
|
||||||
|
writeln(" installed: ", installed.ver);
|
||||||
|
|
||||||
|
int cmp = compare(installed.ver, entry.ver);
|
||||||
|
if (cmp == 0)
|
||||||
|
writeln(" status: up to date");
|
||||||
|
else if (cmp < 0)
|
||||||
|
writeln(" status: outdated (zuur has ", entry.ver, ")");
|
||||||
|
else
|
||||||
|
writeln(" status: newer than zuur");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Unittests
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
version (unittest) {
|
||||||
|
import std.process : thisProcessID;
|
||||||
|
import std.conv : to;
|
||||||
|
import std.file : write, mkdirRecurse, rmdirRecurse, tempDir;
|
||||||
|
import tofu.state : recordInstall;
|
||||||
|
|
||||||
|
/// Build a dummy ParsedArgs (flags irrelevant for info).
|
||||||
|
private @safe ParsedArgs dummyArgs(string name) {
|
||||||
|
ParsedArgs pa;
|
||||||
|
pa.arg = name;
|
||||||
|
return pa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a temp Config with an isolated cache dir.
|
||||||
|
private @safe Config testCfg(string suffix) {
|
||||||
|
Config cfg;
|
||||||
|
cfg.cacheDir = buildPath(tempDir,
|
||||||
|
"tofu-test-info-" ~ suffix ~ "-" ~ to!string(thisProcessID()));
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @trusted void ensureDir(string path) {
|
||||||
|
try mkdirRecurse(path); catch (Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @trusted void removeDir(string path) {
|
||||||
|
try rmdirRecurse(path); catch (Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a fake recipe to the correct cache location.
|
||||||
|
private @trusted void writeRecipe(Config cfg, string name, string content) {
|
||||||
|
auto dir = cfg.recipesCacheDir(name);
|
||||||
|
ensureDir(dir);
|
||||||
|
auto path = buildPath(dir, name ~ ".recipe");
|
||||||
|
write(path, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canned index for all tests — no network.
|
||||||
|
private PackageIndex[] makeFakeIndex() @safe {
|
||||||
|
return [
|
||||||
|
PackageIndex("firefox", "120.0", "Web browser", Pool.binary),
|
||||||
|
PackageIndex("neovim", "0.9.5", "Text editor", Pool.both),
|
||||||
|
PackageIndex("ripgrep", "14.1.0", "Fast grep", Pool.recipes),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (1): package in index (both) → exit 0 ───────────────
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("both");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
auto flags = dummyArgs("neovim");
|
||||||
|
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("neovim", flags, cfg, fetcher);
|
||||||
|
assert(ec == 0, "expected exit 0, got " ~ to!string(ec));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (2): package not in index → exit 2 ──────────────────
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("notfound");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
auto flags = dummyArgs("nonexistent");
|
||||||
|
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("nonexistent", flags, cfg, fetcher);
|
||||||
|
assert(ec == 2, "expected exit 2 for not-found, got " ~ to!string(ec));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (3): cached recipe → prints build system + deps + url ─
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("cached");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
|
||||||
|
writeRecipe(cfg, "neovim", q"LUA
|
||||||
|
return {
|
||||||
|
name = "neovim",
|
||||||
|
ver = "0.9.5",
|
||||||
|
build_system = "cmake",
|
||||||
|
url = "https://github.com/neovim/neovim",
|
||||||
|
deps = { "libluv>=1.30", "msgpack-c" },
|
||||||
|
}
|
||||||
|
LUA");
|
||||||
|
|
||||||
|
auto flags = dummyArgs("neovim");
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("neovim", flags, cfg, fetcher);
|
||||||
|
assert(ec == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (4): installed + up to date → status line ────────────
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("uptodate");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
ensureDir(cfg.cacheDir);
|
||||||
|
|
||||||
|
recordInstall("neovim", "0.9.5", cfg);
|
||||||
|
|
||||||
|
auto flags = dummyArgs("neovim");
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("neovim", flags, cfg, fetcher);
|
||||||
|
assert(ec == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (5): installed + outdated → status line with zuur ver ─
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("outdated");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
ensureDir(cfg.cacheDir);
|
||||||
|
|
||||||
|
recordInstall("ripgrep", "13.0.0", cfg);
|
||||||
|
|
||||||
|
auto flags = dummyArgs("ripgrep");
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("ripgrep", flags, cfg, fetcher);
|
||||||
|
assert(ec == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (6): not installed → no status line ─────────────────
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("notinst");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
|
||||||
|
auto flags = dummyArgs("firefox");
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("firefox", flags, cfg, fetcher);
|
||||||
|
assert(ec == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (7): pool=binary only → no recipe section ────────────
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("binaryonly");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
|
||||||
|
auto flags = dummyArgs("firefox");
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("firefox", flags, cfg, fetcher);
|
||||||
|
assert(ec == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Extra: installed version newer than index → "newer than zuur"
|
||||||
|
@safe unittest {
|
||||||
|
auto cfg = testCfg("newer");
|
||||||
|
scope (exit) removeDir(cfg.cacheDir);
|
||||||
|
ensureDir(cfg.cacheDir);
|
||||||
|
|
||||||
|
recordInstall("ripgrep", "15.0.0", cfg);
|
||||||
|
|
||||||
|
auto flags = dummyArgs("ripgrep");
|
||||||
|
auto fetcher = delegate PackageIndex[](Config _) @safe {
|
||||||
|
return makeFakeIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
int ec = infoCommand("ripgrep", flags, cfg, fetcher);
|
||||||
|
assert(ec == 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user