qa-agent-kit 0.6.0__py3-none-any.whl
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.
- qa_agent_kit-0.6.0.dist-info/METADATA +157 -0
- qa_agent_kit-0.6.0.dist-info/RECORD +32 -0
- qa_agent_kit-0.6.0.dist-info/WHEEL +4 -0
- qa_agent_kit-0.6.0.dist-info/entry_points.txt +6 -0
- qa_agent_kit-0.6.0.dist-info/licenses/LICENSE +21 -0
- qakit/AGENTS.md +48 -0
- qakit/__init__.py +21 -0
- qakit/_cli.py +46 -0
- qakit/docs/README.md +14 -0
- qakit/docs/agents.md +40 -0
- qakit/docs/case-studies.md +146 -0
- qakit/docs/device-qa-protocol.md +426 -0
- qakit/docs/getting-started.md +100 -0
- qakit/docs/ko/device-qa-protocol.md +433 -0
- qakit/docs/manifest.md +96 -0
- qakit/docs/mutation-testing.md +69 -0
- qakit/docs/prose-qa.md +49 -0
- qakit/docs/pypi.md +40 -0
- qakit/docs/report.md +64 -0
- qakit/skills/README.md +51 -0
- qakit/skills/device-qa/SKILL.md +87 -0
- qakit/skills/prose-qa/SKILL.md +49 -0
- qakit/skills/qa-rigor/SKILL.md +105 -0
- qakit/tools/device_lock.py +151 -0
- qakit/tools/device_qa.py +838 -0
- qakit/tools/install_skills.py +120 -0
- qakit/tools/mutate.py +684 -0
- qakit/tools/prose_check.py +291 -0
- qakit/tools/recording_frames.swift +78 -0
- qakit/tools/release_checks.py +287 -0
- qakit/tools/screenshot_contrast.swift +42 -0
- qakit/tools/shots_gif.py +81 -0
qakit/docs/manifest.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# The `device_qa.json` manifest
|
|
2
|
+
|
|
3
|
+
One file per app tells `tools/device_qa.py` and `tools/release_checks.py` what to build, which tests form which suite, how
|
|
4
|
+
to change the phone for each variant and where the store material lives. Paths are relative to the manifest's folder.
|
|
5
|
+
The conventional location is `<app>/release/testing/device_qa.json`, which is what `--app <slug>` resolves under
|
|
6
|
+
`--apps-root` (default `$QAKIT_APPS_ROOT`, else `<git top>/apps`). `--manifest <path>` works anywhere.
|
|
7
|
+
|
|
8
|
+
`examples/device_qa.json` is a complete example. `validate_manifest` in `tools/device_qa.py` rejects a manifest with a
|
|
9
|
+
message per mistake; `tests/test_device_qa.py` pins those messages.
|
|
10
|
+
|
|
11
|
+
## Top level
|
|
12
|
+
|
|
13
|
+
| Key | Required | Meaning |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `schema_version` | yes | `"1"` |
|
|
16
|
+
| `app` | yes | Display name; names the artifact folder and the contact sheet |
|
|
17
|
+
| `project` or `workspace` | one of them | `.xcodeproj` or `.xcworkspace` passed to xcodebuild |
|
|
18
|
+
| `scheme` | yes | Scheme that builds the app and its test targets |
|
|
19
|
+
| `process_name` | yes | The app's process name on the phone, used to pick crash and jetsam reports |
|
|
20
|
+
| `extension_processes` | no | Extension process names (widget, notification service) whose crashes also count |
|
|
21
|
+
| `suites` | yes | Name to list of `-only-testing` identifiers (`Target`, `Target/Class` or `Target/Class/test`) |
|
|
22
|
+
| `variants` | no | Name to variant (below). `default` is applied when no `--variant` is given |
|
|
23
|
+
| `bundle_id` | for perf, upgrade, release, traits | The app's bundle identifier (else read from `appstore_config`) |
|
|
24
|
+
| `team_id` | for release | The Apple team id (else read from `appstore_config`) |
|
|
25
|
+
| `signing` | no | `key_path`, `key_id`, `issuer_id` of an App Store Connect API key (below) |
|
|
26
|
+
| `appstore_config` | no | Path to a JSON whose `api` block holds `private_key_path`, `key_id`, `issuer_id` (an alternative to `signing`) |
|
|
27
|
+
| `derived_data` | no | DerivedData folder (default `<project folder>/build/DerivedData-device`); the run's build output is excluded from the source fingerprint either way |
|
|
28
|
+
| `fingerprint_root` | no | Folder whose edits during a run mark the round as mixed (default the project folder); set it when the app's core sources live beside the project folder |
|
|
29
|
+
| `xcodebuild_args` | no | Extra xcodebuild arguments, for example `["CODE_SIGN_STYLE=Automatic"]` |
|
|
30
|
+
| `traits` | for `traits` | `screens`: screen name to the launch arguments that open it (`{"map": ["-start", "map"]}`); default one launch with no arguments |
|
|
31
|
+
| `upgrade` | for `upgrade` | `seed_file` (a black-box UI test copied into the previous release's UI-test folder), `uitests_dir`, `seed` and `check` test identifiers, optional `copy` (gitignored files the old build needs) and `generator` (a project generator script to run in the old worktree) |
|
|
32
|
+
| `release` | for release checks | Store material (below); `version` overrides the store config's `app_store_version` for the What's New check |
|
|
33
|
+
|
|
34
|
+
## Suites
|
|
35
|
+
|
|
36
|
+
A suite is what one `test --suite <name>` run executes. Common names:
|
|
37
|
+
|
|
38
|
+
- `smoke`: five or fewer tests that must be green within five minutes before anything else runs.
|
|
39
|
+
- `unit`, `ui`: the whole test targets.
|
|
40
|
+
- `qa`: the walkthrough class (`<App>QAWalkthroughTests`) that screenshots, audits and sweeps every screen.
|
|
41
|
+
- `qa-screens`: the same walkthrough split per screen, so a failing screen can be rerun alone.
|
|
42
|
+
|
|
43
|
+
## Variants
|
|
44
|
+
|
|
45
|
+
A variant changes the whole phone, so system alerts and other apps render the same way. The runner applies it before the
|
|
46
|
+
suite and restores iOS defaults afterwards, even when the suite fails.
|
|
47
|
+
|
|
48
|
+
| Key | Values | devicectl call |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `appearance` | `light`, `dark` | `device settings appearance --mode` |
|
|
51
|
+
| `text_size` | `extra-small` ... `extra-extra-extra-large`, `accessibility-medium` ... `accessibility-extra-extra-extra-large` | `device settings appearance --text-size` (accessibility sizes turn Larger Accessibility Sizes on) |
|
|
52
|
+
| `location` | `[latitude, longitude]` | `device simulate location coordinate` |
|
|
53
|
+
| `voiceover` | `true` | `device settings voiceover --enable`. The first time VoiceOver goes on, iOS 27.2 shows a welcome prompt over the app (Start VoiceOver Tutorial / Use VoiceOver / Cancel); call `QA.settleAfterLaunch(app)` right after `app.launch()` so the walkthrough's first tap is not swallowed |
|
|
54
|
+
| `env` | object | Forwarded to the UI-test runner as `TEST_RUNNER_<KEY>`; QAKit reads `QA_SWEEP`, `QA_LANG`, `QA_ALLOW_LABELS`, `QA_DISMISS_LABELS`, `QA_ALERT_HOSTS`, `QA_LOCALES` |
|
|
55
|
+
|
|
56
|
+
The variant name itself reaches the tests as `QA_VARIANT` and prefixes every screenshot name.
|
|
57
|
+
|
|
58
|
+
Typical set: `dark` and `large-text` and `small-text` and `english` with `QA_SWEEP=0` (layout variants: every screen
|
|
59
|
+
seen, no second sweep), one or two `location` variants for a location-aware app, and `voiceover` for a major release.
|
|
60
|
+
|
|
61
|
+
## Signing
|
|
62
|
+
|
|
63
|
+
Running on a real device needs development signing. With an App Store Connect API key xcodebuild registers the phone and
|
|
64
|
+
mints profiles without an Xcode login (`-allowProvisioningUpdates -allowProvisioningDeviceRegistration` plus the
|
|
65
|
+
`-authenticationKey*` flags). `key_path` may be absolute, relative to the git top level that holds the manifest, or
|
|
66
|
+
relative to `$QAKIT_SECRETS_ROOT` (so a worktree can reach the key kept in the main checkout). Without a key the runner
|
|
67
|
+
still passes the provisioning flags and relies on the Mac's Xcode account.
|
|
68
|
+
|
|
69
|
+
## Projects that pin App Store profiles
|
|
70
|
+
|
|
71
|
+
A project with `CODE_SIGN_STYLE = Manual` and `PROVISIONING_PROFILE_SPECIFIER` set (the usual shape after a release
|
|
72
|
+
script took over signing) cannot be signed for the test phone with the development certificate the key mints. `doctor`
|
|
73
|
+
reports it. Put the overrides in the manifest and every device command applies them:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
"xcodebuild_args": ["CODE_SIGN_STYLE=Automatic", "PROVISIONING_PROFILE_SPECIFIER=", "CODE_SIGN_IDENTITY=Apple Development", "DEVELOPMENT_TEAM=ABCDE12345"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The archive you upload keeps the project's own settings; the overrides only reach the `test`, `traits`, `upgrade` and `perf`
|
|
80
|
+
builds.
|
|
81
|
+
|
|
82
|
+
## Release block
|
|
83
|
+
|
|
84
|
+
Read by `tools/release_checks.py`.
|
|
85
|
+
|
|
86
|
+
| Key | Meaning |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `info_plist` | The app's Info.plist (purpose strings, `CFBundleLocalizations`) |
|
|
89
|
+
| `resources_dir` | Folder that holds `<lang>.lproj/InfoPlist.strings` or `InfoPlist.xcstrings` |
|
|
90
|
+
| `source_dirs` | Swift source folders scanned for protected APIs (camera, location, ...) |
|
|
91
|
+
| `bundles` | One entry per code-signed bundle: `name`, `sources`, `privacy` (its `PrivacyInfo.xcprivacy`, or `null`) |
|
|
92
|
+
| `metadata_dir` | Folder of `<locale>.md` store metadata files with `## Name`, `## Subtitle`, `## Description`, `## Keywords`, `## What's New`, `## Promotional Text` sections |
|
|
93
|
+
| `review_notes` | The review notes markdown (4,000 character limit) |
|
|
94
|
+
| `app_id` | `TEAMID.bundle.id` as it must appear in the AASA file |
|
|
95
|
+
| `aasa_domain`, `aasa_paths` | Universal link domain and the paths the AASA must list (checked on Apple's CDN) |
|
|
96
|
+
| `link_ignore` | URL prefixes not fetched (API bases that reject GET) |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Mutation testing with `tools/mutate.py`
|
|
2
|
+
|
|
3
|
+
`mutate.py` is a single-file mutation testing engine for any repo with a test command. It copies the project root to a
|
|
4
|
+
temporary folder, applies one mutant at a time, runs your build and test commands there and reports which mutants the
|
|
5
|
+
tests killed, which survived, which did not compile and which sit on lines no test runs.
|
|
6
|
+
|
|
7
|
+
## Languages
|
|
8
|
+
|
|
9
|
+
- **Python**: tokenize and ast based. Comparison and arithmetic operators, boolean literals, `not` removal, call
|
|
10
|
+
statements (`store.save(x)` dropped when the call is a whole statement).
|
|
11
|
+
- **C-family syntax** (`.swift .js .jsx .ts .tsx .mjs .kt .go .java .cs`): regex on binary operators that have spaces on
|
|
12
|
+
both sides, which keeps `Array<Int>`, `->`, `0..<n`, prefix `!` and `-x` intact. Strings, comments and `#if` lines are
|
|
13
|
+
skipped.
|
|
14
|
+
|
|
15
|
+
## Config keys
|
|
16
|
+
|
|
17
|
+
| Key | Meaning |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `name` | Report name |
|
|
20
|
+
| `root` | Folder copied for each mutant (relative to the git top level that holds the config) |
|
|
21
|
+
| `sources` | Globs of files to mutate, relative to root |
|
|
22
|
+
| `exclude` | Globs left alone (views, generated code) |
|
|
23
|
+
| `copy_exclude` | Extra folders not copied (build output); `.git`, `.build`, `DerivedData`, `.venv`, `xcuserdata` and the like are always skipped |
|
|
24
|
+
| `setup` | Shell command run once per copy before `build` |
|
|
25
|
+
| `build` | Compile step; failure means the mutant is invalid (not counted) |
|
|
26
|
+
| `test` | Test step; failure or timeout means killed. `{orig}` real root, `{file}`/`{stem}` the mutated file, `{lcov}` coverage path |
|
|
27
|
+
| `coverage` | Command that writes lcov to `{lcov}`; mutants on unexecuted lines are reported uncovered and not run |
|
|
28
|
+
| `env` | Extra environment for the commands |
|
|
29
|
+
| `jobs` | Parallel copies (default 3); the engine runs under `nice -n 10` |
|
|
30
|
+
| `timeout_factor` | Per-mutant timeout as a multiple of the baseline test time (default 3) |
|
|
31
|
+
| `test_timeout` | Fixed per-mutant timeout in seconds when the test command differs from the baseline one |
|
|
32
|
+
| `operators` | Subset of `boundary equality logical arithmetic boolean negation drop-call` |
|
|
33
|
+
| `sweep_sample` | Seeded sample size for a full sweep (Xcode projects: one to two minutes per mutant) |
|
|
34
|
+
|
|
35
|
+
## Command line
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
--config <json> config file (or --root/--source/--build/--test inline)
|
|
39
|
+
--changed-since <ref> only lines added or changed since a git ref; 'latest-tag:<glob>' picks the newest matching tag
|
|
40
|
+
--file <path[:line]> limit to files or one line (repeatable)
|
|
41
|
+
--operator <name> limit operators (repeatable)
|
|
42
|
+
--max-mutants N --seed S seeded sample
|
|
43
|
+
--jobs N parallel copies
|
|
44
|
+
--max-survivors N exit 0 up to N survivors (for a gate that is tightening over time)
|
|
45
|
+
--out <dir> report folder (default $TMPDIR/mutation-reports/<name>-<time>)
|
|
46
|
+
--list print the mutants and stop
|
|
47
|
+
--keep keep the scratch copies
|
|
48
|
+
--resume reuse verdicts already in --out/results.jsonl
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Exit codes: 0 no survivors, 1 survivors (or uncovered), 2 setup or baseline failure. A failing baseline names the failing
|
|
52
|
+
test.
|
|
53
|
+
|
|
54
|
+
## Reports
|
|
55
|
+
|
|
56
|
+
- `results.jsonl`: one verdict per mutant (file, line, operator, before, after, status, seconds, output tail).
|
|
57
|
+
- `mutation.md`: the human and agent readable report, survivors first, with the line to open.
|
|
58
|
+
- `mutation.json`: counts per status and per operator, the score (`killed / (killed + survived + uncovered)`).
|
|
59
|
+
|
|
60
|
+
Without `coverage`, a line no test reaches is reported as survived, not uncovered; a project whose tests skip in a fresh
|
|
61
|
+
clone (locale data, network fixtures) looks weaker than it is. Give the engine a coverage command when the test suite can
|
|
62
|
+
produce lcov.
|
|
63
|
+
|
|
64
|
+
## Reading the score
|
|
65
|
+
|
|
66
|
+
The score is not a target to maximise by excluding files. It is a map: each survivor is one behaviour nobody checks. A
|
|
67
|
+
boundary survivor on a pricing or grading line is a bug waiting; a survivor in a log message is noise and takes a
|
|
68
|
+
`mutation-ok` comment. The gate that works in practice is "every mutant on a changed line is dead or explained", run
|
|
69
|
+
before merge (`--changed-since origin/main`) and before release (`--changed-since 'latest-tag:myapp/*'`).
|
qakit/docs/prose-qa.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Prose QA
|
|
2
|
+
|
|
3
|
+
`skills/prose-qa/SKILL.md` is the procedure. `tools/prose_check.py` is the mechanical half: seven deterministic gates
|
|
4
|
+
over a Markdown file, and a claims ledger skeleton for the verification step.
|
|
5
|
+
|
|
6
|
+
## Gates
|
|
7
|
+
|
|
8
|
+
| Gate | Finds | Severity |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `outline` | heading level jumps (H2 to H4), headings with no body, a document with exactly one H2 | WARN |
|
|
11
|
+
| `sentences` | sentences over `--max-words` (35; DEFECT past 1.5 times that), paragraphs over `--max-sentences` (7) | WARN / DEFECT |
|
|
12
|
+
| `duplicates` | the same sentence twice (six words or more, punctuation ignored) | DEFECT |
|
|
13
|
+
| `claims` | a number, percentage, year, currency or evidence word with no source marker in that sentence or its neighbours | WARN |
|
|
14
|
+
| `acronyms` | an acronym of three to six capitals used before `Full Name (ACR)` appears; common ones (API, JSON, PDF ...) skipped | WARN |
|
|
15
|
+
| `links` | empty link targets, bare URLs in prose, the same link text pointing at two targets | DEFECT / WARN |
|
|
16
|
+
| `tells` | em dashes, "In conclusion", "Firstly", "It is worth noting", "delve", three sentences in a row with the same first word | WARN |
|
|
17
|
+
|
|
18
|
+
Source markers the `claims` gate accepts: `[1]`, `[^note]`, a Markdown link or URL, `(2024)`, `et al.`, `source:`,
|
|
19
|
+
`according to`, `p. 12`, `Table 3`, `Figure 2`.
|
|
20
|
+
|
|
21
|
+
## The claims ledger
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
line,claim,source_marker_nearby,source,quote,verdict
|
|
25
|
+
12,"Revenue grew 42% in 2025.",no,,,
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The reviewer fills `source` (URL, file, page), `quote` (the exact words that support the claim) and `verdict`:
|
|
29
|
+
|
|
30
|
+
| Verdict | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| SUPPORTED | the quote says what the sentence says, numbers digit for digit |
|
|
33
|
+
| PARTIAL | the quote says less; note what the sentence adds beyond it |
|
|
34
|
+
| NOT IN SOURCE | the source exists but does not say this |
|
|
35
|
+
| NO SOURCE | nothing found; the claim gets a source or leaves the document |
|
|
36
|
+
|
|
37
|
+
A document passes the claims step when every row is SUPPORTED or PARTIAL with the addition justified in the text.
|
|
38
|
+
|
|
39
|
+
## Running it
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python3 tools/prose_check.py memo.md # findings, defects first; exit 1 on any DEFECT
|
|
43
|
+
python3 tools/prose_check.py memo.md --gate claims --gate links
|
|
44
|
+
python3 tools/prose_check.py paper.md --claims claims.csv --max-words 40
|
|
45
|
+
python3 tools/prose_check.py memo.md --json | jq '.[] | select(.severity=="DEFECT")'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Code blocks, front matter, tables and HTML comments are skipped. The sentence splitter understands `.`, `!`, `?` and the
|
|
49
|
+
CJK full stop, so Korean and Japanese documents get the structural gates; the lexical tells are English.
|
qakit/docs/pypi.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# QAKit on PyPI
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
pip install qa-agent-kit # the Python tools (the import name and the commands stay `qakit`); the Swift package stays on SwiftPM (Package.swift)
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## The five commands
|
|
8
|
+
|
|
9
|
+
| Command | Same as, from a checkout |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `qakit-mutate` | `python3 tools/mutate.py` |
|
|
12
|
+
| `qakit-prose` | `python3 tools/prose_check.py` |
|
|
13
|
+
| `qakit-device` | `python3 tools/device_qa.py` |
|
|
14
|
+
| `qakit-release-checks` | `python3 tools/release_checks.py` |
|
|
15
|
+
| `qakit-install-skills` | `python3 tools/install_skills.py` |
|
|
16
|
+
|
|
17
|
+
Flags are identical. Each command is a shim in `src/qakit/_cli.py`. The wheel copies `tools/` to `qakit/tools/`; the
|
|
18
|
+
shim puts that folder first on `sys.path` and calls the tool's `main()`, so the sibling imports (`device_qa` needs
|
|
19
|
+
`device_lock`) resolve as they do for a plain script. The scripts are unchanged and still run from a checkout.
|
|
20
|
+
|
|
21
|
+
## Where the skills are after a pip install
|
|
22
|
+
|
|
23
|
+
The wheel also copies `skills/`, `docs/` and `AGENTS.md` under the package, so `qakit-install-skills` links
|
|
24
|
+
`.claude/skills/<skill>` (or the Codex, Cursor or Gemini equivalent) to `<site-packages>/qakit/skills/<skill>`. Find the
|
|
25
|
+
folder with `python3 -c "import qakit; print(qakit.SKILLS)"`. Run `qakit-install-skills --target all --uninstall` before `pip uninstall`.
|
|
26
|
+
|
|
27
|
+
## Releasing
|
|
28
|
+
|
|
29
|
+
The version is read at build time from the newest `## [x.y.z]` heading in `CHANGELOG.md`, so a release starts with the
|
|
30
|
+
changelog step in `RELEASING.md`, then:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python3 -m venv /tmp/qakit-release && /tmp/qakit-release/bin/pip install build twine
|
|
34
|
+
rm -rf dist && /tmp/qakit-release/bin/python -m build # dist/qakit-<version>.tar.gz and the wheel
|
|
35
|
+
/tmp/qakit-release/bin/twine check dist/*
|
|
36
|
+
/tmp/qakit-release/bin/twine upload dist/* # token in ~/.pypirc, or TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Before `twine upload`, install the wheel into a clean venv and run `qakit-mutate --help` outside the checkout.
|
|
40
|
+
The sdist excludes `Sources/`, `Package.swift`, `.github/` and `assets/`; the wheel excludes the tests.
|
qakit/docs/report.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# What a run leaves behind
|
|
2
|
+
|
|
3
|
+
`tools/device_qa.py test` writes everything under one folder: `--out`, else `$QAKIT_ARTIFACTS/<app>/<stamp>`, else
|
|
4
|
+
`/tmp/qakit/<app>/<stamp>`. The folder is the evidence an agent reviews and the evidence a verdict cites.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
<out>/
|
|
8
|
+
summary.json the run's verdict and pointers (schema below)
|
|
9
|
+
<variant>-<suite>.log full xcodebuild output
|
|
10
|
+
<variant>-<suite>.xcresult the result bundle (open in Xcode, or export again with `shots`)
|
|
11
|
+
<variant>-<suite>-shots/
|
|
12
|
+
index.md contact sheet: one section per test, every attachment in order, failures marked
|
|
13
|
+
manifest.json xcresulttool's export manifest
|
|
14
|
+
*.png, *.txt screenshots (`QA.snap`) and notes (`QA.note`, accessibility audit reports)
|
|
15
|
+
crashes/ .ips crash and jetsam reports from the phone that name the app or its extensions
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## summary.json (`qakit.device-qa.summary.v1`)
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"schema": "qakit.device-qa.summary.v1",
|
|
23
|
+
"app": "MyApp",
|
|
24
|
+
"device": {"udid": "...", "name": "iPhone", "transport": "wired"},
|
|
25
|
+
"started": "2026-10-28T09:00:00+00:00",
|
|
26
|
+
"runs": [
|
|
27
|
+
{"variant": "dark", "suite": "qa", "executed": 9, "skipped": 0, "failures": 1,
|
|
28
|
+
"failed_cases": ["MyAppQAWalkthroughTests testWalkSettings"], "succeeded": false,
|
|
29
|
+
"screenshots": 143, "log": ".../dark-qa.log", "xcresult": ".../dark-qa.xcresult"}
|
|
30
|
+
],
|
|
31
|
+
"crashes": [".../crashes/MyApp-2026-10-28-090501.ips"],
|
|
32
|
+
"sources_changed_during_run": false,
|
|
33
|
+
"succeeded": false
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`succeeded` is true only when every run succeeded, no crash report named the app, and no source under the app changed while
|
|
38
|
+
the run was going (a round that compiled a mix of two trees describes neither). The exit code follows it.
|
|
39
|
+
|
|
40
|
+
## Other commands
|
|
41
|
+
|
|
42
|
+
| Command | Leaves |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `perf` | `perf-<stamp>/launch.trace` (open in Instruments) and `perf.json` with the potential-hangs and hang-risks rows |
|
|
45
|
+
| `traits` | `traits-<stamp>/<screen>.png` after the changes and `crashes/` |
|
|
46
|
+
| `release` | `release-<stamp>/export/*.ipa`, `launch-<n>.png` per cold launch and `crashes/` |
|
|
47
|
+
| `upgrade` | `upgrade-<stamp>/old-seed.xcresult`, `new-check.xcresult`, their logs and shot folders |
|
|
48
|
+
|
|
49
|
+
## Screenshot names
|
|
50
|
+
|
|
51
|
+
`QA.snap("<step>")` stores `"<variant> <step>"`. The walkthrough convention is `<screen> › <control>` for a sweep tap and
|
|
52
|
+
`a11y <screen>` for an accessibility report, so a reviewer can group the contact sheet by screen with a plain text search.
|
|
53
|
+
|
|
54
|
+
## Reviewing a contact sheet
|
|
55
|
+
|
|
56
|
+
Read `index.md` top to bottom, one screen at a time: clipping, overlap, empty states, wrong facts, translation, contrast,
|
|
57
|
+
touch targets, attribution. For every suspected defect try to refute it (is it the variant? a test fixture? a real
|
|
58
|
+
bug?) before putting it on the defect list. Accessibility notes (`a11y ...` text attachments) go on the same list with
|
|
59
|
+
the issue type Apple reported.
|
|
60
|
+
|
|
61
|
+
## Static checks
|
|
62
|
+
|
|
63
|
+
`tools/release_checks.py` prints one line per check (`PASS`, `WARN`, `FAIL`, the check ID from the protocol and the
|
|
64
|
+
evidence) and `--json` gives the same as a list. Any FAIL exits 1.
|
qakit/skills/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Agent skills
|
|
2
|
+
|
|
3
|
+
Three skills, written for coding agents that read `SKILL.md` files (Claude Code, Codex, and any tool that follows the
|
|
4
|
+
same convention). They are the operating procedure; the tools under `tools/` and the Swift package do the work.
|
|
5
|
+
|
|
6
|
+
| Skill | Scope |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `qa-rigor` | Any code: test design, mutation testing, runtime checkers, real-environment check, verdict ledger |
|
|
9
|
+
| `device-qa` | Apple apps on a USB-attached test iPhone: walkthrough, variants, screenshots, static submission checks, release verdict |
|
|
10
|
+
| `prose-qa` | Memos, READMEs, design docs, papers: deterministic gates, claims ledger, argument check, cold read |
|
|
11
|
+
|
|
12
|
+
## Install with the installer
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python3 tools/install_skills.py --target claude --scope project # this project's .claude/skills
|
|
16
|
+
python3 tools/install_skills.py --target all --scope user # ~/.claude/skills, ~/.agents/skills, ~/.cursor/rules, ~/.gemini
|
|
17
|
+
python3 tools/install_skills.py --target codex --scope project --project /path/to/repo
|
|
18
|
+
python3 tools/install_skills.py --target all --uninstall
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Target | What it gets |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `claude` | `.claude/skills/<skill>` symlink to `skills/<skill>`; Claude Code lists the skill and loads `SKILL.md` on use |
|
|
24
|
+
| `codex` | `.agents/skills/<skill>` symlink, and a pointer line in the project's `AGENTS.md` |
|
|
25
|
+
| `cursor` | `.cursor/rules/qakit-<skill>.mdc`, a rule that tells Cursor to read the `SKILL.md` |
|
|
26
|
+
| `gemini` | A pointer line in `GEMINI.md` (project) or `~/.gemini/GEMINI.md` (user) |
|
|
27
|
+
|
|
28
|
+
Existing files that are not QAKit links or rules are left alone and reported as `skip`. The installer is idempotent.
|
|
29
|
+
|
|
30
|
+
## Install by hand
|
|
31
|
+
|
|
32
|
+
Project-wide (checked into your repo):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git submodule add https://github.com/younatics/QAKit.git vendor/qakit
|
|
36
|
+
mkdir -p .claude/skills
|
|
37
|
+
ln -s ../../vendor/qakit/skills/qa-rigor .claude/skills/qa-rigor
|
|
38
|
+
ln -s ../../vendor/qakit/skills/device-qa .claude/skills/device-qa
|
|
39
|
+
ln -s ../../vendor/qakit/skills/prose-qa .claude/skills/prose-qa
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
User-wide:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/younatics/QAKit.git ~/qakit
|
|
46
|
+
for s in qa-rigor device-qa prose-qa; do ln -s ~/qakit/skills/$s ~/.claude/skills/$s; done
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Any other tool: paste or link the kit's `AGENTS.md`, which names the skill to load for each kind of job. Relative paths
|
|
50
|
+
inside the skills (`tools/...`) are relative to the QAKit checkout; say where the checkout is in your project's
|
|
51
|
+
instructions file so the agent can find `tools/device_qa.py`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: device-qa
|
|
3
|
+
description: QA an Apple app like a QA engineer on a USB-attached test iPhone. Walk every screen and tap every button, review screenshots, audit accessibility, run dark, large-text, language and location variants, run the static submission checks, fix and loop, and write the release verdict. Use on "QA this app", "tap through every screen", "be thorough before release", "test on a real device", and as the gate before App Review. Argument: the app slug or manifest path.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# device-qa: real-device QA for a solo developer whose QA team is an agent
|
|
7
|
+
|
|
8
|
+
The procedure and the check list live in `docs/device-qa-protocol.md` (24 areas, each check with an ID, a pass
|
|
9
|
+
criterion, a method and a source). This skill is the order in which to run the tools. "Tests green" is not "ready to
|
|
10
|
+
ship". The release verdict comes from the protocol's exit criteria.
|
|
11
|
+
|
|
12
|
+
## Tools
|
|
13
|
+
|
|
14
|
+
| Tool | Does |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `python3 tools/device_qa.py doctor --app <slug>` | Device, Developer Mode, passcode, developer disk image, signing identity, App Store Connect key. Prints a fix per problem |
|
|
17
|
+
| `python3 tools/device_qa.py devmode reveal` | Makes Settings > Privacy & Security > Developer Mode appear (`enable` turns it on when the phone has no passcode) |
|
|
18
|
+
| `python3 tools/device_qa.py test --app <slug> --suite qa --variant dark ...` | Puts the whole phone into a variant (dark, text size, location, VoiceOver, language), runs the suite, restores iOS defaults. Leaves the result bundle, a screenshot `index.md`, crash and jetsam reports and `summary.json`. `--only <test>` runs single tests while iterating on one defect; `--configuration Release` runs the UI suites on the optimized build. A source edit that lands mid-run marks the round as mixed and fails it |
|
|
19
|
+
| `python3 tools/device_qa.py traits --app <slug> [--screen X]` | Changes text size and dark mode on each manifest screen in front and in the background and checks the app survived in the same process (no 0x8BADF00D) |
|
|
20
|
+
| `python3 tools/device_qa.py perf --app <slug> --seconds 40` | Launches the app, attaches Time Profiler and counts Instruments' potential hangs and hang risks |
|
|
21
|
+
| `python3 tools/device_qa.py upgrade --app <slug> --from <previous tag>` | Builds the previous release in a temporary worktree, seeds real data with the manifest's seed test, installs this build over it and runs the check test |
|
|
22
|
+
| `python3 tools/device_qa.py release --app <slug> --archive <xcarchive>` | Exports the Release archive for the phone, installs it and cold-launches it with no debugger, then pulls crash reports |
|
|
23
|
+
| `python3 tools/device_qa.py lock status` and `lock run --note "<who>" -- <command>` | One phone, many sessions: who holds it and who waits. Raw xcodebuild, devicectl or xctrace work goes through `lock run` so it takes its turn |
|
|
24
|
+
| `swift tools/recording_frames.swift <video.mp4> <out-dir>` | Frames and a contact sheet from a UI-test screen recording (the `.mp4` in an xcresult or a `*-shots` folder). Look at them before calling a device UI failure an app defect. Given `<a.png> <b.png> ... <out-dir>` it tiles screenshots side by side |
|
|
25
|
+
| `swift tools/screenshot_contrast.swift <shot.png> <x0> <y0> <x1> <y1>` | The real WCAG contrast of text in a screenshot box (fractions). The audit reports "Contrast failed" on glass, materials and shadows; a reviewed exception needs the number the user sees |
|
|
26
|
+
| `Sources/QAKit/QAKit.swift` | UI-test helpers. Evidence: `QA.snap`, `QA.note`, `QA.assertInFront`. Accessibility: `QA.audit` (one report per screen, explained classes counted, one retry on an audit timeout). Alerts: `QA.answerSystemAlert`, `QA.dismissTopmost` (alert, share sheet, in-app Safari, action sheet, the app's closers, Back, system alert, swipe down). Walking: `QA.walk` (snap, audit, sweep, scroll loop), `QA.sweepButtons` (every visible button, from one snapshot of the tree), `QA.back`, `QA.closeInAppSafari`, `QA.exitSearch`, `QA.settle`, `QA.onScreen`, `QA.reveal` (scroll an off-screen row into view, `clearOf:` floating bars that swallow taps). Stability on a device: `QA.waitForStill`, `QA.tapUntilGone`, `QA.replaceText`, `QA.switches(labelled:)`, `QA.flip` (a Toggle's inner switch), `QA.waitForValue`, `QA.searchField` (iOS 26 bottom bar or minimised Search button), `QA.tree` (the element tree as text when something is missing), `QA.count` (rows of a lazy list that keeps only the visible ones in the tree). The app passes markers and identifiers only |
|
|
27
|
+
| `python3 tools/mutate.py --config <app>/release/testing/mutation.json --changed-since 'latest-tag:<app>/*'` | The mutation gate (SUITE-08): breaks the logic lines changed since the last release one at a time and checks the unit tests notice. Every survivor gets a test |
|
|
28
|
+
| `python3 tools/release_checks.py --app <slug> [--ipa X]` | Static checks right before submission: What's New, field limits, placeholders and markup, review notes, age-rating block, localized purpose strings, required-reason APIs, links, AASA, IPA entitlements |
|
|
29
|
+
|
|
30
|
+
Attaching a new app: write `<app>/release/testing/device_qa.json` (`docs/manifest.md`, start from `examples/device_qa.json`)
|
|
31
|
+
and link the `QAKit` package into the UI-test target (`docs/getting-started.md`).
|
|
32
|
+
|
|
33
|
+
## Order
|
|
34
|
+
|
|
35
|
+
1. **Readiness.** Run `doctor` until it prints `ready`. The test phone has no passcode, Developer Mode on, UI automation
|
|
36
|
+
allowed, automatic updates off.
|
|
37
|
+
2. **Risk map.** From `git diff --stat` since the last release tag and the changelog, list the changed areas. Build a UI
|
|
38
|
+
matrix of every screen, control and state (one reader per area plus a completeness critic when you have parallel
|
|
39
|
+
agents). Deduplicate suspected defects, try to refute each one, keep only CONFIRMED. Product decisions go on the
|
|
40
|
+
owner's decision list.
|
|
41
|
+
3. **Walkthrough test.** `<App>QAWalkthroughTests`: for every screen `QA.walk` (snap, audit, sweep, scroll until the bottom) with
|
|
42
|
+
a `settle` closure that brings the walk back to its screen (`QA.dismissTopmost` then `QA.back`; `QA.settle` relaunches only as
|
|
43
|
+
the last resort). Controls that leave the app or have real effects (call, purchase, delete) are excluded from the
|
|
44
|
+
sweep and get their own test that ends safely (a call is cancelled at the confirmation sheet). Always keep one
|
|
45
|
+
**first real use** test (no test mode: real location, network, onboarding).
|
|
46
|
+
4. **Run.** Unit, UI and qa suites under the default variant, then qa under `dark`, `large-text`, `english` and location
|
|
47
|
+
variants; `traits` for the trait-change survival; the mutation gate on the changed logic lines. Read the failure summary and
|
|
48
|
+
`crashes`.
|
|
49
|
+
5. **Screenshot review.** Split the screens of `index.md` and review them in parallel (clipping, overlap, empty states,
|
|
50
|
+
wrong facts, translation, contrast, touch targets, attribution). Each defect gets a refutation pass. Accessibility audit
|
|
51
|
+
notes (`a11y ...` attachments) go on the same list.
|
|
52
|
+
6. **Fix.** Group defects by file set, fix each in its own worktree (failing test first), integrate, rerun 3 to 5. Stop
|
|
53
|
+
when **two consecutive rounds find zero new defects**.
|
|
54
|
+
7. **Static checks.** `release_checks.py` (not offline) with 0 FAIL. After archiving, run it again with `--ipa` for the
|
|
55
|
+
entitlements.
|
|
56
|
+
8. **Verdict.** Fill the protocol's exit criteria into `<app>/docs/qa/QA_<version>.md` with evidence. Checks you did not
|
|
57
|
+
run go into an "untested" list with one line of risk each. Submission to review happens only on the owner's explicit
|
|
58
|
+
word.
|
|
59
|
+
|
|
60
|
+
## Pitfalls (measured on real phones)
|
|
61
|
+
|
|
62
|
+
- `xcode-select` pointing at CommandLineTools makes `xcrun devicectl` and `simctl` fail. The tools set
|
|
63
|
+
`DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer` unless you override it.
|
|
64
|
+
- The Developer Mode menu only appears after a Mac reveals it: `devmode reveal`.
|
|
65
|
+
- On iOS 27 the confirmation before dialling a business number is presented by `com.apple.BusinessActionSheet`, not
|
|
66
|
+
SpringBoard. `QA.answerSystemAlert` watches both; add others with `QA_ALERT_HOSTS`.
|
|
67
|
+
- Asking `isHittable` on an element outside the screen records a failure instead of returning false. The sweep only asks
|
|
68
|
+
elements whose frame centre is on screen.
|
|
69
|
+
- Behind a full-screen cover (onboarding) the first screen is already loaded, so its identifier exists while the cover is
|
|
70
|
+
up. Decide by the cover's buttons.
|
|
71
|
+
- A test that reads repo fixtures through `#filePath` finds nothing on the device. Put fixtures into the test bundle as
|
|
72
|
+
resources.
|
|
73
|
+
- Device tests cannot write to Mac paths (`/tmp/...`). Evidence goes into the result bundle through `XCTAttachment`.
|
|
74
|
+
- A simulator can wedge on app launch after days of use. Keep the UI gate on the device.
|
|
75
|
+
- Two sessions on one phone fight over the foreground app and the phone-wide settings. Every phone command takes the lock;
|
|
76
|
+
a second session waits in arrival order and `lock status` says who holds it.
|
|
77
|
+
- A sheet that closes between a query and the frame read fails a live query with "Failed to get matching snapshot". The sweep
|
|
78
|
+
reads one snapshot of the tree and taps coordinates.
|
|
79
|
+
- Rows of a SwiftUI List that are off screen are not in the tree, so `waitForExistence` on them never succeeds. Wait for an
|
|
80
|
+
anchor that is always on screen, then `QA.reveal` the row.
|
|
81
|
+
- A layout that is still moving swallows a tap on a device (an alert sliding with the keyboard, a bottom bar sliding in).
|
|
82
|
+
`QA.tapUntilGone` waits for the screen to be still and retries once.
|
|
83
|
+
- Tapping a text field that already has keyboard focus raises the edit menu over the buttons next to it. `QA.replaceText`
|
|
84
|
+
only taps when the field has no focus.
|
|
85
|
+
- The first time a variant switches VoiceOver on, iOS 27.2 shows a welcome prompt over the app and the first tap of the
|
|
86
|
+
walkthrough lands on it. `QA.settleAfterLaunch(app)` after `app.launch()` answers it (Use VoiceOver) and any permission
|
|
87
|
+
prompt that is already up.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prose-qa
|
|
3
|
+
description: QA for prose: a memo, a README, a design doc, a paper draft, a blog post. Run the deterministic gates (tools/prose_check.py), build a claims ledger and verify every consequential claim against its source, check the argument, then do a cold read as the intended reader. Use on "review this doc", "check the paper", "is this ready to send", "fact-check", and before publishing anything a reader will act on.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# prose-qa: a document is a product; test it like one
|
|
7
|
+
|
|
8
|
+
The same principles as `qa-rigor`, applied to text: decide what the reader must take away before editing (expected
|
|
9
|
+
value), check the boundaries (the first paragraph, the numbers, the conclusion), test the failure paths (a reader who
|
|
10
|
+
skips, a reader who disagrees), assert exact values (every number traced to a source), and leave a ledger.
|
|
11
|
+
|
|
12
|
+
## Order
|
|
13
|
+
|
|
14
|
+
1. **Expected reading.** Write one line: who reads this, what they must do or believe afterwards, and the one sentence
|
|
15
|
+
they would quote. If you cannot write it, the document is not ready for QA.
|
|
16
|
+
2. **Mechanical gates.** `python3 tools/prose_check.py draft.md`. DEFECTs (duplicate sentences, empty links, runaway
|
|
17
|
+
sentences) are fixed before anything else. WARNs are judgment calls: look at each, fix or leave it on purpose.
|
|
18
|
+
3. **Claims ledger.** `python3 tools/prose_check.py draft.md --claims claims.csv` lists every sentence with a number, a
|
|
19
|
+
year or an evidence word. For each row find the source, paste the quote that supports it, and write the verdict:
|
|
20
|
+
SUPPORTED (quote says it), PARTIAL (quote says less; the text adds something), NOT IN SOURCE, or NO SOURCE. A claim
|
|
21
|
+
with NO SOURCE either gets one or leaves the document. Numbers are compared digit by digit; a rounded figure says
|
|
22
|
+
"about". For a paper, the ledger also covers every citation: the cited work exists, the page or section is right, the
|
|
23
|
+
cited work says what the sentence says it says.
|
|
24
|
+
4. **Argument.** For a recommendation or a paper: the problem is stated before the answer, the answer follows from the
|
|
25
|
+
evidence shown (not from evidence the author has in mind), the alternatives considered are named and the reason for
|
|
26
|
+
rejecting them is given, and the limits are stated by the author before a reviewer states them. Each gap is one
|
|
27
|
+
line with the paragraph it belongs to.
|
|
28
|
+
5. **Consistency.** The same thing has one name throughout. Numbers that appear twice agree. The summary matches the
|
|
29
|
+
body. Terms defined on first use are used as defined.
|
|
30
|
+
6. **Cold read.** Read it once as the intended reader with no memory of the drafting (a separate agent or a fresh
|
|
31
|
+
context when you have one). Mark where you stopped understanding, where you stopped believing, and what you would
|
|
32
|
+
ask the author. A reader who stops is a defect, not a style note.
|
|
33
|
+
7. **Verdict.** The ledger: gates run and results, claims verified and their verdicts, argument gaps, cold-read stops,
|
|
34
|
+
and what was not checked with one line of risk each.
|
|
35
|
+
|
|
36
|
+
## What the tool does not do
|
|
37
|
+
|
|
38
|
+
`prose_check.py` cannot judge whether the argument holds or whether a quote supports a sentence; that is steps 3 to 6.
|
|
39
|
+
It also has no lexicon for a given house voice; keep that in your own skill and run it after this one.
|
|
40
|
+
|
|
41
|
+
## For papers specifically
|
|
42
|
+
|
|
43
|
+
- Every number in the abstract appears in the body with the same value and a table or figure reference.
|
|
44
|
+
- Every table and figure is referenced in the text, and every reference points at an existing table or figure.
|
|
45
|
+
- Methods give what a reader needs to rerun the experiment: data, versions, seeds, hardware, the exact metric.
|
|
46
|
+
- Related work names the closest prior result and says what is different, not only what is similar.
|
|
47
|
+
- Limitations are specific ("we did not test languages other than English") rather than generic.
|
|
48
|
+
- Reproduce one number yourself when the artifacts are available. A paper whose headline number cannot be reproduced
|
|
49
|
+
from its own artifacts fails QA regardless of the prose.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-rigor
|
|
3
|
+
description: Common QA procedure for any code (apps, Python, scripts, other repos) before merge or release. Design tests from boundaries, states and failure paths, measure test strength with mutation testing (tools/mutate.py), run runtime checkers, confirm in the user's real environment, and leave a verdict ledger. Use on "QA this", "make the tests solid", "ship without bugs", "mutation test", and before reporting a feature done. Apple apps add the device-qa layer on top.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# qa-rigor: green tests and no bugs are different things
|
|
7
|
+
|
|
8
|
+
A real example from the first measurement (2026-10-28): an app with every test green had 13 surviving mutants where
|
|
9
|
+
`>=` became `>` in its BMI grading. The grade was wrong at every boundary weight and no test knew.
|
|
10
|
+
|
|
11
|
+
## Layers
|
|
12
|
+
|
|
13
|
+
| Layer | What it looks at | Where |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Common (this skill) | test design, test strength (mutation), runtime checkers, real environment, verdict ledger | all code |
|
|
16
|
+
| Pull request | repo rules, CI replicated locally, the edges a bot reviewer will probe | your repo's PR skill |
|
|
17
|
+
| Apple app | real device, variants (dark, large text, language), screenshots, store requirements | `skills/device-qa` + `docs/device-qa-protocol.md` |
|
|
18
|
+
|
|
19
|
+
A lower layer never replaces an upper one. An app follows the nine principles below first.
|
|
20
|
+
|
|
21
|
+
## Principles
|
|
22
|
+
|
|
23
|
+
1. **Expected values before code.** For every changed behaviour write one line: "this input gives this output". If you
|
|
24
|
+
cannot write it, the requirement is not settled yet.
|
|
25
|
+
2. **Partition the inputs.** One case per group of inputs that get the same treatment, and three values at every
|
|
26
|
+
boundary (below, on, above: 14.9, 15, 15.1 km). Empty, maximum length, negative and Unicode are groups too. Screens
|
|
27
|
+
and lifecycles become a state table that covers every valid transition and every invalid attempt.
|
|
28
|
+
3. **Failure paths are features.** Network gone, permission denied, disk full, bad payload, time zone and DST,
|
|
29
|
+
concurrent calls: each has a test.
|
|
30
|
+
4. **Assertions are exact values.** A test that only checks `XCTAssertNotNil` or "did not crash" passes when the output is
|
|
31
|
+
wrong. Compare the result value, the stored state, the side effect that was called.
|
|
32
|
+
5. **Tests are deterministic.** No fixed `sleep` (bounded waits instead); inject time, randomness, network and location;
|
|
33
|
+
fix seeds; no order dependence.
|
|
34
|
+
6. **Test strength is measured by mutation testing.** Coverage only says "this line ran". Every mutant on a changed line
|
|
35
|
+
must die, or the line carries the reason it cannot. Procedure below.
|
|
36
|
+
7. **Turn on a runtime checker at least once.** Swift: ASan, UBSan, Main Thread Checker, TSan (Apple platforms: simulator
|
|
37
|
+
or Mac only). Python: `-W error`, pyflakes (undefined names). JS: strict mode.
|
|
38
|
+
8. **The last check happens in the user's environment.** Run the build the user receives, where they receive it: a Release
|
|
39
|
+
build on a real device for an app, the boot log after a restart for a server, the installed binary for a CLI.
|
|
40
|
+
9. **Every defect leaves two things.** A regression test that catches it again (watch it fail first) and a guard that stops
|
|
41
|
+
the same class next time (a lint rule, a harness test, a checklist line).
|
|
42
|
+
|
|
43
|
+
The verdict is a ledger: the gates you ran and their results, the gates you did not run and one line of risk each. Even
|
|
44
|
+
when every gate is green, an unrun gate is written down.
|
|
45
|
+
|
|
46
|
+
## Mutation testing
|
|
47
|
+
|
|
48
|
+
Break the code on purpose, one small change at a time, and rerun the tests. A test failing means the mutant **died** (a
|
|
49
|
+
test pins that behaviour). Everything passing means it **survived** (nobody checks that behaviour).
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
M=tools/mutate.py # from the QAKit checkout, or <your-repo>/path/to/qakit/tools/mutate.py
|
|
53
|
+
python3 $M --config <mutation.json> --list # which mutants would be made
|
|
54
|
+
python3 $M --config <mutation.json> --changed-since origin/main # before merge: changed lines only
|
|
55
|
+
python3 $M --config <mutation.json> --changed-since 'latest-tag:<app>/*' # before release: lines changed since the last tag
|
|
56
|
+
python3 $M --config <mutation.json> --jobs 5 --out <dir> # full measurement (periodic)
|
|
57
|
+
python3 $M --config <mutation.json> --out <dir> --resume # continue a long run that was cut short
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Config (JSON; paths are relative to the git top level that holds the config):
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{"name": "myapp-core", "root": "apps/myapp", "sources": ["Core/Sources/**/*.swift"], "exclude": [],
|
|
64
|
+
"setup": "", "build": "swift build --build-tests", "test": "swift test --skip-build",
|
|
65
|
+
"coverage": "swift test --enable-code-coverage ... > {lcov}", "jobs": 4}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- Work happens in a temporary copy of `root`. The checkout is never touched, so other sessions can keep working.
|
|
69
|
+
- A `build` failure means the mutant does not compile (invalid, not counted). A `test` failure or timeout means killed.
|
|
70
|
+
- With `coverage`, mutants on lines no test runs are not executed and are reported **uncovered**. They count with survivors.
|
|
71
|
+
- `{orig}` is the real root (for virtualenv paths), `{file}` and `{stem}` the mutated file (to run related tests only),
|
|
72
|
+
`{lcov}` the coverage output path.
|
|
73
|
+
- Python example: `"test": "{orig}/.venv/bin/python -m pytest -x -q -p no:cacheprovider"`.
|
|
74
|
+
- Xcode apps take one to two minutes per mutant, so measure a full sweep with `sweep_sample` and run every changed line at
|
|
75
|
+
release time.
|
|
76
|
+
|
|
77
|
+
Operators: boundary (`<` and `<=`, `>` and `>=`), equality (`==` and `!=`), logical (`&&` and `||`, `and` and `or`),
|
|
78
|
+
arithmetic (`+` and `-`, `*` and `/`, `+=` and `-=`), boolean (`true` and `false`), negation removal (`!x`, `not x` to
|
|
79
|
+
`x`), call statement removal (`store.save(x)` dropped; logging, print, assert, super and builder elements such as
|
|
80
|
+
`Text(...)` are left alone). C-family languages (Swift, JS, TS, Kotlin, Go, Java, C#) only swap binary operators with
|
|
81
|
+
spaces on both sides, which keeps generics (`Array<Int>`), arrows and unary operators intact. Strings, comments and `#if`
|
|
82
|
+
lines are never touched.
|
|
83
|
+
|
|
84
|
+
### Handling survivors
|
|
85
|
+
|
|
86
|
+
For every line of the report (`mutation.md`) do one of two things.
|
|
87
|
+
|
|
88
|
+
1. **Write a test.** Write the test that fails under that mutant, rerun, confirm it dies. Most survivors are a missing
|
|
89
|
+
boundary value (principle 2) or a loose assertion (principle 4).
|
|
90
|
+
2. **Write the reason.** Only when the change cannot alter behaviour (an equivalent mutant) add `// mutation-ok: <reason>`
|
|
91
|
+
(`# mutation-ok: <reason>` in Python) on that line. Examples: `count > 0` and `count >= 1` on an integer, a cache size
|
|
92
|
+
that only changes performance, log message assembly. "Writing the test is tedious" is not a reason.
|
|
93
|
+
|
|
94
|
+
"no test runs this line" means no test reaches that code at all. If it is not unit-testable (View code, for instance),
|
|
95
|
+
move it to `exclude` and write next to the config which layer owns it (UI tests, screenshots, device QA).
|
|
96
|
+
|
|
97
|
+
### Pitfalls (measured)
|
|
98
|
+
|
|
99
|
+
- **The machine is shared.** SwiftPM builds start one compiler per core. Five jobs pushed a shared Mac's load to 174 and
|
|
100
|
+
slowed another session's simulator. The engine runs under `nice -n 10`; SwiftPM configs default to `-j 4` and 3 jobs.
|
|
101
|
+
- **Same-size mutants hide behind caches.** Python `.pyc` freshness uses mtime (seconds) and size. A `*` to `/` mutant
|
|
102
|
+
written in the same second runs the old bytecode and survives falsely. The engine sets `PYTHONDONTWRITEBYTECODE=1`
|
|
103
|
+
and advances mtimes.
|
|
104
|
+
- **Branches that never compile.** The other side of `#if SWIFT_PACKAGE ... #else` is not compiled in that build, so its
|
|
105
|
+
mutants always survive. Mark the line `mutation-ok`.
|