Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,14 @@ jobs:
- export.json
- gh-write.json
- land-batch.json
- land-batch-merge.json
- landing-lib-ci.json
- landing-lib-gates.json
- landing-lib-inputs.json
- landing-lib-model.json
- landing-lib-risk.json
- landing-lib-verdict.json
- landing-lib-workflow.json
- lane-hygiene.json
- lane-land-checks.json
- lane-land-config.json
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes are listed here. Versions follow [Semantic Versioning](https

### Added

- `tools/export.py` copies its own files (the tool, its lint and their tests) verbatim, and a
test checks that exporting the repo changes Markdown prose only. The export had dropped a
fixture from the public copy of its own tests, which let a mutant live there.

- CI runs install.sh's dry runs under bash 3.2 on Linux (`bash32`, the `bash:3.2` image pinned
by digest) on every PR. The `macos` job and the `mutants` matrix are off while the repo is
private, and `ci-ok` accepts their skip only then. `tools/check-workflows.py` fails a macOS
Expand Down
6 changes: 6 additions & 0 deletions docs/tdd/export-red.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ FAILED (failures=1)

The tool describing the clone-path form in its own docs (before the MAY_NAME exemption):
FAIL: test_the_tool_itself_may_describe_the_clone_path_form

The tool's own files transformed (found by the public repo's first CI run: the export dropped a
drop-region fixture from tests/test_export.py, and the "blank lines around a dropped region
doubled" mutant survived there). The new tests on the old export.py:
FAIL: test_exporting_this_repo_leaves_the_tools_files_unchanged
FAIL: test_the_tools_own_files_are_copied_verbatim
3 changes: 2 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ doesn't occur exactly once in its target, so editing a guarded line shows the mu
at lint time, not after a mutation run. Keep each spec well inside the job's
`timeout-minutes`: when one grows past about ten minutes, split its mutants into specs by area,
with the same target and suite, as lane-land's `-checks`, `-config`, `-gates`, `-verdict` and
`-merge` specs do. `tools/mutate.sh` (install.sh's mutants) runs in a job
`-merge` specs and the landing-lib and land-batch specs do. Those run two bats suites per mutant,
about a minute each on CI's runners, so keep them to about six mutants. `tools/mutate.sh` (install.sh's mutants) runs in a job
of its own too, against `tests/install.bats` only, the suite that runs `install.sh`.

## The checks invariant
Expand Down
16 changes: 8 additions & 8 deletions tests/mutants/export.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
{
"name": "an unknown citation form passes",
"old": " if PLACEHOLDER in text and rel not in MAY_NAME:",
"old": " if PLACEHOLDER in text:",
"new": " if False:"
},
{
Expand All @@ -49,8 +49,8 @@
},
{
"name": "a dev-repo PR or issue link is let through",
"old": " if DEV_LINK.search(text):",
"new": " if False:"
"old": " if DEV_LINK.search(text):\n raise ValueError(",
"new": " if False:\n raise ValueError("
},
{
"name": "only PR links are caught, not issues",
Expand All @@ -59,18 +59,18 @@
},
{
"name": "a plain-text lesson citation is let through",
"old": " if rel not in MAY_NAME and PLAIN_CITATION.search(text):",
"old": " if PLAIN_CITATION.search(text):",
"new": " if False:"
},
{
"name": "a local clone path is let through",
"old": " if rel not in MAY_NAME and CLONE_PATH.search(text):",
"old": " if CLONE_PATH.search(text):",
"new": " if False:"
},
{
"name": "the tool may not describe the clone path form",
"old": " if rel not in MAY_NAME and CLONE_PATH.search(text):",
"new": " if CLONE_PATH.search(text):"
"name": "the tool's own files are transformed",
"old": " refuse_dev_links(rel, text)\n return text\n",
"new": " refuse_dev_links(rel, text)\n"
}
]
}
61 changes: 61 additions & 0 deletions tests/mutants/land-batch-merge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"target": "plugins/crew-orchestrator/bin/land-batch",
"suite": [
"bats",
"--abort",
"tests/landing-lib.bats",
"tests/land-batch.bats"
],
"mutants": [
{
"name": "squash not pinned to the verified head",
"old": "--squash --match-head-commit \"${HEAD[$pr]}\"",
"new": "--squash"
},
{
"name": "parent check skipped",
"old": " [[ $(git -C \"$WT\" rev-parse \"$mc^\" 2>/dev/null) == \"$prev\" ]] ||",
"new": " true ||"
},
{
"name": "tree check skipped",
"old": " [[ $(git -C \"$WT\" rev-parse \"$mc^{tree}\") == \"${TREE[$pr]}\" ]] ||",
"new": " true ||"
},
{
"name": "main not re-checked before each squash",
"old": " main_is \"$prev\" || stop_merge 7",
"new": " true || stop_merge 7"
},
{
"name": "no recheck before merging",
"old": " if [[ -n $PROB || $PR_HEAD != \"${HEAD[$pr]}\" ]]; then",
"new": " if false; then"
},
{
"name": "up-to-date refusal retried as a generic failure",
"old": " stop_merge 10 \"#$pr REFUSED",
"new": " stop_merge 8 \"#$pr REFUSED"
},
{
"name": "partial landing not reported",
"old": " if ((${#LANDED[@]})); then loud_partial \"$why\" \"$*\"; else",
"new": " if false; then loud_partial \"$why\" \"$*\"; else"
},
{
"name": "a normal exit 20 reported as a partial landing",
"old": "FINISHED=1\n",
"new": "true\n"
},
{
"name": "an unchecked fetch after a squash",
"old": " git -C \"$WT\" fetch -q origin \"$MAIN\" ||\n stop_merge 12 \"cannot fetch",
"new": " git -C \"$WT\" fetch -q origin \"$MAIN\" ||\n true || stop_merge 12 \"cannot fetch"
},
{
"name": "PR heads aren't re-checked before the squash",
"old": " [[ $c == success ]] || stop_merge 9 \"#$pr: CI on its head is no longer green",
"new": " true || stop_merge 9 \"#$pr: CI on its head is no longer green"
}
]
}
50 changes: 0 additions & 50 deletions tests/mutants/land-batch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,6 @@
"tests/land-batch.bats"
],
"mutants": [
{
"name": "squash not pinned to the verified head",
"old": "--squash --match-head-commit \"${HEAD[$pr]}\"",
"new": "--squash"
},
{
"name": "parent check skipped",
"old": " [[ $(git -C \"$WT\" rev-parse \"$mc^\" 2>/dev/null) == \"$prev\" ]] ||",
"new": " true ||"
},
{
"name": "tree check skipped",
"old": " [[ $(git -C \"$WT\" rev-parse \"$mc^{tree}\") == \"${TREE[$pr]}\" ]] ||",
"new": " true ||"
},
{
"name": "main not re-checked before each squash",
"old": " main_is \"$prev\" || stop_merge 7",
"new": " true || stop_merge 7"
},
{
"name": "no recheck before merging",
"old": " if [[ -n $PROB || $PR_HEAD != \"${HEAD[$pr]}\" ]]; then",
"new": " if false; then"
},
{
"name": "high-risk PRs batched with the rest",
"old": " if [[ -n $RISK ]]; then\n AREA[$pr]=$RISK; ALONE+=(\"$pr\")",
Expand Down Expand Up @@ -62,26 +37,11 @@
"old": " if [[ -s $WT/.git/lb-seen && -n $(comm -12 \"$WT/.git/lb-seen\" \"$WT/.git/lb-cur\") ]]; then",
"new": " if false; then"
},
{
"name": "up-to-date refusal retried as a generic failure",
"old": " stop_merge 10 \"#$pr REFUSED",
"new": " stop_merge 8 \"#$pr REFUSED"
},
{
"name": "partial landing not reported",
"old": " if ((${#LANDED[@]})); then loud_partial \"$why\" \"$*\"; else",
"new": " if false; then loud_partial \"$why\" \"$*\"; else"
},
{
"name": "unreadable changed files fail open",
"old": " { PROB=\"changed-files-unreadable\"; return; }",
"new": " true"
},
{
"name": "a normal exit 20 reported as a partial landing",
"old": "FINISHED=1\n",
"new": "true\n"
},
{
"name": "renames show only the new path",
"old": "diff --no-renames -z --name-only",
Expand All @@ -92,20 +52,10 @@
"old": "diff --no-renames -z --name-only",
"new": "diff --no-renames --name-only"
},
{
"name": "an unchecked fetch after a squash",
"old": " git -C \"$WT\" fetch -q origin \"$MAIN\" ||\n stop_merge 12 \"cannot fetch",
"new": " git -C \"$WT\" fetch -q origin \"$MAIN\" ||\n true || stop_merge 12 \"cannot fetch"
},
{
"name": "the sweep closes any labelled batch-shaped PR",
"old": "[[ -n $n && $title == \"batch:\"* ]] || continue",
"new": "[[ -n $n ]] || continue"
},
{
"name": "PR heads aren't re-checked before the squash",
"old": " [[ $c == success ]] || stop_merge 9 \"#$pr: CI on its head is no longer green",
"new": " true || stop_merge 9 \"#$pr: CI on its head is no longer green"
}
]
}
75 changes: 0 additions & 75 deletions tests/mutants/landing-lib-ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,11 @@
"tests/land-batch.bats"
],
"mutants": [
{
"name": "merge pause exemption ignored",
"old": "(.mergePauseExempt // []) | map(tostring) | index($pr) != null' \"$CAMPAIGN\") != true",
"new": "(.mergePauseExempt // []) | map(tostring) | index($pr) != null' \"$CAMPAIGN\") != false"
},
{
"name": "stale lane head ignored",
"old": " if [[ -n $lane && ${lane#*$'\\t'} != \"\" && ${lane#*$'\\t'} != \"$PR_HEAD\" ]]; then",
"new": " if false; then"
},
{
"name": "lane head compared by prefix",
"old": "${lane#*$'\\t'} != \"$PR_HEAD\" ]]; then",
"new": "$PR_HEAD != \"${lane#*$'\\t'}\"* ]]; then"
},
{
"name": "corrupt campaign state opens the gates",
"old": "if [[ ! -r $CAMPAIGN ]] || ! jq -e 'type == \"object\"' \"$CAMPAIGN\" >/dev/null 2>&1; then",
"new": "if false; then"
},
{
"name": "merge pause ignored by default",
"old": "HONOR_PAUSE=$(crew_setting land.honor_merge_pause true)",
"new": "HONOR_PAUSE=$(crew_setting land.honor_merge_pause false)"
},
{
"name": "old bash accepted",
"old": "crew_land_bash_ok() {\n (($1 > 4 || ($1 == 4 && $2 >= 4))) ||",
"new": "crew_land_bash_ok() {\n true ||"
},
{
"name": "ci.workflow optional",
"old": " CI_WORKFLOW=$(crew_require ci.workflow) || exit 78",
"new": " CI_WORKFLOW=$(crew_setting ci.workflow .github/workflows/ci.yml)"
},
{
"name": "any event counts by default",
"old": " CI_EVENT=$(crew_setting ci.event pull_request)",
"new": " CI_EVENT=$(crew_setting ci.event \"\")"
},
{
"name": "the workflow isn't passed",
"old": "--required-check \"$CHECK\" --workflow \"$CI_WORKFLOW\" \\",
"new": "--required-check \"$CHECK\" --workflow \".github/workflows/x.yml\" \\"
},
{
"name": "statuses aren't read",
"old": " ! g api \"repos/$R/commits/$1/status?per_page=100\" > \"$d/statuses.json\"; then",
"new": " ! echo '{\"statuses\":[]}' > \"$d/statuses.json\"; then"
},
{
"name": "ignored checks aren't passed",
"old": " --ignored \"$IGNORED\" --min-green",
"new": " --ignored \"\" --min-green"
},
{
"name": "a failed read is green",
"old": "\"$d/statuses.json\"; then\n rm -rf \"$d\"; echo \"0 \"; return 0",
Expand All @@ -72,40 +22,15 @@
"old": " *) echo \"${att:-0} \" ;;",
"new": " *) echo \"${att:-1} success\" ;;"
},
{
"name": "only ci.workflow's jobs are read",
"old": " for id in $(jq -r '.[].id' \"$d/runs.json\"); do",
"new": " for id in $(jq -r --arg wf \"$CI_WORKFLOW\" '.[] | select(.path == $wf) | .id' \"$d/runs.json\"); do"
},
{
"name": "statuses read 30 per page",
"old": "commits/$1/status?per_page=100\"",
"new": "commits/$1/status\""
},
{
"name": "jobs listed without filter=latest",
"old": "actions/runs/$id/jobs?filter=latest&per_page=100\"",
"new": "actions/runs/$id/jobs?per_page=100\""
},
{
"name": "ignored_checks may name the required check",
"old": " [[ $v != \"$CHECK\" ]] || { echo \"$TOOL: ignored_checks names",
"new": " true || { echo \"$TOOL: ignored_checks names"
},
{
"name": "ignored_checks names aren't trimmed",
"old": " v=${v#\"${v%%[![:space:]]*}\"}; v=${v%\"${v##*[![:space:]]}\"}\n",
"new": ""
},
{
"name": "a failed status read is ignored",
"old": " ! g api \"repos/$R/commits/$1/status?per_page=100\" > \"$d/statuses.json\"; then",
"new": " ! { g api \"repos/$R/commits/$1/status?per_page=100\" > \"$d/statuses.json\"; true; }; then"
},
{
"name": "an App author isn't mapped to its [bot] login",
"old": " [[ $author != app/* ]] || author=\"${author#app/}[bot]\"\n",
"new": ""
}
]
}
41 changes: 41 additions & 0 deletions tests/mutants/landing-lib-gates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"target": "plugins/crew-orchestrator/lib/landing.sh",
"suite": [
"bats",
"--abort",
"tests/landing-lib.bats",
"tests/land-batch.bats"
],
"mutants": [
{
"name": "merge pause exemption ignored",
"old": "(.mergePauseExempt // []) | map(tostring) | index($pr) != null' \"$CAMPAIGN\") != true",
"new": "(.mergePauseExempt // []) | map(tostring) | index($pr) != null' \"$CAMPAIGN\") != false"
},
{
"name": "merge pause ignored by default",
"old": "HONOR_PAUSE=$(crew_setting land.honor_merge_pause true)",
"new": "HONOR_PAUSE=$(crew_setting land.honor_merge_pause false)"
},
{
"name": "corrupt campaign state opens the gates",
"old": "if [[ ! -r $CAMPAIGN ]] || ! jq -e 'type == \"object\"' \"$CAMPAIGN\" >/dev/null 2>&1; then",
"new": "if false; then"
},
{
"name": "old bash accepted",
"old": "crew_land_bash_ok() {\n (($1 > 4 || ($1 == 4 && $2 >= 4))) ||",
"new": "crew_land_bash_ok() {\n true ||"
},
{
"name": "stale lane head ignored",
"old": " if [[ -n $lane && ${lane#*$'\\t'} != \"\" && ${lane#*$'\\t'} != \"$PR_HEAD\" ]]; then",
"new": " if false; then"
},
{
"name": "lane head compared by prefix",
"old": "${lane#*$'\\t'} != \"$PR_HEAD\" ]]; then",
"new": "$PR_HEAD != \"${lane#*$'\\t'}\"* ]]; then"
}
]
}
Loading
Loading