diff --git a/README.md b/README.md index 35afaff..e0c9b28 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,69 @@ # constraint-error-ci-example - -- example of catching constraint errors during test execution with `lldb`. -- check the [pull request](https://github.com/kaanbiryol/constraint-error-ci-example/pull/3) to see danger errors. + +Catch broken Auto Layout constraints during test execution using LLDB breakpoints, and surface them as PR warnings via Danger. + +## Why? + +`UIViewAlertForUnsatisfiableConstraints` fires at runtime but is invisible in CI. Broken constraints silently ship to production, causing layout bugs that are hard to trace back to a specific change. + +This project demonstrates a technique to intercept these warnings during tests and block them at the PR level. + +## What it does + +- Sets an LLDB breakpoint on `UIViewAlertForUnsatisfiableConstraints` that runs in the background during test execution +- A Python script attached to the breakpoint walks the call stack, identifies the failing `XCTestCase`, and logs it to `brokenConstraints.txt` +- Danger reads the output file and posts constraint violations as warnings on the pull request +- See [PR #3](https://github.com/kaanbiryol/constraint-error-ci-example/pull/3) for an example of the Danger output + +## Requirements + +- Xcode 14.1+ +- macOS (CI runs on `macos-latest`) +- `danger-swift` (installed via Homebrew in CI) + +## Quick start + +```bash +# Run tests locally +make test + +# Run LLDB constraint detection (background) + tests +lldb --source lldb-command & +make test +``` + +## How it works + +1. **LLDB attaches** to the test process and sets a breakpoint on `UIViewAlertForUnsatisfiableConstraints` with auto-continue enabled +2. **On each hit**, a Python command iterates the thread's frames, finds the `XCTestCase` subclass, and appends the module/file/function to `brokenConstraints.txt` +3. **After tests complete**, Danger checks for the output file and warns on the PR with the logged violations + +## CI pipeline + +Defined in `.github/workflows/snapshot.yml`, triggered on pull requests: + +``` +checkout -> lldb --source lldb-command & -> make test -> danger-swift ci +``` + +## Project structure + +``` +lldb-command # LLDB breakpoint script (Python) +Dangerfile.swift # Danger config - reads constraint errors, warns on PR +Makefile # xcodebuild wrapper +constraint-error-ci-example/ + BrokenView.swift # UIView with intentionally broken constraints (demo) +constraint-error-ci-snapshots/ + BrokenViewTests.swift # Snapshot tests that trigger constraint violations +``` + +## Limitations + +- LLDB must attach to the process before tests start - requires background launch (`&`) +- The `lldb-command` script relies on `GITHUB_WORKSPACE` env var, so local runs won't write to `brokenConstraints.txt` without modification +- Only detects constraints broken during test execution - no static analysis + +## License + +MIT diff --git a/constraint-error-ci-snapshots/BrokenViewTests.swift b/constraint-error-ci-snapshots/BrokenViewTests.swift index 9569cbc..496613f 100644 --- a/constraint-error-ci-snapshots/BrokenViewTests.swift +++ b/constraint-error-ci-snapshots/BrokenViewTests.swift @@ -12,22 +12,22 @@ import SnapshotTesting final class BrokenViewTests: XCTestCase { -// func testBrokenView0() { -// let brokenView = BrokenView() -// brokenView.text = "BrokenView0" -// assertSnapshots(matching: brokenView, as: [.image]) -// } -// -// func testBrokenView1() { -// let brokenView = BrokenView() -// brokenView.text = "BrokenView1" -// assertSnapshots(matching: brokenView, as: [.image]) -// } -// -// func testPassingTest() { -// let brokenView = UILabel() -// brokenView.text = "BrokenView2" -// assertSnapshots(matching: brokenView, as: [.image]) -// } + func testBrokenView0() { + let brokenView = BrokenView() + brokenView.text = "BrokenView0" + assertSnapshots(matching: brokenView, as: [.image]) + } + + func testBrokenView1() { + let brokenView = BrokenView() + brokenView.text = "BrokenView1" + assertSnapshots(matching: brokenView, as: [.image]) + } + + func testPassingTest() { + let brokenView = UILabel() + brokenView.text = "BrokenView2" + assertSnapshots(matching: brokenView, as: [.image]) + } }