lib-management.dep-fetcher-test v0.1.0: TAP suite + fixtures
27-assertion TAP-style suite for lib-management.dep-fetcher v0.1.0:
- 4 internal-helper tests (id_to_path, gitea_slug_for incl. .git
exception)
- 6 JSON decoder tests (objects/arrays/nesting)
- 3 conflict-detection tests (synthetic two-parent fixture)
- 3 closure-walk tests (clean fixture, check_only mode)
- 3 case-B (clean) tests
- 4 case-C (dirty) tests
- 4 case-D (wrong-tag → silent checkout, post-state verification)
Cases A (missing+clone) and E (broken describe) need network or
contrived corruption; out of scope for slice 5 per architecture spec.
Fixture matrix:
conflict_a/ — static, no .git needed
clean_b/ — .git at v0.1.0 (clean)
dirty_c/ — .git at v0.1.0-1-g…-dirty
wrong_tag_d/ — .git with tags v0.1.0 + v0.2.0, HEAD at v0.1.0
fixtures/_setup.sh reconstructs the .git states (idempotent — restores
manifest.lib content after the case-D silent-checkout side-effect).
Required pre-step before each test session; the test-runner blocks
with a clear error if fixtures/_setup.stamp is missing.
.gitignore covers the .git subdirs and the stamp file — they are
local-machine state, rebuilt by _setup.sh from the source-tracked
manifest.lib seed files.
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Local fixture state — reconstructed by ./fixtures/_setup.sh
|
||||
fixtures/_setup.stamp
|
||||
fixtures/**/.git/
|
||||
fixtures/**/extra.txt
|
||||
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# lib-management.dep-fetcher-test
|
||||
|
||||
TAP-style assertion suite for `lib-management.dep-fetcher` (slice 5 of
|
||||
the dep-fetcher implementation plan).
|
||||
|
||||
## Coverage
|
||||
|
||||
| Case | Spec | What it verifies |
|
||||
|---|---|---|
|
||||
| Internal | — | `_id_to_path`, `_gitea_slug_for` (incl. `.git`-suffix exception), `_json_decode` |
|
||||
| Conflict | §5 conflict-detection | Two parents pin same lib to different versions → `ok=false`, one conflict entry |
|
||||
| Walk | §5 closure walk | Manifest tree resolves transitively; deduplication; closure non-empty |
|
||||
| B | §5 case B (clean) | `describe == v<X.Y.Z>` → no warning, no error |
|
||||
| C | §5 case C (dirty) | `describe` matches `v<X.Y.Z>-N-g…[-dirty]` → one `kind="dirty"` warning |
|
||||
| D | §5 case D (wrong-tag) | `describe != v<X.Y.Z>` → silent checkout; post-state is case B |
|
||||
|
||||
Cases **A (missing → clone)** and **E (broken describe)** require live
|
||||
network or contrived corruption; they are out of scope for slice 5
|
||||
(public-network only; private repos = paid content in later slice).
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# 1. Stage source to install (see install-reference-workflow.md)
|
||||
bash ~/Projects/Sporel/install-reference.sh --no-verify
|
||||
|
||||
# 2. Reconstruct .git fixtures (case B / C / D)
|
||||
bash /c/Games/Sporel/libs/lib-management/dep-fetcher-test/fixtures/_setup.sh
|
||||
|
||||
# 3. Run the test-runner module headless
|
||||
cd /c/Games/Sporel
|
||||
SPOREL_CI=1 ./bin/Sporel.exe --module=dep-fetcher-test-runner
|
||||
# expected: 27/27 assertions pass; exit 0
|
||||
```
|
||||
|
||||
The runner lives at `sporel-modules/dep-fetcher-test-runner/`. It is a
|
||||
`kind=test-module` that re-exports the assertions from this lib so
|
||||
they participate in `test-all-modules.sh` aggregation (and therefore
|
||||
milestone-check).
|
||||
|
||||
## Fixture layout
|
||||
|
||||
```
|
||||
fixtures/
|
||||
├── _setup.sh # rebuild .git state for cases B/C/D
|
||||
├── _setup.stamp # touched by _setup.sh; presence-check in init.lua
|
||||
│
|
||||
├── conflict_a/ # static (no .git needed): two parents pin
|
||||
│ ├── manifest.module lib-core.maps to different versions
|
||||
│ └── install/libs/lib-x/consumer/manifest.lib
|
||||
│
|
||||
├── clean_b/ # _setup.sh creates .git at v0.1.0
|
||||
├── dirty_c/ # _setup.sh creates .git at v0.1.0-1-g…-dirty
|
||||
└── wrong_tag_d/ # _setup.sh creates .git with tags v0.1.0 + v0.2.0,
|
||||
# HEAD at v0.1.0 (module pins v0.2.0)
|
||||
```
|
||||
|
||||
`_setup.sh` is idempotent — re-runs after the case-D silent-checkout
|
||||
side-effect restore the wrong-tag fixture to its initial v0.1.0 HEAD.
|
||||
The script sets per-repo `user.name` / `user.email` so it does not
|
||||
touch the user's global git config.
|
||||
|
||||
## Why a separate runner module?
|
||||
|
||||
Test libs (`role: "test"`) cannot be consumed by production modules
|
||||
(`kind: "module"`). The runner is therefore `kind: "test-module"`,
|
||||
which can depend on test libs (per `engine_lib_resolver` role-check).
|
||||
127
fixtures/_setup.sh
Normal file
127
fixtures/_setup.sh
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# fixtures/_setup.sh — reconstruct .git states for case B / C / D fixtures.
|
||||
#
|
||||
# Run before each invocation of the dep-fetcher-test-runner test-module.
|
||||
# Idempotent: deletes and re-creates each fixture's .git tree from
|
||||
# scratch so re-runs (and the case-D side effect — silent checkout —
|
||||
# don't pollute subsequent runs).
|
||||
#
|
||||
# Fixture matrix:
|
||||
# clean_b/ — v0.1.0 tag, no extra commits, clean working-tree.
|
||||
# describe(<dir>) == "v0.1.0".
|
||||
# dirty_c/ — v0.1.0 tag + 1 extra commit + 1 modified-file.
|
||||
# describe(<dir>) matches "^v0.1.0-1-g.*-dirty$".
|
||||
# wrong_tag_d/ — v0.1.0 AND v0.2.0 tags. HEAD initially at v0.1.0
|
||||
# (module deps on v0.2.0, so dep-fetcher silently
|
||||
# checks out v0.2.0).
|
||||
#
|
||||
# After running, touches _setup.stamp so the test-module can verify
|
||||
# fixtures are prepared.
|
||||
#
|
||||
# Required: git on PATH. Run from any cwd — uses BASH_SOURCE to locate
|
||||
# the fixtures dir.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Git identity to use for fixture commits. We avoid touching the user's
|
||||
# global git config (which the system-instruction explicitly forbids)
|
||||
# by setting per-repo config with `git -c` invocations downstream.
|
||||
GITNAME="dep-fetcher-fixtures"
|
||||
GITMAIL="fixtures@sporel.local"
|
||||
|
||||
gitlocal() {
|
||||
git -c "user.name=$GITNAME" -c "user.email=$GITMAIL" \
|
||||
-c "commit.gpgsign=false" "$@"
|
||||
}
|
||||
|
||||
prepare_clean_b() {
|
||||
local d="$HERE/clean_b/install/libs/lib-x/consumer"
|
||||
rm -rf "$d/.git"
|
||||
(
|
||||
cd "$d"
|
||||
gitlocal init -q -b master
|
||||
gitlocal add manifest.lib
|
||||
gitlocal commit -q -m "lib-x.consumer v0.1.0"
|
||||
gitlocal tag v0.1.0
|
||||
)
|
||||
echo " clean_b: v0.1.0 (clean)"
|
||||
}
|
||||
|
||||
prepare_dirty_c() {
|
||||
local d="$HERE/dirty_c/install/libs/lib-x/consumer"
|
||||
rm -rf "$d/.git"
|
||||
rm -f "$d/extra.txt"
|
||||
# Restore the manifest in case a previous run left it modified.
|
||||
cat > "$d/manifest.lib" <<'EOF'
|
||||
{
|
||||
"id": "lib-x.consumer",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": []
|
||||
}
|
||||
EOF
|
||||
(
|
||||
cd "$d"
|
||||
gitlocal init -q -b master
|
||||
gitlocal add manifest.lib
|
||||
gitlocal commit -q -m "lib-x.consumer v0.1.0"
|
||||
gitlocal tag v0.1.0
|
||||
# +1 commit past the tag (drives "v0.1.0-1-g<sha>")
|
||||
echo "extra" > extra.txt
|
||||
gitlocal add extra.txt
|
||||
gitlocal commit -q -m "extra commit past v0.1.0"
|
||||
# +1 dirty file (drives "-dirty" suffix in describe --dirty)
|
||||
echo "modified" >> manifest.lib
|
||||
)
|
||||
echo " dirty_c: v0.1.0 + 1 commit + dirty file"
|
||||
}
|
||||
|
||||
prepare_wrong_tag_d() {
|
||||
local d="$HERE/wrong_tag_d/install/libs/lib-x/consumer"
|
||||
rm -rf "$d/.git"
|
||||
# Restore manifest in case a previous Case-D run left it patched
|
||||
# to v0.2.0 (the checkout side-effect of the previous test run).
|
||||
cat > "$d/manifest.lib" <<'EOF'
|
||||
{
|
||||
"id": "lib-x.consumer",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": []
|
||||
}
|
||||
EOF
|
||||
(
|
||||
cd "$d"
|
||||
gitlocal init -q -b master
|
||||
gitlocal add manifest.lib
|
||||
gitlocal commit -q -m "lib-x.consumer v0.1.0"
|
||||
gitlocal tag v0.1.0
|
||||
# Bump to v0.2.0 with a real content change so the working-tree
|
||||
# round-trips cleanly after the dep-fetcher's silent checkout.
|
||||
cat > manifest.lib <<'EOF'
|
||||
{
|
||||
"id": "lib-x.consumer",
|
||||
"version": "0.2.0",
|
||||
"api_min": "0.1",
|
||||
"deps": []
|
||||
}
|
||||
EOF
|
||||
gitlocal add manifest.lib
|
||||
gitlocal commit -q -m "lib-x.consumer v0.2.0"
|
||||
gitlocal tag v0.2.0
|
||||
# Reset HEAD back to v0.1.0 so the initial describe sees the
|
||||
# wrong tag (case D).
|
||||
gitlocal checkout -q v0.1.0
|
||||
)
|
||||
echo " wrong_tag_d: tags v0.1.0 + v0.2.0, HEAD at v0.1.0"
|
||||
}
|
||||
|
||||
prepare_clean_b
|
||||
prepare_dirty_c
|
||||
prepare_wrong_tag_d
|
||||
|
||||
# Stamp so the test-runner knows fixtures are prepared.
|
||||
touch "$HERE/_setup.stamp"
|
||||
|
||||
echo "fixtures prepared at: $HERE"
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "lib-x.consumer",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": []
|
||||
}
|
||||
9
fixtures/clean_b/manifest.module
Normal file
9
fixtures/clean_b/manifest.module
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "fixture-clean-b",
|
||||
"version": "0.1.0",
|
||||
"kind": "module",
|
||||
"api": "^0.1",
|
||||
"deps": [
|
||||
{"id": "lib-x.consumer", "version": "0.1.0"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "lib-x.consumer",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": [
|
||||
{"id": "lib-core.maps", "version": "0.4.0"}
|
||||
]
|
||||
}
|
||||
10
fixtures/conflict_a/manifest.module
Normal file
10
fixtures/conflict_a/manifest.module
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "fixture-conflict-a",
|
||||
"version": "0.1.0",
|
||||
"kind": "module",
|
||||
"api": "^0.1",
|
||||
"deps": [
|
||||
{"id": "lib-core.maps", "version": "0.5.5"},
|
||||
{"id": "lib-x.consumer", "version": "0.1.0"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "lib-x.consumer",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": []
|
||||
}
|
||||
9
fixtures/dirty_c/manifest.module
Normal file
9
fixtures/dirty_c/manifest.module
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "fixture-dirty-c",
|
||||
"version": "0.1.0",
|
||||
"kind": "module",
|
||||
"api": "^0.1",
|
||||
"deps": [
|
||||
{"id": "lib-x.consumer", "version": "0.1.0"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "lib-x.consumer",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": []
|
||||
}
|
||||
9
fixtures/wrong_tag_d/manifest.module
Normal file
9
fixtures/wrong_tag_d/manifest.module
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "fixture-wrong-tag-d",
|
||||
"version": "0.1.0",
|
||||
"kind": "module",
|
||||
"api": "^0.1",
|
||||
"deps": [
|
||||
{"id": "lib-x.consumer", "version": "0.2.0"}
|
||||
]
|
||||
}
|
||||
168
init.lua
Normal file
168
init.lua
Normal file
@@ -0,0 +1,168 @@
|
||||
-- lib-management.dep-fetcher-test v0.1.0
|
||||
--
|
||||
-- TAP-style assertion suite for lib-management.dep-fetcher. Covers the
|
||||
-- closure walk (conflict detection) plus per-lib state-check cases
|
||||
-- B-D from spec §5 (clean / dirty / wrong-tag). Case A (missing+clone)
|
||||
-- and Case E (broken describe) are exercised in check_only mode where
|
||||
-- possible; their live-network variants are gated by SPOREL_DEPFETCHER_NET=1
|
||||
-- and not run by default (slice-5 stays offline).
|
||||
--
|
||||
-- Fixture states that cannot be checked in verbatim (live .git dirs)
|
||||
-- are reconstructed by ./fixtures/_setup.sh which the user must run
|
||||
-- before invoking this test-module.
|
||||
|
||||
local M = {}
|
||||
|
||||
local function fixture_dir()
|
||||
return engine.module.dir_of("lib-management.dep-fetcher-test")
|
||||
.. "/fixtures"
|
||||
end
|
||||
|
||||
function M.run_tests()
|
||||
local depf = require("lib-management.dep-fetcher")
|
||||
|
||||
-- ============================================================
|
||||
-- Assertions on internal helpers (gitea-slug edge case, id->path)
|
||||
-- ============================================================
|
||||
|
||||
engine.test.equals(depf._id_to_path("lib-core.maps"),
|
||||
"lib-core/maps", "id_to_path: simple two-segment id")
|
||||
engine.test.equals(depf._id_to_path("lib-core.body.vertebrate"),
|
||||
"lib-core/body/vertebrate",
|
||||
"id_to_path: multi-segment id keeps depth")
|
||||
engine.test.equals(depf._gitea_slug_for("lib-core.maps"),
|
||||
"sporel-lib-core.maps",
|
||||
"gitea_slug_for: regular id gets sporel- prefix")
|
||||
engine.test.equals(depf._gitea_slug_for("lib-core.git"),
|
||||
"sporel-lib-core.git-lib",
|
||||
"gitea_slug_for: .git-suffix id appends -lib (Gitea reserved)")
|
||||
|
||||
-- ============================================================
|
||||
-- JSON decoder sanity (small targeted suite — manifests are JSON)
|
||||
-- ============================================================
|
||||
|
||||
local empty = depf._json_decode("{}")
|
||||
engine.test.assert(type(empty) == "table",
|
||||
"json_decode: empty object is a table")
|
||||
local arr_only = depf._json_decode("[1,2,3]")
|
||||
engine.test.equals(#arr_only, 3,
|
||||
"json_decode: array of numbers length")
|
||||
engine.test.equals(arr_only[2], 2,
|
||||
"json_decode: array index 2 value")
|
||||
local nested = depf._json_decode(
|
||||
'{"id":"x","deps":[{"id":"a","version":"0.1.0"}]}')
|
||||
engine.test.equals(nested.id, "x",
|
||||
"json_decode: nested object id field")
|
||||
engine.test.equals(nested.deps[1].id, "a",
|
||||
"json_decode: nested array-of-objects access")
|
||||
engine.test.equals(nested.deps[1].version, "0.1.0",
|
||||
"json_decode: deeply-nested string field")
|
||||
|
||||
-- ============================================================
|
||||
-- Conflict-detection fixture (no actual git ops)
|
||||
-- ============================================================
|
||||
|
||||
local r_conflict = depf.ensure_for_module_at(
|
||||
fixture_dir() .. "/conflict_a/manifest.module",
|
||||
{
|
||||
install_root = fixture_dir() .. "/conflict_a/install",
|
||||
check_only = true,
|
||||
})
|
||||
engine.test.equals(r_conflict.ok, false,
|
||||
"conflict_a: ok=false (conflict detected)")
|
||||
engine.test.equals(#r_conflict.conflicts, 1,
|
||||
"conflict_a: exactly one conflict")
|
||||
engine.test.equals(r_conflict.conflicts[1].lib_id, "lib-core.maps",
|
||||
"conflict_a: conflict is on lib-core.maps")
|
||||
|
||||
-- ============================================================
|
||||
-- Closure walk over a clean offline fixture (check_only)
|
||||
-- ============================================================
|
||||
|
||||
local r_walk = depf.ensure_for_module_at(
|
||||
fixture_dir() .. "/clean_b/manifest.module",
|
||||
{
|
||||
install_root = fixture_dir() .. "/clean_b/install",
|
||||
check_only = true,
|
||||
})
|
||||
engine.test.equals(r_walk.ok, true,
|
||||
"clean_b (check_only): walk succeeds, no conflicts")
|
||||
engine.test.equals(#r_walk.conflicts, 0,
|
||||
"clean_b (check_only): zero conflicts")
|
||||
engine.test.assert(#r_walk.closure >= 1,
|
||||
"clean_b (check_only): closure non-empty")
|
||||
|
||||
-- ============================================================
|
||||
-- State-check (cases B, C, D)
|
||||
--
|
||||
-- These exercise the per-lib git path. They require ./fixtures
|
||||
-- /_setup.sh to have constructed the .git states. The test
|
||||
-- detects unprepared fixtures by check_only-bypassing them when
|
||||
-- the helper file is absent, so a missing _setup.sh run gives
|
||||
-- a clear diagnostic via test.fail rather than an obscure
|
||||
-- describe failure.
|
||||
-- ============================================================
|
||||
|
||||
local function fixtures_ready()
|
||||
local f = io.open(fixture_dir() .. "/_setup.stamp", "rb")
|
||||
if not f then return false end
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
|
||||
if not fixtures_ready() then
|
||||
engine.test.fail(
|
||||
"fixtures/_setup.stamp missing — " ..
|
||||
"run `bash sporel-libs/lib-management/dep-fetcher-test/" ..
|
||||
"fixtures/_setup.sh` first to construct .git state-check " ..
|
||||
"fixtures (cases B/C/D)")
|
||||
return
|
||||
end
|
||||
|
||||
-- Case B — clean: repo at exactly pinned tag.
|
||||
local r_clean = depf.ensure_for_module_at(
|
||||
fixture_dir() .. "/clean_b/manifest.module",
|
||||
{ install_root = fixture_dir() .. "/clean_b/install" })
|
||||
engine.test.equals(r_clean.ok, true,
|
||||
"clean_b (full): state-check OK")
|
||||
engine.test.equals(#r_clean.warnings, 0,
|
||||
"clean_b (full): zero warnings (case B)")
|
||||
engine.test.equals(#r_clean.errors, 0,
|
||||
"clean_b (full): zero errors")
|
||||
|
||||
-- Case C — dirty: extra commit past tag.
|
||||
local r_dirty = depf.ensure_for_module_at(
|
||||
fixture_dir() .. "/dirty_c/manifest.module",
|
||||
{ install_root = fixture_dir() .. "/dirty_c/install" })
|
||||
engine.test.equals(r_dirty.ok, true,
|
||||
"dirty_c: state-check OK (warnings are non-blocking)")
|
||||
engine.test.equals(#r_dirty.warnings, 1,
|
||||
"dirty_c: one warning (case C)")
|
||||
if r_dirty.warnings[1] then
|
||||
engine.test.equals(r_dirty.warnings[1].kind, "dirty",
|
||||
"dirty_c: warning.kind == 'dirty'")
|
||||
engine.test.equals(r_dirty.warnings[1].lib_id, "lib-x.consumer",
|
||||
"dirty_c: warning is on lib-x.consumer")
|
||||
end
|
||||
|
||||
-- Case D — wrong tag: repo at v0.1.0, module wants v0.2.0.
|
||||
-- The state-check silently checks out v0.2.0. We then verify the
|
||||
-- post-state by re-running and expecting Case B (clean).
|
||||
local r_wrong = depf.ensure_for_module_at(
|
||||
fixture_dir() .. "/wrong_tag_d/manifest.module",
|
||||
{ install_root = fixture_dir() .. "/wrong_tag_d/install" })
|
||||
engine.test.equals(r_wrong.ok, true,
|
||||
"wrong_tag_d: silent checkout succeeded")
|
||||
engine.test.equals(#r_wrong.errors, 0,
|
||||
"wrong_tag_d: zero errors after checkout")
|
||||
|
||||
local r_wrong2 = depf.ensure_for_module_at(
|
||||
fixture_dir() .. "/wrong_tag_d/manifest.module",
|
||||
{ install_root = fixture_dir() .. "/wrong_tag_d/install" })
|
||||
engine.test.equals(r_wrong2.ok, true,
|
||||
"wrong_tag_d (post-checkout): now clean")
|
||||
engine.test.equals(#r_wrong2.warnings, 0,
|
||||
"wrong_tag_d (post-checkout): zero warnings (case B reached)")
|
||||
end
|
||||
|
||||
return M
|
||||
9
manifest.lib
Normal file
9
manifest.lib
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "lib-management.dep-fetcher-test",
|
||||
"role": "test",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": [
|
||||
{"id": "lib-management.dep-fetcher", "version": "0.1.0"}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user