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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
34 changes: 17 additions & 17 deletions constraint-error-ci-snapshots/BrokenViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}

}
Loading