Catching unsatisfiable Auto Layout constraints won’t catch 100% of the times the UI breaks, but it can give us a good hint. Unsatisfiable Auto Layout constraints errors happen when there are two or more conflicting Auto Layout constraints installed in the app’s view hierarchy. When that happens, UIKit logs the conflicting constraints in the console, along with the constraint removed by UIKit in an attempt to fix this issue.
We had one more motivation to do this. We can’t ship UI that shows unsatisfiable Auto Layout constraints issues in the logs to our developers (whether those constraints result in a broken UI or not).
How We Catch Unsatisfiable Auto Layout Constraints
I’ll be honest. At first, we thought we should just parse the logs from the tests and look for the unsatisfiable Auto Layout constraints issue, but there was this itch that wouldn’t go away to find a better way, and we did.
UIKit generously offers a symbolic breakpoint
UIViewAlertForUnsatisfiableConstraints for this issue and we can use that in our development setup, but what about CI?
First, let’s try to make it work in the terminal locally.
Our development setup will be two terminal windows: one for running the UITests in the command line and another for running the LLDB interactive shell.
1. Attach Debugger to UITests
First, we need to successfully attach the debugger to the UITests from the command line. To do that, we will:
- Run the debugger to wait for the process
- Run the UITests
- Wait for verification
Running the Debugger to Wait for the Process
First, let’s launch the debugger interactive shell. We can do that by running:
From that, we move to the interactive shell:
Now we want to attach it to the app and for that, we need the process ID. Fortunately, we can just use the name to do this. Also, our UITests didn’t run yet, so we want the shell to wait.
Our demo app for UITests is called
InstabugDemo so we will use it in the examples:
(lldb) process attach --name "InstabugDemo" --waitfor
The interactive shell should be stopped and waiting as expected.
Run the UI Tests
First of all, make sure you have an unsatisfiable Auto Layout constraints issue.
Now, for that we will use
xcodebuild, If you are not familiar with it, it is just like running with Xcode except you have to be precise about what you want exactly.
You will need to run this:
xcodebuild -workspace Instabug/Instabug.xcworkspace -scheme -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=12.1,name=iPhone XR' test
Note: You may need to adjust the OS version depending on what version of Xcode you are using, we are using Xcode 10.1
This will build your UITests target and run it in the terminal.
Wait for Verification
Switch back to the debugger window and you should see this now:
(lldb) process attach --name "InstabugDemo" --waitfor Process 27192 stopped * thread #1, stop reason = signal SIGSTOP frame #0: 0x0000000110391836 dyld`pread + 10 dyld`pread: -> 0x110391836 <+10>: jae 0x110391840 ; <+20> 0x110391838 <+12>: movq %rax, %rdi 0x11039183b <+15>: jmp 0x1103907ee ; cerror 0x110391840 <+20>: retq Target 0: (InstabugDemo) stopped. *Executable module set to "/Users/yousefhamza/Library/Developer/CoreSimulator/Devices/F9A10403-5370-45EB-9D5B-A7C48A0F42DE/data/Containers/Bundle/Application/7AEB66C6-8F0C-4DCF-B5FC-0616A3382506/InstabugDemo.app/InstabugDemo". *Architecture set to: x86_64h-apple-ios
2. Add Break Symbolic Breakpoint
For this, we can use a simple LLDB command:
(lldb) b UIViewAlertForUnsatisfiableConstraints Breakpoint 1: no locations (pending). WARNING: Unable to resolve breakpoint to any actual locations.
Now, when that exception is fired from
UIKit, your interactive shell will stop just like it does with Xcode and you can add any LLDB commands you want to them. However, we are building for CI so we don’t want to do things ourselves.
While we’re here, we can add some commands to be executed with that break. Even better, LLDB supports running Python with breakpoints. How cool is that!
Our solution is to add a file on the desktop that we can check on later:
(lldb) breakpoint command add -s python Enter your Python command(s). Type 'DONE' to end. def function (frame, bp_loc, internal_dict): """frame: the lldb.SBFrame for the location at which you stopped bp_loc: an lldb.SBBreakpointLocation for the breakpoint location information internal_dict: an LLDB support object not to be used"""
Then we add these three lines:
Enter your Python command(s). Type 'DONE' to end. def function (frame, bp_loc, internal_dict): """frame: the lldb.SBFrame for the location at which you stopped bp_loc: an lldb.SBBreakpointLocation for the breakpoint location information internal_dict: an LLDB support object not to be used""" f = open('UNSTATISFIABLE_CONSTRAINTS', 'w') f.write('a') f.close()
DONE and press return.
3. Continue Running the UITests
(lldb) continue Process 29271 resuming 1 location added to breakpoint 1
Now you should see the app continue execution of the UITests and when your introduced bug happens, you should see something like this in the LLDB window:
Process 29271 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGTERM frame #0: 0x000000010c2acc2a libsystem_kernel.dylib`mach_msg_trap + 10 libsystem_kernel.dylib`mach_msg_trap: -> 0x10c2acc2a <+10>: retq 0x10c2acc2b <+11>: nop libsystem_kernel.dylib`mach_msg_overwrite_trap: 0x10c2acc2c <+0>: movq %rcx, %r10 0x10c2acc2f <+3>: movl $0x1000020, %eax ; imm = 0x1000020 Target 0: (InstabugDemo) stopped
Note: By the way, at this point, you can execute any LLDB command just like you do with Xcode, something like this:
(lldb) po x
4. Automating LLDB
We did what we wanted, but in the interactive shell, there’s another way to use LLDB by supplying all the commands that we entered one by one in a text file (“lldb.cmd”?) like this:
process attach --name "InstabugDemo" --waitfor b UIViewAlertForUnsatisfiableConstraints breakpoint command add -s python # python script f = open('UNSTATISFIABLE_CONSTRAINTS', 'w') f.write('a') f.close() DONE continue
Now close the interactive shell and run this in the terminal:
$ lldb --source lldb.cmd
Make sure your tests are running and you will see both executing automatically. Cool, right?
5. Making Sure Your Tests Keep Running
To make sure our tests keep running, we need to:
- Terminate the test
- Re-add the breakpoint
Terminating the Test
To terminate the test, we need to add this to our file:
This will kill our app process and UITests will continue with the next test.
Re-adding the Breakpoint
The script we added to LLDB is just a non-programmable text field so we don’t have loops or anything. However, we can still use loops with recursion.
Remember the LLDB command we used to run the “lldb.cmd” file? We can actually invoke it inside our file and it will do just that. After killing the app, it will start waiting for the process again and add the breakpoint. We need to add this to the file:
command source lldb.cmd
The whole file should look like this:
process attach --name "InstabugDemo" --waitfor b UIViewAlertForUnsatisfiableConstraints breakpoint command add -s python # python script f = open('UNSTATISFIABLE_CONSTRAINTS', 'w') f.write('a') f.close() DONE continue process kill command source lldb.cmd
6. Adding it to Circle CI
We use CircleCI, and we need to do two things there:
- Run LLDB in the background.
- Check if there’s a UITests failure which is an unsatisfiable Auto Layout constraints failure.
Run LLDB in the Background
We use CircleCI 2.0 and to run LLDB in the background, we need to add this before running the UITests:
- run: name: Run debugger command: lldb --source lldb.cmd background: true
The key here is adding
background: true which will make it return right away but continue to run in the background.
Check if UITests Failure Is an Unsatisfiable Auto Layout Constraints Failure
We need to run that check only if UITests fail to check if that failure is a normal failure or is related to unsatisfiable Auto Layout constraints issues.
- run: name: Check unsatisfiable constraints when: on_fail command: test -f UNSTATISFIABLE_CONSTRAINTS && echo 'There's unsatisfiable constraints failure in UITests' || 'That's a normal failure'
when: on_fail will make sure it runs on failure only.
I hope you found this useful and that you start using it with your teams. Also, please let me know about your experience with this and share more fun stuff to do with LLDB.
- How We Automate Our iOS Workflow at Instabug Using CircleCI
- How We Refactored Our Monolithic iOS Framework
- Swift 5 Module Stability Workaround for Binary Frameworks
- For more info about using LLDB, check out this tutorial