@techierathore/techieflow 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.opencode/plugin/techieflow.js +179 -7
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +1 -1
- package/.tfcore/tasks/day1-greenfield.md +1 -1
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/telemetry/SCHEMA.md +4 -0
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -2
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +80 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +136 -6
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +92 -35
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +79 -29
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +15 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +60 -2
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +72 -32
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +171 -32
- package/.tfcore/utils/tf-verify-screens.sh +8 -3
- package/.tfcore/utils/tf-verify-tests.sh +215 -14
- package/.tfcore/utils/tf-verify-verdict.py +48 -9
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +4 -0
- package/docs/TechieFlow-Installation.md +5 -3
- package/package.json +1 -1
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
# tf-verify-tests.sh — run the acceptance tests and map them to rows (Sitting 4c, 2026-09-06).
|
|
3
3
|
#
|
|
4
4
|
# bash .tfcore/utils/tf-verify-tests.sh [--base URL] [--target <sln|csproj>] [--no-browser] [--no-unit]
|
|
5
|
+
# [--shard N/M] [--spec <file or glob>]… [--list <list.json>] [--all-specs]
|
|
5
6
|
# [--json-out tests/.artifacts/verify/tests.json]
|
|
7
|
+
# bash .tfcore/utils/tf-verify-tests.sh --merge <tests-a.json> <tests-b.json>… [--json-out tests/.artifacts/verify/tests.json]
|
|
6
8
|
#
|
|
7
9
|
# Browser tests: `npx playwright test` over tests/verify/ (the JSON reporter). Unit tests:
|
|
8
10
|
# `tf-build.sh test` with normal console verbosity. A test belongs to a row when its name contains
|
|
@@ -11,30 +13,151 @@
|
|
|
11
13
|
# NOT-TESTED when every test carrying its id was SKIPPED, and absent when no test carries its id.
|
|
12
14
|
# A skipped clause is a third outcome, never a failure: `test.skip(!SEEDED, …)` says the state does
|
|
13
15
|
# not exist in the data, which is not a defect (TF-022). Writes tests/.artifacts/verify/tests.json.
|
|
16
|
+
# A suite too long for one command runs in parts (AppManager TF-013): `--shard N/M` runs Playwright's
|
|
17
|
+
# shard N of M (browser only; run the unit tests once with --no-browser), `--spec` names files, and
|
|
18
|
+
# each part writes its own playwright-<part>.json and tests-<part>.json. `--merge` then combines the
|
|
19
|
+
# parts' rows and totals into one tests.json; a later run of the same test stands for it (Lekhak TF-007).
|
|
20
|
+
# When tf-verify-list.sh was given a list of ids, the browser run keeps only the tests carrying them
|
|
21
|
+
# (`--grep`), and is skipped when no row has a screen and no browser test names one (Lekhak TF-017).
|
|
22
|
+
# `--list` names another list.json; `--all-specs` runs every browser test whatever the scope.
|
|
14
23
|
# Exit 0 ran (whatever the results) · 2 nothing could run.
|
|
15
24
|
set -uo pipefail
|
|
16
25
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
-
|
|
26
|
+
source "$HERE/tf-portable.sh" # tf_read_lines, for bash 3.2 on a stock Mac
|
|
27
|
+
BASE=""; TARGET=""; BROWSER=1; UNIT=1; OUT=""; SHARD=""; SPECS=(); MERGE=(); LIST="tests/.artifacts/verify/list.json"; ALLSPECS=0
|
|
18
28
|
while [[ $# -gt 0 ]]; do
|
|
19
29
|
case "$1" in
|
|
20
30
|
--base) BASE="${2:-}"; shift 2 ;;
|
|
21
31
|
--target) TARGET="${2:-}"; shift 2 ;;
|
|
22
32
|
--no-browser) BROWSER=0; shift ;;
|
|
23
33
|
--no-unit) UNIT=0; shift ;;
|
|
34
|
+
--shard) SHARD="${2:-}"; UNIT=0; shift 2 ;;
|
|
35
|
+
--spec) SPECS+=("${2:-}"); shift 2 ;;
|
|
36
|
+
--list) LIST="${2:-}"; shift 2 ;;
|
|
37
|
+
--all-specs) ALLSPECS=1; shift ;;
|
|
38
|
+
--merge) shift; while [[ $# -gt 0 && "$1" != --* ]]; do MERGE+=("$1"); shift; done ;;
|
|
24
39
|
--json-out) OUT="${2:-}"; shift 2 ;;
|
|
25
|
-
-h|--help) sed -n '2,
|
|
40
|
+
-h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
26
41
|
*) echo "tf-verify-tests: unknown argument $1" >&2; exit 2 ;;
|
|
27
42
|
esac
|
|
28
43
|
done
|
|
44
|
+
PART=""
|
|
45
|
+
if [[ -n "$SHARD" ]]; then
|
|
46
|
+
[[ "$SHARD" =~ ^([1-9][0-9]*)/([1-9][0-9]*)$ ]] || { echo "tf-verify-tests: --shard takes N/M, such as 2/4" >&2; exit 2; }
|
|
47
|
+
PART="${BASH_REMATCH[1]}of${BASH_REMATCH[2]}"
|
|
48
|
+
fi
|
|
49
|
+
[[ -n "$OUT" ]] || OUT="tests/.artifacts/verify/tests${PART:+-$PART}.json"
|
|
29
50
|
mkdir -p "$(dirname "$OUT")" tests/.artifacts/verify
|
|
30
|
-
|
|
51
|
+
|
|
52
|
+
# ---- --merge: the parts into one tests.json, no test run ------------------------------------------
|
|
53
|
+
if [[ ${#MERGE[@]} -gt 0 ]]; then
|
|
54
|
+
TF_OUT="$OUT" python3 - "${MERGE[@]}" <<'PY'
|
|
55
|
+
import json, os, sys
|
|
56
|
+
# Lekhak TF-007. The parts are read oldest first (by the time each ran, else in the order given), and
|
|
57
|
+
# a later run of the SAME test stands for it: a test that failed because its service was not started,
|
|
58
|
+
# run again once it was, clears the row. A later skip never replaces a run that happened. A part
|
|
59
|
+
# written before tests carried their own outcome counts each of its tests as the row's result.
|
|
60
|
+
loaded = []
|
|
61
|
+
for i, p in enumerate(sys.argv[1:]):
|
|
62
|
+
try:
|
|
63
|
+
loaded.append((json.load(open(p, encoding="utf-8")), i, p))
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"merge: {p} unreadable ({str(e)[:80]}); skipped")
|
|
66
|
+
loaded.sort(key=lambda x: (x[0].get("ran_at") or "", x[1]) if all(y[0].get("ran_at") for y in loaded) else (x[1],))
|
|
67
|
+
reqs, browser, unit, parts = {}, {"ran": False, "passed": 0, "failed": 0, "skipped": 0, "tests": 0}, {"ran": False, "line": ""}, []
|
|
68
|
+
outs, sources, superseded = {}, {}, 0
|
|
69
|
+
for d, _i, p in loaded:
|
|
70
|
+
parts.append(p)
|
|
71
|
+
for rid, r in d.get("reqs", {}).items():
|
|
72
|
+
oc = r.get("outcomes")
|
|
73
|
+
if oc is None: # an older part: every test it lists carries the row's result
|
|
74
|
+
res = {"PASS": "pass", "FAIL": "fail"}.get(r.get("result"), "skip")
|
|
75
|
+
oc = {t: {"outcome": res, "reason": r.get("reason", "") if res == "fail" else "", "screenshot": r.get("screenshot", "")} for t in r.get("tests", [])}
|
|
76
|
+
oc.update({t: {"outcome": "skip", "reason": r.get("reason", ""), "screenshot": ""} for t in r.get("skipped", [])})
|
|
77
|
+
o = outs.setdefault(rid, {})
|
|
78
|
+
for name, rec in oc.items():
|
|
79
|
+
old = o.get(name)
|
|
80
|
+
if old and rec["outcome"] == "skip" and old["outcome"] != "skip":
|
|
81
|
+
continue
|
|
82
|
+
if old and old["outcome"] != rec["outcome"]:
|
|
83
|
+
superseded += 1
|
|
84
|
+
o[name] = rec
|
|
85
|
+
src = sources.get(rid)
|
|
86
|
+
sources[rid] = r.get("source", "") if src in (None, r.get("source", "")) else "browser+unit"
|
|
87
|
+
b = d.get("browser", {})
|
|
88
|
+
if b.get("ran"):
|
|
89
|
+
browser["ran"] = True
|
|
90
|
+
for k in ("passed", "failed", "skipped", "tests"):
|
|
91
|
+
browser[k] += b.get(k, 0)
|
|
92
|
+
u = d.get("unit", {})
|
|
93
|
+
if u.get("ran") and not unit["ran"]:
|
|
94
|
+
unit = u
|
|
95
|
+
for rid, o in outs.items():
|
|
96
|
+
fails = [(n, x) for n, x in o.items() if x["outcome"] == "fail"]
|
|
97
|
+
npass = sum(1 for x in o.values() if x["outcome"] == "pass")
|
|
98
|
+
skips = [(n, x) for n, x in o.items() if x["outcome"] == "skip"]
|
|
99
|
+
result = "FAIL" if fails else "PASS" if npass else "NOT-TESTED"
|
|
100
|
+
reason = fails[0][1]["reason"] if fails else (skips[0][1]["reason"] if skips and not npass else "")
|
|
101
|
+
reqs[rid] = {"result": result, "source": sources.get(rid, ""), "tests": [n for n, x in o.items() if x["outcome"] != "skip"],
|
|
102
|
+
"skipped": [n for n, _ in skips], "passed": npass, "failed": len(fails), "reason": reason,
|
|
103
|
+
"screenshot": fails[0][1].get("screenshot", "") if fails else "", "outcomes": o}
|
|
104
|
+
json.dump({"reqs": reqs, "browser": browser, "unit": unit, "merged_from": parts, "superseded": superseded},
|
|
105
|
+
open(os.environ["TF_OUT"], "w"), indent=1)
|
|
106
|
+
p = sum(1 for r in reqs.values() if r["result"] == "PASS"); f = sum(1 for r in reqs.values() if r["result"] == "FAIL")
|
|
107
|
+
ns = sum(1 for r in reqs.values() if r["result"] == "NOT-TESTED")
|
|
108
|
+
print(f"merged {len(parts)} part(s): rows with a test: {len(reqs)} — {p} PASS, {f} FAIL, {ns} NOT-TESTED (every clause skipped)"
|
|
109
|
+
+ (f" (browser {browser['passed']}/{browser['tests']} tests passed, {browser['skipped']} skipped)" if browser["ran"] else "")
|
|
110
|
+
+ (f"; {superseded} test(s) ran again in a later part, and the later run stands" if superseded else ""))
|
|
111
|
+
for rid, r in sorted(reqs.items()):
|
|
112
|
+
if r["result"] == "FAIL":
|
|
113
|
+
print(f" FAIL {rid} — {r['reason']}" + (f" — {r['screenshot']}" if r["screenshot"] else ""))
|
|
114
|
+
print(f"JSON: {os.environ['TF_OUT']}")
|
|
115
|
+
PY
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
source "$HERE/tf-lock.sh"; tf_take_lock "verify-tests${PART:+ $PART}" # one browser check at a time (TF-048)
|
|
120
|
+
PWJSON="tests/.artifacts/verify/playwright${PART:+-$PART}.json"; PWLOG="tests/.artifacts/verify/playwright${PART:+-$PART}.log"; UNITLOG=""; UNITLINE=""
|
|
31
121
|
ran_any=0
|
|
32
122
|
|
|
33
123
|
if [[ $BROWSER -eq 1 ]]; then
|
|
34
124
|
if ls tests/verify/*.spec.* >/dev/null 2>&1 || ls tests/verify/**/*.spec.* >/dev/null 2>&1; then
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
125
|
+
# --base reaches the tests only as BASE_URL, which Playwright never reads by itself. When neither
|
|
126
|
+
# the config nor a spec reads it, every test opens the config's own address, where an older build
|
|
127
|
+
# may still be running and pass in the new one's name (TfLens TF-031, 2026-09-11): refuse instead.
|
|
128
|
+
if [[ -n "$BASE" ]] && ! grep -qs "BASE_URL" playwright.config.* tests/verify/*.spec.* tests/verify/**/*.spec.*; then
|
|
129
|
+
echo "browser tests: NOT RUN — nothing reads BASE_URL, so the tests would open another address than --base $BASE; run bash .tfcore/utils/tf-verify-env.sh, which makes playwright.config.ts read it"
|
|
130
|
+
rm -f "$PWJSON"
|
|
131
|
+
elif node -e "require.resolve('@playwright/test')" >/dev/null 2>&1; then
|
|
132
|
+
PWARGS=(--reporter=json); [[ -n "$SHARD" ]] && PWARGS+=("--shard=$SHARD")
|
|
133
|
+
# (the block below ends in SCOPEPY, not PY: tests/regression lifts the PY blocks out verbatim)
|
|
134
|
+
# Lekhak TF-017: a verify of a list of ids runs only the browser tests carrying those ids, and
|
|
135
|
+
# none when no row in scope has a screen and no file under tests/verify/ names one of them. A
|
|
136
|
+
# verify of one no-screen NFR row ran the whole suite and was stopped at 30 minutes. ui,
|
|
137
|
+
# functional and all run everything; so do --spec, --all-specs and a run without list.json.
|
|
138
|
+
SCOPE=""
|
|
139
|
+
[[ $ALLSPECS -eq 0 && ${#SPECS[@]} -eq 0 && -f "$LIST" ]] && SCOPE="$(python3 - "$LIST" <<'SCOPEPY'
|
|
140
|
+
import json, sys
|
|
141
|
+
try:
|
|
142
|
+
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
143
|
+
except Exception:
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
if str(d.get("scope", "")).strip().lower() in ("", "ui", "functional", "all"):
|
|
146
|
+
sys.exit(0)
|
|
147
|
+
rows = d.get("rows", [])
|
|
148
|
+
ids = [r["id"] for r in rows if r.get("id")]
|
|
149
|
+
if ids:
|
|
150
|
+
print(("screen " if any(r.get("screen") for r in rows) else "none ") + "|".join(ids))
|
|
151
|
+
SCOPEPY
|
|
152
|
+
)"
|
|
153
|
+
if [[ "$SCOPE" == none\ * ]] && ! grep -rqsE "${SCOPE#none }" tests/verify; then
|
|
154
|
+
echo "browser tests: skipped — no row in scope (${SCOPE#none }) has a screen and no file under tests/verify/ names one; its tests are unit tests (--all-specs runs every spec anyway)"
|
|
155
|
+
rm -f "$PWJSON"
|
|
156
|
+
else
|
|
157
|
+
[[ -n "$SCOPE" ]] && PWARGS+=(--grep "${SCOPE#* }")
|
|
158
|
+
BASE_URL="$BASE" PLAYWRIGHT_JSON_OUTPUT_NAME="$PWJSON" npx playwright test "${PWARGS[@]}" ${SPECS[@]+"${SPECS[@]}"} > "$PWLOG" 2>&1
|
|
159
|
+
echo "browser tests: ran${SHARD:+ shard $SHARD}${SPECS:+ (${#SPECS[@]} spec argument(s))}${SCOPE:+ only the tests carrying ${SCOPE#* } (the scope in $LIST)} (log $PWLOG)"; ran_any=1
|
|
160
|
+
fi
|
|
38
161
|
else
|
|
39
162
|
echo "browser tests: @playwright/test is not installed here (bash .tfcore/utils/tf-verify-env.sh); skipped"; rm -f "$PWJSON"
|
|
40
163
|
fi
|
|
@@ -44,17 +167,61 @@ if [[ $BROWSER -eq 1 ]]; then
|
|
|
44
167
|
else rm -f "$PWJSON"; fi
|
|
45
168
|
|
|
46
169
|
if [[ $UNIT -eq 1 ]]; then
|
|
47
|
-
|
|
48
|
-
|
|
170
|
+
# Each pattern on its own: `ls *.sln *.slnx` fails when either matches nothing, so a .slnx-only
|
|
171
|
+
# solution read as none; and a test project may sit at any depth under tests/, not one folder down
|
|
172
|
+
# (AppManager TF-007: tests/unit/AppManager.UnitTests/). tf-build.sh finds a root solution itself;
|
|
173
|
+
# without one it is handed the only test project, and several are named rather than guessed between.
|
|
174
|
+
ROOTSLN="$(compgen -G '*.sln'; compgen -G '*.slnx'; compgen -G '*.csproj')"
|
|
175
|
+
tf_read_lines TESTPROJ < <(find tests -name '*.csproj' -not -path 'tests/.artifacts/*' -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' 2>/dev/null | sort)
|
|
176
|
+
[[ -z "$TARGET" && -z "$ROOTSLN" && ${#TESTPROJ[@]} -eq 1 ]] && TARGET="${TESTPROJ[0]}"
|
|
177
|
+
if [[ -z "$TARGET" && -z "$ROOTSLN" && ${#TESTPROJ[@]} -gt 1 ]]; then
|
|
178
|
+
echo "unit tests: NOT RUN — no solution here and ${#TESTPROJ[@]} test projects under tests/ (${TESTPROJ[*]}); name one with --target"
|
|
179
|
+
elif [[ -n "$TARGET" || -n "$ROOTSLN" ]]; then
|
|
180
|
+
# the VERDICT line, not the first line: tf-build.sh may print a note before it, and a note read
|
|
181
|
+
# as the verdict left 975 passing TfLens tests recorded as never run (TF-037)
|
|
182
|
+
# A test project on Microsoft.Testing.Platform ignores --logger and prints no line per test, so no
|
|
183
|
+
# unit test ever reached a row (AppManager TF-005, xunit.v3, 286 tests). Such a project is asked for
|
|
184
|
+
# a TRX report, the per-test record both platforms write, through the property `dotnet test` passes
|
|
185
|
+
# to it. One switch serves every project or none is passed: an unknown switch stops a test app.
|
|
186
|
+
MTP="$(python3 - <<'PY'
|
|
187
|
+
import os, re
|
|
188
|
+
flags, mtp = set(), 0
|
|
189
|
+
for root, dirs, files in os.walk("."):
|
|
190
|
+
dirs[:] = [d for d in dirs if d not in ("bin", "obj", "node_modules", ".git") and not (root == "./tests" and d == ".artifacts")]
|
|
191
|
+
for fn in files:
|
|
192
|
+
if not fn.endswith(".csproj"):
|
|
193
|
+
continue
|
|
194
|
+
t = open(os.path.join(root, fn), encoding="utf-8", errors="replace").read()
|
|
195
|
+
if not re.search(r"<(TestingPlatformDotnetTestSupport|UseMicrosoftTestingPlatformRunner|EnableMSTestRunner|EnableNUnitRunner)>\s*true", t, re.I) and 'Sdk="MSTest.Sdk' not in t:
|
|
196
|
+
continue
|
|
197
|
+
mtp += 1
|
|
198
|
+
if re.search(r'Include="xunit\.v3"', t, re.I):
|
|
199
|
+
flags.add("--report-xunit-trx")
|
|
200
|
+
elif re.search(r'Include="Microsoft\.Testing\.Extensions\.TrxReport"|Sdk="MSTest\.Sdk', t, re.I):
|
|
201
|
+
flags.add("--report-trx")
|
|
202
|
+
else:
|
|
203
|
+
flags.add("?")
|
|
204
|
+
print(next(iter(flags)) if mtp and len(flags) == 1 and "?" not in flags else ("mixed" if mtp else ""))
|
|
205
|
+
PY
|
|
206
|
+
)"
|
|
207
|
+
UARGS=(--logger "console;verbosity=normal")
|
|
208
|
+
case "$MTP" in
|
|
209
|
+
--report-*) UARGS+=("-p:TestingPlatformCommandLineArguments=$MTP") ;;
|
|
210
|
+
mixed) echo "unit tests: note the Testing Platform projects here take different report switches, so none was passed; their tests are not mapped to rows" ;;
|
|
211
|
+
esac
|
|
212
|
+
STAMP="$(mktemp)"
|
|
213
|
+
UNITLINE="$(bash "$HERE/tf-build.sh" test ${TARGET:+"$TARGET"} -- "${UARGS[@]}" 2>&1 | grep -m1 -E '^(PASS|FAIL|NOT-RUN)' || true)"
|
|
49
214
|
UNITLOG="$(sed -n 's/.*log \(tests\/\.artifacts\/build\/[^ ;]*\).*/\1/p' <<<"$UNITLINE" | head -1)"
|
|
215
|
+
TRX="$(find . -name '*.trx' -newer "$STAMP" -not -path '*/node_modules/*' 2>/dev/null)"; rm -f "$STAMP"
|
|
50
216
|
echo "unit tests: $UNITLINE"; [[ "$UNITLINE" != NOT-RUN* ]] && ran_any=1
|
|
51
217
|
else
|
|
52
218
|
echo "unit tests: no solution or test project found; skipped"
|
|
53
219
|
fi
|
|
54
220
|
fi
|
|
55
221
|
|
|
56
|
-
TF_PWJSON="$PWJSON" TF_UNITLOG="$UNITLOG" TF_UNITLINE="$UNITLINE" TF_OUT="$OUT" python3 - <<'PY'
|
|
222
|
+
TF_PWJSON="$PWJSON" TF_UNITLOG="$UNITLOG" TF_UNITLINE="$UNITLINE" TF_TRX="${TRX:-}" TF_OUT="$OUT" python3 - <<'PY'
|
|
57
223
|
import json, os, re
|
|
224
|
+
import xml.etree.ElementTree as ET
|
|
58
225
|
ID = re.compile(r"(REQ-(?:UI|FN|NFR|RAG)-\d{3})", re.I)
|
|
59
226
|
reqs = {}
|
|
60
227
|
browser = {"ran": False, "passed": 0, "failed": 0, "skipped": 0, "tests": 0}
|
|
@@ -72,7 +239,9 @@ def add(rid, outcome, name, source, reason="", shot=""):
|
|
|
72
239
|
defect. tests/regression/run.sh tf_022."""
|
|
73
240
|
r = reqs.setdefault(rid.upper(), {"result": "NOT-TESTED", "source": source, "tests": [],
|
|
74
241
|
"skipped": [], "passed": 0, "failed": 0,
|
|
75
|
-
"reason": "", "screenshot": ""})
|
|
242
|
+
"reason": "", "screenshot": "", "outcomes": {}})
|
|
243
|
+
# each test's own outcome, so a merge can let a later run of the same test stand (Lekhak TF-007)
|
|
244
|
+
r["outcomes"][name] = {"outcome": outcome, "reason": (reason or "")[:200], "screenshot": shot}
|
|
76
245
|
if outcome == "skip":
|
|
77
246
|
r["skipped"].append(name)
|
|
78
247
|
if not r["reason"]:
|
|
@@ -132,25 +301,57 @@ if pw and os.path.isfile(pw):
|
|
|
132
301
|
except Exception as e:
|
|
133
302
|
browser["error"] = str(e)
|
|
134
303
|
|
|
304
|
+
seen = set() # one test read from both a TRX report and the console is counted once
|
|
305
|
+
for trx in [p for p in os.environ.get("TF_TRX", "").splitlines() if p.strip()]:
|
|
306
|
+
try:
|
|
307
|
+
tree = ET.parse(trx).getroot()
|
|
308
|
+
except Exception as e:
|
|
309
|
+
unit.setdefault("trx_unreadable", []).append(f"{trx}: {str(e)[:80]}")
|
|
310
|
+
continue
|
|
311
|
+
unit["ran"] = True
|
|
312
|
+
unit.setdefault("trx", []).append(trx)
|
|
313
|
+
for el in tree.iter():
|
|
314
|
+
if not el.tag.endswith("UnitTestResult"):
|
|
315
|
+
continue
|
|
316
|
+
name = (el.get("testName") or "").strip()
|
|
317
|
+
oc = (el.get("outcome") or "").lower() # Passed / Failed / NotExecuted …, any case (TF-005)
|
|
318
|
+
outcome = "pass" if oc == "passed" else "skip" if oc in ("notexecuted", "inconclusive", "pending", "notrunnable", "disconnected") else "fail"
|
|
319
|
+
if (name, outcome) in seen:
|
|
320
|
+
continue
|
|
321
|
+
seen.add((name, outcome))
|
|
322
|
+
msg = next(((m.text or "").strip() for m in el.iter() if m.tag.endswith("Message") and (m.text or "").strip()), "")
|
|
323
|
+
for rid in set(ID.findall(name)):
|
|
324
|
+
add(rid, outcome, name, "unit",
|
|
325
|
+
("unit test failed: " + (msg.splitlines()[0] if msg else name))[:200] if outcome == "fail"
|
|
326
|
+
else ("unit test skipped: " + name[:120]) if outcome == "skip" else "")
|
|
327
|
+
|
|
135
328
|
ul = os.environ.get("TF_UNITLOG", "")
|
|
136
329
|
if ul and os.path.isfile(ul):
|
|
137
330
|
unit["ran"] = True
|
|
138
331
|
for line in open(ul, encoding="utf-8", errors="replace"):
|
|
139
|
-
|
|
332
|
+
# any case, and a "(133ms)" tail: the Testing Platform's own log writes "failed REQ-NFR-007 … (133ms)" (TF-005)
|
|
333
|
+
m = re.match(r"\s*(passed|failed|skipped)\s+(\S.*?)\s*(?:\[[^\]]*\]|\(\d+(?:\.\d+)?\s*m?s\))?\s*$", line, re.I)
|
|
140
334
|
if not m:
|
|
141
335
|
continue
|
|
142
336
|
# a skipped unit test is recorded as skipped, not dropped: the row then reads
|
|
143
337
|
# NOT-TESTED with the evidence, rather than looking as if no test exists (TF-022)
|
|
144
|
-
outcome = {"
|
|
338
|
+
outcome = {"passed": "pass", "failed": "fail", "skipped": "skip"}[m.group(1).lower()]
|
|
145
339
|
name = m.group(2).strip()
|
|
340
|
+
if (name, outcome) in seen:
|
|
341
|
+
continue
|
|
342
|
+
seen.add((name, outcome))
|
|
343
|
+
# a pass has no reason: every non-failure was once written "unit test skipped" (Lekhak TF-018)
|
|
146
344
|
for rid in set(ID.findall(name)):
|
|
147
345
|
add(rid, outcome, name, "unit",
|
|
148
|
-
|
|
346
|
+
"unit test failed: " + name[:120] if outcome == "fail"
|
|
347
|
+
else "unit test skipped: " + name[:120] if outcome == "skip" else "")
|
|
149
348
|
unit["passed"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "PASS")
|
|
150
349
|
unit["failed"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "FAIL")
|
|
151
350
|
unit["not_tested"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "NOT-TESTED")
|
|
152
351
|
|
|
153
|
-
|
|
352
|
+
import datetime
|
|
353
|
+
ran_at = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
354
|
+
json.dump({"reqs": reqs, "browser": browser, "unit": unit, "ran_at": ran_at}, open(os.environ["TF_OUT"], "w"), indent=1)
|
|
154
355
|
p = sum(1 for r in reqs.values() if r["result"] == "PASS")
|
|
155
356
|
f = sum(1 for r in reqs.values() if r["result"] == "FAIL")
|
|
156
357
|
ns = sum(1 for r in reqs.values() if r["result"] == "NOT-TESTED")
|
|
@@ -22,7 +22,9 @@ Verdicts: PASS · FAIL (acceptance) · BUILD-FAIL · RENDER-FAIL · ASSET-FAIL
|
|
|
22
22
|
PERF-FAIL · NOT-TESTED (no test carried the id, or every test carrying it was skipped; the screen
|
|
23
23
|
checks passed or did not run) ·
|
|
24
24
|
NOT-OBSERVABLE (an NFR row with no budget and no unit test) · NOT-DRIVEN (its screen was never driven:
|
|
25
|
-
the head could not be booted, or has no driver)
|
|
25
|
+
the head could not be booted, or has no driver) · DEFECT-OPEN (every check passed, but the old Remarks
|
|
26
|
+
carry a defect no verify can see, tf_defect.py: the row goes to Needs re-verify, and so to a fix, with
|
|
27
|
+
the defect kept; AppManager TF-021). A row is Verified only on PASS. NOT-* rows keep their
|
|
26
28
|
status and get a Remark saying so; nothing is ever written as a static-only pass.
|
|
27
29
|
|
|
28
30
|
Writes docs/.last-verify.json (the ledger the verify hook reads: date, app, scope, booted, the checks
|
|
@@ -38,6 +40,8 @@ import re
|
|
|
38
40
|
import sys
|
|
39
41
|
|
|
40
42
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
43
|
+
sys.path.insert(0, HERE)
|
|
44
|
+
import tf_defect # noqa: E402
|
|
41
45
|
FAILING = {"BUILD-FAIL": "build", "FAIL": "acceptance", "RENDER-FAIL": "render", "ASSET-FAIL": "assets",
|
|
42
46
|
"VISUAL-FAIL": "visual", "MOCKUP-FAIL": "mockup-parity", "PERF-FAIL": "perf"}
|
|
43
47
|
STATUS = {"PASS": "Verified", "FAIL": "FAIL", "BUILD-FAIL": "FAIL", "RENDER-FAIL": "Needs re-verify", "ASSET-FAIL": "Needs re-verify",
|
|
@@ -71,6 +75,14 @@ def norm_route(r):
|
|
|
71
75
|
return r or "/"
|
|
72
76
|
|
|
73
77
|
|
|
78
|
+
def quoted(s, n=None):
|
|
79
|
+
"""A test's own words — its title or its failure message — in backticks, so the Remark shows them as
|
|
80
|
+
quoted and tf-doc-check reads them as quoted: a title saying "Found/Not found" failed the checker's
|
|
81
|
+
not-present rule on every verify (Lekhak TF-012)."""
|
|
82
|
+
s = re.sub(r"\s+", " ", (s or "").replace("`", "'").replace("|", "/")).strip()
|
|
83
|
+
return f"`{s[:n] if n else s}`" if s else ""
|
|
84
|
+
|
|
85
|
+
|
|
74
86
|
def words(s, n=60):
|
|
75
87
|
w = s.split()
|
|
76
88
|
return s if len(w) <= n else " ".join(w[:n]) + "…" # the ellipsis rides on the last word: the checker counts words, and a bare "…" was a 61st (miss 29)
|
|
@@ -92,6 +104,8 @@ def main(argv):
|
|
|
92
104
|
tests_ran = os.path.isfile(os.path.join(d, "tests.json"))
|
|
93
105
|
screens = load(os.path.join(d, "screens.json")) or {}
|
|
94
106
|
scr_by_name = {s["name"]: s for s in screens.get("screens", [])}
|
|
107
|
+
# screens tf-verify-screens did not drive because their route needs a sample value (Lekhak TF-005)
|
|
108
|
+
scr_skipped = {s["name"]: s for s in screens.get("skipped", []) or []}
|
|
95
109
|
assets = load(os.path.join(d, "assets.json")) or {}
|
|
96
110
|
pages = {norm_route(p.get("path")): p for p in assets.get("pages", [])}
|
|
97
111
|
parity = load(os.path.join(d, "parity.json")) or {}
|
|
@@ -129,7 +143,7 @@ def main(argv):
|
|
|
129
143
|
checks.append(("build", booted or build_failed, build_failed, "BUILD-FAIL", "build-error", boot.get("reason", "build failed"), ""))
|
|
130
144
|
# 2 acceptance
|
|
131
145
|
if t:
|
|
132
|
-
checks.append(("acceptance", True, t["result"] != "PASS", "FAIL", "assert-fail", t.get("reason") or "test failed", t.get("screenshot") or ""))
|
|
146
|
+
checks.append(("acceptance", True, t["result"] != "PASS", "FAIL", "assert-fail", quoted(t.get("reason")) or "test failed", t.get("screenshot") or ""))
|
|
133
147
|
# 3 render
|
|
134
148
|
if scr:
|
|
135
149
|
if scr.get("render") == "UNREACHABLE":
|
|
@@ -192,7 +206,10 @@ def main(argv):
|
|
|
192
206
|
detail = "NFR row with no perf-budget and no unit test carrying its id"
|
|
193
207
|
elif not t and r.get("screen") and not driven:
|
|
194
208
|
verdict = "NOT-DRIVEN"
|
|
209
|
+
sk = scr_skipped.get(r["screen"])
|
|
195
210
|
detail = (f"{boot.get('head', 'the app')} not driven: {boot.get('reason')}" if not booted
|
|
211
|
+
else f"screen {r['screen']} was not driven: its route {sk['route']} needs --route-value "
|
|
212
|
+
+ ", ".join(f"{n}=<id>" for n in sk.get("needs", [])) if sk
|
|
196
213
|
else f"screen {r['screen']} was not driven in this run")
|
|
197
214
|
elif not t:
|
|
198
215
|
verdict = "NOT-TESTED"
|
|
@@ -203,7 +220,7 @@ def main(argv):
|
|
|
203
220
|
detail = f"test passed but {boot.get('head', 'the app')} was not driven: {boot.get('reason')}"
|
|
204
221
|
else:
|
|
205
222
|
verdict = "PASS"
|
|
206
|
-
parts = [f"test {t['tests'][0]
|
|
223
|
+
parts = [f"test {quoted(t['tests'][0], 60)}" if t and t.get("tests") else "test passed"]
|
|
207
224
|
if driven:
|
|
208
225
|
parts.append(f"{r['screen']} renders and looks right @{'/'.join(str(w['width']) for w in scr['widths'])}")
|
|
209
226
|
if "mockup-parity" in ran:
|
|
@@ -212,22 +229,29 @@ def main(argv):
|
|
|
212
229
|
parts.append("within its speed budget")
|
|
213
230
|
detail = "; ".join(parts)
|
|
214
231
|
|
|
215
|
-
|
|
232
|
+
# A defect found by the guide, the owner's testing or production is not in the acceptance line, so
|
|
233
|
+
# a pass cannot clear it and a rewrite must not erase it (TF-021). tests/regression/run.sh am_021.
|
|
234
|
+
kept = tf_defect.unseen(r.get("remarks"))
|
|
235
|
+
if kept and verdict == "PASS":
|
|
236
|
+
verdict = "DEFECT-OPEN"
|
|
237
|
+
status = STATUS.get(verdict, "Needs re-verify" if verdict == "DEFECT-OPEN" else prior)
|
|
216
238
|
pct = r.get("pct")
|
|
217
239
|
if verdict == "PASS":
|
|
218
240
|
pct = 100
|
|
219
|
-
elif verdict in ("RENDER-FAIL", "ASSET-FAIL", "VISUAL-FAIL", "MOCKUP-FAIL", "PERF-FAIL"):
|
|
241
|
+
elif verdict in ("DEFECT-OPEN", "RENDER-FAIL", "ASSET-FAIL", "VISUAL-FAIL", "MOCKUP-FAIL", "PERF-FAIL"):
|
|
220
242
|
pct = min(pct if pct is not None else 75, 75)
|
|
221
243
|
gate = FAILING.get(verdict)
|
|
222
244
|
marks = {"FAIL": "⚠ acceptance", "BUILD-FAIL": "⚠ build", "RENDER-FAIL": "⚠ render", "ASSET-FAIL": "⚠ assets",
|
|
223
245
|
"VISUAL-FAIL": "⚠ visual", "MOCKUP-FAIL": "⚠ mockup-parity", "PERF-FAIL": "⚠ perf",
|
|
224
|
-
"NOT-TESTED": "not verified", "NOT-OBSERVABLE": "not observable", "NOT-DRIVEN": "not verified", "PASS": "PASS"
|
|
246
|
+
"NOT-TESTED": "not verified", "NOT-OBSERVABLE": "not observable", "NOT-DRIVEN": "not verified", "PASS": "PASS",
|
|
247
|
+
"DEFECT-OPEN": "checks pass, defect open"}
|
|
225
248
|
remark = f"{today} verify: {marks[verdict]} — {detail}"
|
|
226
249
|
if evidence:
|
|
227
250
|
remark += f" ({evidence})"
|
|
228
251
|
if notes:
|
|
229
252
|
remark += "; " + "; ".join(notes[:2])
|
|
230
|
-
|
|
253
|
+
carried = words(" ".join(kept).replace("|", "/"), 30) if kept else ""
|
|
254
|
+
remark = words(remark.replace("|", "/"), 60 - len(carried.split())) + (f" {carried}" if carried else "")
|
|
231
255
|
emit = verdict == "PASS" or gate is not None
|
|
232
256
|
row = {"id": rid, "class": r["class"], "verdict": verdict, "gate": gate, "gates_run": ran, "failure_class": cls if gate else None,
|
|
233
257
|
"prior_verdict": prior or None, "status": status, "pct": pct, "remark": remark, "screen": r.get("screen"),
|
|
@@ -240,8 +264,23 @@ def main(argv):
|
|
|
240
264
|
for c in ("build", "acceptance", "render", "assets", "visual", "mockup-parity", "perf"):
|
|
241
265
|
if any(c in x["gates_run"] for x in rows_out):
|
|
242
266
|
checks.append(c)
|
|
267
|
+
# Lekhak TF-007: a targeted verify rewrote the ledger with its own rows only (395 became 1), so every
|
|
268
|
+
# other row lost its last verdict. The ledger is kept row by row: a row this run graded is replaced,
|
|
269
|
+
# with the date and run it was graded in; the rest keep theirs. A scope-all run starts it afresh.
|
|
270
|
+
# The verify guard unlocks a Verified only for a row graded today, and fix-close reads only the rows
|
|
271
|
+
# graded since its fix began, by these dates.
|
|
272
|
+
all_rows, row_dates, row_runs = dict(ledger_rows), {k: today for k in ledger_rows}, {k: started for k in ledger_rows}
|
|
273
|
+
prev = load(os.path.join("docs", ".last-verify.json")) if os.path.isfile(os.path.join("docs", ".last-verify.json")) else None
|
|
274
|
+
if lst.get("scope", "all") != "all" and isinstance(prev, dict) and prev.get("app") in (None, app) \
|
|
275
|
+
and isinstance(prev.get("rows"), dict) and prev.get("mode") != "reconcile":
|
|
276
|
+
for k, v in prev["rows"].items():
|
|
277
|
+
if k not in all_rows:
|
|
278
|
+
all_rows[k] = v
|
|
279
|
+
row_dates[k] = (prev.get("row_dates") or {}).get(k, prev.get("date", ""))
|
|
280
|
+
row_runs[k] = (prev.get("row_runs") or {}).get(k, prev.get("run_id", ""))
|
|
243
281
|
ledger = {"date": today, "app": app, "scope": lst.get("scope", "all"), "booted": boot_text, "gates": checks,
|
|
244
|
-
"evidence": d, "run_id": started, "rows":
|
|
282
|
+
"evidence": d, "run_id": started, "rows": all_rows, "row_dates": row_dates, "row_runs": row_runs,
|
|
283
|
+
"this_run": sorted(ledger_rows)}
|
|
245
284
|
os.makedirs("docs", exist_ok=True)
|
|
246
285
|
with open(os.path.join("docs", ".last-verify.json"), "w", encoding="utf-8") as f:
|
|
247
286
|
json.dump(ledger, f, indent=1)
|
|
@@ -283,7 +322,7 @@ def main(argv):
|
|
|
283
322
|
for t in table:
|
|
284
323
|
print("| " + " | ".join(str(x).replace("|", "/") for x in t) + " |")
|
|
285
324
|
print()
|
|
286
|
-
print(f"Ledger: docs/.last-verify.json ({len(ledger_rows)} rows). Verdicts: {os.path.join(d, 'verdicts.json')}."
|
|
325
|
+
print(f"Ledger: docs/.last-verify.json ({len(ledger_rows)} rows graded now, {len(all_rows)} in all). Verdicts: {os.path.join(d, 'verdicts.json')}."
|
|
287
326
|
+ (f" Checklist: {applied} row(s) rewritten in {lst.get('checklist')}." if apply else " Checklist untouched (no --apply)."))
|
|
288
327
|
return 0
|
|
289
328
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""tf_defect.py — the defect marks the framework's own writers put in a checklist row's Remarks.
|
|
2
|
+
|
|
3
|
+
devguide.md writes `⚠ DevGuide`, tf-checklist-edit's demote `⚠ UAT bug` / `⚠ prod bug` / `⚠ miss`,
|
|
4
|
+
tf-verify-verdict one mark per failing check, the smoke policy `⚠ visual`, security findings
|
|
5
|
+
`⚠ SECURITY`. A bare ⚠ is not a defect: hand-written remarks use it for "not verifiable here".
|
|
6
|
+
|
|
7
|
+
Imported by tf-status-facts.py (TF-019), tf-build-list.py (TF-020) and tf-verify-verdict.py (TF-021).
|
|
8
|
+
tests/regression/run.sh am_019, am_020, am_021.
|
|
9
|
+
"""
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
# found by a verify check: a new verify grades it again, so its verdict replaces the old mark
|
|
13
|
+
VERIFY = r"acceptance|build|render|assets|visual|mockup-parity|perf"
|
|
14
|
+
# found by another step (guide, owner's testing, production, a miss, a review): no verify sees it
|
|
15
|
+
OTHER = r"DevGuide|UAT bug|prod bug|miss \d{4}-|SECURITY"
|
|
16
|
+
DEFECT = re.compile(rf"⚠\s*(?:{OTHER}|{VERIFY})\b")
|
|
17
|
+
UNSEEN = re.compile(rf"⚠\s*(?:{OTHER})\b")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def clauses(remarks, pattern=DEFECT):
|
|
21
|
+
"""The defect clauses of a Remarks cell: each runs from its ⚠ mark to the next ⚠ or the end."""
|
|
22
|
+
return [c.strip().rstrip(";").strip() for c in re.split(r"(?=⚠)", remarks or "") if pattern.match(c)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def unseen(remarks):
|
|
26
|
+
"""The clauses a verify cannot see, so a verify must neither erase them nor pass over them."""
|
|
27
|
+
return clauses(remarks, UNSEEN)
|