add a Makefile for the common commands, and a test reporter

The invocations in CLAUDE.md were correct but long, and retyping them is how
flags get dropped. `make check` is the gate a change should pass: Yarn compiles,
voice sheets are in sync, EditMode is green.

`make test` and `make build` depend on a `lock` target that fails if the Editor
has the project open, which removes the most common confusing failure. It
filters out AssetImportWorker children, which are not a second Editor.

tools/ci/report_tests.py parses the NUnit results and exits non-zero on failure,
because Unity documents no common exit-code definition across the components
under test — the XML is the authority, not $?.

`make merge-driver` configures UnityYAMLMerge for the .gitattributes rules added
earlier. Note the binary lives in the Editor bundle under Contents/Helpers, not
Contents/Tools as most guides say; there is no Tools directory in Unity 6 on
macOS.

Also ignores the local .test-results/ and build/ output.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 20:29:39 +02:00
co-authored by Claude Opus 5
parent 2b35d3f754
commit f5b302b3e3
4 changed files with 146 additions and 0 deletions
+6
View File
@@ -127,3 +127,9 @@ tools/**/obj/
*.tar.gz *.tar.gz
*.7z *.7z
*.rar *.rar
# ---------------------------------------------------------------------------
# Local build and test output (Makefile targets)
# ---------------------------------------------------------------------------
/.test-results/
/build/
+30
View File
@@ -58,6 +58,36 @@ AI Navigation 2.0.14, ProBuilder 6.1.2, Test Framework 1.7.0. Yarn Spinner is an
--- ---
## 2b. Everyday commands
A `Makefile` at the repo root wraps everything in this document. Prefer it — the exact
invocations live there instead of being retyped.
```bash
make # list targets
make check # yarn + voice sheets + EditMode — run this before committing
make test # EditMode suite, with a readable summary
make yarn # compile every Yarn script (seconds, no Unity)
make build # macOS player into build/
make lock # fail if the Editor has the project open
```
`make test` and `make build` refuse to run while the Editor is open, which removes the most
common way these commands fail confusingly.
### Merge driver — one-time per machine
`.gitattributes` marks Unity YAML for `unityyamlmerge`. Git needs to be told what that is, or
it falls back to a line-oriented text merge that can produce a scene Unity still loads and that
is quietly wrong:
```bash
make merge-driver
```
That points git at `UnityYAMLMerge` inside the Editor bundle (`Contents/Helpers/`, not
`Contents/Tools/` — the path in most online guides is wrong for Unity 6 on macOS).
## 3. Unity from the command line ## 3. Unity from the command line
### 3.0 Two different tools, both present ### 3.0 Two different tools, both present
+66
View File
@@ -0,0 +1,66 @@
# Nightclub Arcadia — common commands.
#
# Everything here is documented in CLAUDE.md; this file exists so the exact
# invocations live in one place instead of being retyped from memory.
UNITY_CLI ?= /Users/lennart/.unity/bin/unity
UNITY ?= /Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity
PROJECT ?= $(CURDIR)/NightclubArcadia
RESULTS ?= $(CURDIR)/.test-results
SMARTMERGE ?= /Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/Helpers/UnityYAMLMerge
.DEFAULT_GOAL := help
.PHONY: help check yarn yarn-play voices voices-sync test test-play build lock clean-results merge-driver
help: ## Show this help
@grep -hE '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "} {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo " Unity must be CLOSED for test/build — run 'make lock' to check."
check: yarn voices test ## Everything a change should pass before commit
yarn: ## Compile every Yarn script (seconds, no Unity)
@cd tools/YarnCheck && DOTNET_CLI_TELEMETRY_OPTOUT=1 dotnet run -- ../../NightclubArcadia/Assets/Dialogue
yarn-play: ## Play a node: make yarn-play NODE=Bartender_Talk PICKS="2 2 0 0"
@cd tools/YarnCheck && DOTNET_CLI_TELEMETRY_OPTOUT=1 \
dotnet run -- ../../NightclubArcadia/Assets/Dialogue $(NODE) $(PICKS)
voices: ## Check writing/voices/*.md against skill_bible.json
@python3 tools/writing/sync_voices.py --check
voices-sync: ## Fold edited voice prose into skill_bible.json
@python3 tools/writing/sync_voices.py --to-json
test: lock ## Run the EditMode suite
@mkdir -p $(RESULTS)
@$(UNITY_CLI) test $(PROJECT) --no-banner --mode EditMode \
--output $(RESULTS)/editmode.xml --timeout 900 || true
@python3 tools/ci/report_tests.py $(RESULTS)/editmode.xml
test-play: lock ## Run the PlayMode suite (none exist yet)
@mkdir -p $(RESULTS)
@$(UNITY_CLI) test $(PROJECT) --no-banner --mode PlayMode \
--output $(RESULTS)/playmode.xml --timeout 900 || true
@python3 tools/ci/report_tests.py $(RESULTS)/playmode.xml
build: lock ## Build a macOS player into build/
@$(UNITY_CLI) build $(PROJECT) --no-banner --target StandaloneOSX -o $(CURDIR)/build/NightclubArcadia.app
lock: ## Fail if the Unity Editor has the project open
@if pgrep -fl "Unity.app/Contents/MacOS/Unity" | grep -v AssetImportWorker | grep -q .; then \
echo "The Unity Editor is running — close it first (only one instance per project)."; \
exit 1; \
fi
merge-driver: ## One-time: teach git to merge Unity YAML with UnityYAMLMerge
@test -x "$(SMARTMERGE)" || { echo "UnityYAMLMerge not found at $(SMARTMERGE)"; exit 1; }
@git config merge.unityyamlmerge.name "Unity SmartMerge"
@git config merge.unityyamlmerge.driver "'$(SMARTMERGE)' merge -p --force --fallback none %O %B %A %A"
@git config merge.unityyamlmerge.recursive binary
@echo "configured: $$(git config merge.unityyamlmerge.name)"
clean-results: ## Remove local test results
@rm -rf $(RESULTS)
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Summarise an NUnit results file and exit non-zero if anything failed.
Unity's own docs state there is no common exit-code definition across the
components under test, so the results XML is the authority, not $?.
"""
import sys
import xml.etree.ElementTree as ET
def main(path):
try:
root = ET.parse(path).getroot()
except FileNotFoundError:
print(f"no results at {path} — the run did not get far enough to write one")
return 1
except ET.ParseError as exc:
print(f"could not parse {path}: {exc}")
return 1
total = int(root.get("total") or 0)
passed = int(root.get("passed") or 0)
failed = int(root.get("failed") or 0)
skipped = int(root.get("skipped") or 0)
for case in root.iter("test-case"):
if case.get("result") == "Passed":
continue
print(f"FAIL {case.get('fullname')}")
message = case.find("failure/message")
if message is not None and message.text:
for line in message.text.strip().splitlines()[:6]:
print(f" {line}")
if total == 0:
print("no tests ran")
else:
print(f"{passed}/{total} passed" + (f", {skipped} skipped" if skipped else ""))
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else ".test-results/editmode.xml"))