Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Level up your CI for iOS and macOS

Level up your CI for iOS and macOS

Developers need a fast, reliable CI to be productive. An excellent CI helps you quickly find issues in your code and confidently ship new code, reducing stress and helping you sleep better at night. But setting it up properly takes a lot of time. This talk isn’t about showing you how to set up a CI from scratch. Instead, you’ll get advanced tips and tricks for creating and maintaining an awesome CI for your iOS or macOS app. It’s time to level up your CI.

First, we’ll look at tips and tricks for unit, integration, and UI tests. When tests fail in CI, it’s vital to be able to tell at a glance why they failed — flaky tests can drive developers crazy! We’ll show you a few ways to investigate and fix flaky tests, and even flaky CI.

Linters and code analyzers are often overlooked, although they can help you identify bugs early. Along with code formatting tools and Xcode Thread Sanitizer, they bring your CI to the next level.

Next, we’ll cover how to properly validate your libraries before publishing to CocoaPods, Swift Package Manager, or Carthage. Say goodbye to developers pinging you that they can’t build the latest version of your SDK.

Some code is very hard to test and risky to change without introducing new bugs. You’re going to see a tactic that ensures nobody changes code like that by accident.

Philipp Hofmann

November 10, 2022
Tweet

More Decks by Philipp Hofmann

Other Decks in Technology

Transcript

  1. Tips and Tricks 1. Better Test Logs 2. Flaky Tests

    3. Flaky CI 4. SwiftLint & Clang-Format 5. Xcode Thread Sanitizer 6. Xcode Analyze 7. Validate your Library 8. Borrowing Tests 9. High Risk Files
  2. Command line invocation: /Applications/Xcode_12.5.1.app/Contents/Developer/usr/bin/xcodebuild -workspace Sentry.xcworkspace -scheme Sentry -configuration Test

    GCC_GENERATE_TEST_COVERAGE_FILES=YES GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES -destination "platform=iOS Simulator,OS=latest,name=iPhone 8" test User defaults from command line: IDEPackageSupportUseBuiltinSCM = YES Build settings from command line: GCC_GENERATE_TEST_COVERAGE_FILES = YES GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES note: Using new build system note: Building targets in parallel note: Planning build note: Analyzing workspace note: Constructing build description note: Build preparation complete CreateBuildDirectory /Users/runner/Library/Developer/Xcode/DerivedData/Sentry-azcqyxgbmmrdtdegukmbgarszteu/Build/ Intermediates.noindex cd /Users/runner/work/sentry-cocoa/sentry-cocoa builtin-create-build-directory /Users/runner/Library/Developer/Xcode/DerivedData/Sentry-azcqyxgbmmrdtdegukmbgarszteu/Build/ Intermediates.noindex CreateBuildDirectory /Users/runner/Library/Developer/Xcode/DerivedData/Sentry-azcqyxgbmmrdtdegukmbgarszteu/Build/Products cd /Users/runner/work/sentry-cocoa/sentry-cocoa builtin-create-build-directory /Users/runner/Library/Developer/Xcode/DerivedData/Sentry-azcqyxgbmmrdtdegukmbgarszteu/Build/ Products CreateBuildDirectory /Users/runner/Library/Developer/Xcode/DerivedData/Sentry-azcqyxgbmmrdtdegukmbgarszteu/Build/Products/Test- iphonesimulator cd /Users/runner/work/sentry-cocoa/sentry-cocoa builtin-create-build-directory /Users/runner/Library/Developer/Xcode/DerivedData/Sentry-azcqyxgbmmrdtdegukmbgarszteu/Build/ Products/Test-iphonesimulator
  3. Test Suite 'SentryCrashFileUtils_Tests' started at 2022-10-25 23:28:08.671 Test Case '-[SentryCrashFileUtils_Tests

    testLastPathEntry]' started. Test Case '-[SentryCrashFileUtils_Tests testLastPathEntry]' passed (0.002 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_EmptyFile]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_EmptyFile]' passed (0.005 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_FileIsBigger]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_FileIsBigger]' passed (0.004 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_ReadBufferIsMuchSmaller]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_ReadBufferIsMuchSmaller]' passed (0.004 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_ReadBufferIsSmaller]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_ReadBufferIsSmaller]' passed (0.003 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_SameSize]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBuffered_SameSize]' passed (0.006 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_Beginning]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_Beginning]' passed (0.005 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_End]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_End]' passed (0.003 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_Halfway]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_Halfway]' passed (0.004 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_NotFound]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_NotFound]' passed (0.004 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_NotFound_LargeFile]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_NotFound_LargeFile]' passed (0.003 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_SmallDstBuffer]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_SmallDstBuffer]' passed (0.003 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_SmallReadBuffer]' started. Test Case '-[SentryCrashFileUtils_Tests testReadBufferedUntilChar_SmallReadBuffer]' passed (0.026 seconds). Test Case '-[SentryCrashFileUtils_Tests testReadBytesFromFD]' started.
  4. Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_NoCrashLastLaunch]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_NoCrashLastLaunch]' passed

    (0.011 seconds). Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_NoCurrentSession]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_NoCurrentSession]' passed (0.004 seconds). Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_WhenOOM_WithCurrentSession]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_WhenOOM_WithCurrentSession]' passed (0.023 seconds). Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_WithCurrentSession]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testEndSessionAsCrashed_WithCurrentSession]' passed (0.008 seconds). Test Case '-[SentryTests.SentryCrashIntegrationTests testInstall_WhenStitchAsyncCallsDisabled_DoesNotCallInstallAsyncHooks]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testInstall_WhenStitchAsyncCallsDisabled_DoesNotCallInstallAsyncHooks]' passed (0.003 seconds). Test Case '-[SentryTests.SentryCrashIntegrationTests testInstall_WhenStitchAsyncCallsEnabled_CallsInstallAsyncHooks]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testInstall_WhenStitchAsyncCallsEnabled_CallsInstallAsyncHooks]' passed (0.003 seconds). Test Case '-[SentryTests.SentryCrashIntegrationTests testLocaleChanged_DifferentLocale_SetsCurrentLocale]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testLocaleChanged_DifferentLocale_SetsCurrentLocale]' passed (0.007 seconds). Test Case '-[SentryTests.SentryCrashIntegrationTests testLocaleChanged_NoDeviceContext_SetsCurrentLocale]' started. Test Case '-[SentryTests.SentryCrashIntegrationTests testLocaleChanged_NoDeviceContext_SetsCurrentLocale]' passed (0.003 seconds).
  5. Test Suite 'Selected tests' failed at 2022-10-27 11:29:58.069. Executed 1564

    tests, with 1 test skipped and 3 failures (0 unexpected) in 46.706 (47.486) seconds 2022-10-27 11:30:07.091 xcodebuild[48576:4363253] [MT] IDETestOperationsObserverDebug: 57.629 elapsed -- Testing started completed. 2022-10-27 11:30:07.091 xcodebuild[48576:4363253] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start 2022-10-27 11:30:07.091 xcodebuild[48576:4363253] [MT] IDETestOperationsObserverDebug: 57.629 sec, +57.629 sec -- end Test session results, code coverage, and logs: /Users/philipphofmann/Library/Developer/Xcode/DerivedData/Sentry- gcimrafeikdpcwaanncxmwrieqhi/Logs/Test/Test-Sentry-2022.10.27_11-29-08-+0200.xcresult Failing tests: SentryClientTest.testCaptureCrash_Culture() -[SentryDeviceTests testDeviceModel] SentryNetworkTrackerIntegrationTests.testGetRequest_CompareSentryTraceHeader() ** TEST FAILED **
  6. SentryTests.SentryNetworkTrackerIntegrationTests testGetRequest_CompareSentryTraceHeader, XCTAssertEqual failed: ("Optional("4f6049d2753e484f81ee859aa8c88c7e-bbc6d1f19e76479e-1")") is not equal to ("Optional("")")

    /Users/philipphofmann/git-repos/sentry-cocoa/Tests/SentryTests/Integrations/Performance/Network/ SentryNetworkTrackerIntegrationTests.swift:201 ``` let expectedTraceHeader = networkSpan.toTraceHeader().value() XCTAssertEqual(expectedTraceHeader, response) } ``` Executed 1567 tests, with 1 test skipped and 3 failures (0 unexpected) in 63.980 (64.542) seconds 2022-10-28 09:03:34.884 xcodebuild[5492:40161] [MT] IDETestOperationsObserverDebug: 70.839 elapsed -- Testing started completed. 2022-10-28 09:03:34.884 xcodebuild[5492:40161] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start 2022-10-28 09:03:34.884 xcodebuild[5492:40161] [MT] IDETestOperationsObserverDebug: 70.839 sec, +70.839 sec -- end Failing tests: SentryClientTest.testCaptureCrash_Culture() -[SentryDeviceTests testDeviceModel] SentryNetworkTrackerIntegrationTests.testGetRequest_CompareSentryTraceHeader() ** TEST FAILED **
  7. class TestObserver : XCTestObservation { func testCase(_ testCase: XCTestCase, didRecord

    issue: XCTIssue) { let exception = NSException(name: testCase.name, reason: issue.description) } }
  8. class TestObserver : XCTestObservation { func testCase(_ testCase: XCTestCase, didRecord

    issue: XCTIssue) { let exception = NSException(name: testCase.name, reason: issue.description) SentrySDK.capture(exception: exception) } }
  9. class TestObserver : XCTestObservation { func testCaseWillStart(_ testCase: XCTestCase) {

    let crumb = Breadcrumb(level: .debug, category: "test.started") } }
  10. class TestObserver : XCTestObservation { func testCaseWillStart(_ testCase: XCTestCase) {

    let crumb = Breadcrumb(level: .debug, category: "test.started") crumb.message = testCase.name } }
  11. class TestObserver : XCTestObservation { func testCaseWillStart(_ testCase: XCTestCase) {

    let crumb = Breadcrumb(level: .debug, category: "test.started") crumb.message = testCase.name SentrySDK.addBreadcrumb(crumb: crumb) } }
  12. iPhone 16.1 iPhone 15.7 iPhone 14.0 matrix: ios-version: ["16.1", "15.7",

    "14.0"] device: ["iPhone", "iPad"] iPad 16.1 iPad 15.7 iPad 14.0
  13. class Counter { private var internalCount = 0 func increment()

    { internalCount += 1 } var count: Int { return internalCount } }
  14. class CounterTests: XCTestCase { func testIncrementConcurrent() { let expectedCounts =

    100 let counter = Counter() let queue = DispatchQueue(label: "CounterTest") } }
  15. class CounterTests: XCTestCase { func testIncrementConcurrent() { let expectedCounts =

    100 let counter = Counter() let queue = DispatchQueue(label: "CounterTest") for _ in 0..<expectedCounts { } } }
  16. class CounterTests: XCTestCase { func testIncrementConcurrent() { let expectedCounts =

    100 let counter = Counter() let queue = DispatchQueue(label: "CounterTest") for _ in 0..<expectedCounts { queue.async { counter.increment() } } } }
  17. class CounterTests: XCTestCase { func testIncrementConcurrent() { let expectedCounts =

    100 let counter = Counter() let queue = DispatchQueue(label: "CounterTest") for _ in 0..<expectedCounts { queue.async { counter.increment() } } XCTAssertEqual(expectedCounts, counter.count) } }
  18. ================== WARNING: ThreadSanitizer: data race (pid=61245) Write of size 8

    at 0x7b080029af50 by thread T8: #0 Counter.increment() Counter.swift:8 (MobileDevSummit:x86_64+0x100007052) #1 closure #1 in CounterTests.testIncrementConcurrent() CounterTests.swift:14 (MobileDevSummitTests:x86_64+0x1e7c) #2 partial apply for closure #1 in CounterTests.testIncrementConcurrent() <compiler-generated> (MobileDevSummitTests:x86_64+0x332d) #3 thunk for @escaping @callee_guaranteed () -> () <compiler-generated> (MobileDevSummitTests:x86_64+0x1ef2) #4 __tsan::invoke_and_release_block(void*) <null>:2 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x7f3eb) #5 _dispatch_client_callout <null>:2 (libdispatch.dylib:x86_64+0x2a39) Previous write of size 8 at 0x7b080029af50 by thread T9: #0 Counter.increment() Counter.swift:8 (MobileDevSummit:x86_64+0x100007052) #1 closure #1 in CounterTests.testIncrementConcurrent() CounterTests.swift:14 (MobileDevSummitTests:x86_64+0x1e7c) #2 partial apply for closure #1 in CounterTests.testIncrementConcurrent() <compiler-generated> (MobileDevSummitTests:x86_64+0x332d) #3 thunk for @escaping @callee_guaranteed () -> () <compiler-generated> (MobileDevSummitTests:x86_64+0x1ef2) #4 __tsan::invoke_and_release_block(void*) <null>:2 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x7f3eb) #5 _dispatch_client_callout <null>:2 (libdispatch.dylib:x86_64+0x2a39) Location is heap block of size 24 at 0x7b080029af40 allocated by main thread: #0 __sanitizer_mz_malloc <null>:2 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x5583c) #1 _malloc_zone_malloc_instrumented_or_legacy <null>:2 (libsystem_malloc.dylib:x86_64+0x1742f) #2 CounterTests.testIncrementConcurrent() CounterTests.swift:8 (MobileDevSummitTests:x86_64+0x18fa) #3 @objc CounterTests.testIncrementConcurrent() <compiler-generated> (MobileDevSummitTests:x86_64+0x2151) #4 __invoking___ <null>:2 (CoreFoundation:x86_64+0x12c31b) #5 main MobileDevSummitApp.swift (MobileDevSummit:x86_64+0x100006db5) Thread T8 (tid=2045181, running) is a GCD worker thread Thread T9 (tid=2045180, running) is a GCD worker thread SUMMARY: ThreadSanitizer: data race Counter.swift:8 in Counter.increment() ================== Test session results, code coverage, and logs: /Users/philipphofmann/Library/Developer/Xcode/DerivedData/MobileDevSummit-cqfkhtwtenpewocwjipnmnsajuwd/Logs/Test/Test- MobileDevSummit-2022.11.03_15-35-43-+0100.xcresult ** TEST SUCCEEDED **
  19. #!/bin/bash set -euo pipefail xcodebuild [flags] -enableThreadSanitizer YES test |

    \ tee thread-sanitizer.log if grep -Fq "WARNING: ThreadSanitizer:" thread-sanitizer.log ; then else fi
  20. #!/bin/bash set -euo pipefail xcodebuild [flags] -enableThreadSanitizer YES test |

    \ tee thread-sanitizer.log if grep -Fq "WARNING: ThreadSanitizer:" thread-sanitizer.log ; then message="ThreadSanitizer found problems. Search for \"ThreadSanitizer\" in logs for more details." echo "$message" exit 1 else fi
  21. #!/bin/bash set -euo pipefail xcodebuild [flags] -enableThreadSanitizer YES test |

    \ tee thread-sanitizer.log if grep -Fq "WARNING: ThreadSanitizer:" thread-sanitizer.log ; then message="ThreadSanitizer found problems. Search for \"ThreadSanitizer\" in logs for more details." echo "$message" exit 1 else echo "ThreadSanitizer didn't find problems." exit 0 fi
  22. ▸ Analyzing YourClass.m ⚠ /Users/philipphofmann/git-repos/sample/Sources/YourClass.m:11:5: Returning 'self' while it is

    not set to the result of '[(super or self) init...]' [osx.cocoa.SelfInit] return self; ^~~~~~~~~~~ ▸ Analyze Succeeded
  23. ▸ Analyzing YourClass.m ⚠ /Users/philipphofmann/git-repos/sample/Sources/YourClass.m:11:5: Returning 'self' while it is

    not set to the result of '[(super or self) init...]' [osx.cocoa.SelfInit] return self; ^~~~~~~~~~~ ▸ Analyze Succeeded
  24. ▸ Analyzing YourClass.m ⚠ /Users/philipphofmann/git-repos/sample/Sources/YourClass.m:11:5: Returning 'self' while it is

    not set to the result of '[(super or self) init...]' [osx.cocoa.SelfInit] return self; ^~~~~~~~~~~ ▸ Analyze Succeeded Error 1
  25. pod lib lint -—platforms=iOS pod lib lint -—platforms=macOS pod lib

    lint -—platforms=tvOS pod lib lint --platforms=watchos Run in parallel
  26. let package = Package( name: "macOS-SPM-CommandLine", dependencies: [ .package( name:

    "Sentry", url: "https://github.com/getsentry/sentry-cocoa", .branch(“main") ) ], targets: [ .target( name: "macOS-SPM-CommandLine", dependencies: ["Sentry"], swiftSettings: [ .unsafeFlags(["-warnings-as-errors"]) ]) ] )
  27. sed

  28. + repositoryURL = "https://github.com/org/repo"; + requirement = { + kind

    = revision; + revision = __GITHUB_REVISION_PLACEHOLDER__; + }; Patchfile
  29. git clone https://github.com/other/repo.git git checkout 4e23cas curl “https://raw.githubusercontent.com/org/repo/${SHA}/your.patch" --output your.patch

    # Replace revision with SHA REPLACE="s/__GITHUB_REVISION_PLACEHOLDER__/${SHA}/g" sed -i '' $REPLACE your.patch CI
  30. git clone https://github.com/other/repo.git git checkout 4e23cas curl “https://raw.githubusercontent.com/org/repo/${SHA}/your.patch" --output your.patch

    # Replace revision with SHA REPLACE="s/__GITHUB_REVISION_PLACEHOLDER__/${SHA}/g" sed -i '' $REPLACE your.patch git apply your.patch CI
  31. git clone https://github.com/other/repo.git git checkout 4e23cas curl “https://raw.githubusercontent.com/org/repo/${SHA}/your.patch" --output your.patch

    # Replace revision with SHA REPLACE="s/__GITHUB_REVISION_PLACEHOLDER__/${SHA}/g" sed -i '' $REPLACE your.patch git apply your.patch xcodebuild [flags] test CI
  32. // WARNING START // This code is bulletproof. // Don't

    think you are smart and can // improve it. … // WARNING END
  33. #!/bin/bash set -euo pipefail ACTUAL=$(shasum -a 256 ./Sources/BulletProof.swift) EXPECTED="54a41f19... ./Sources/BulletProof.swift"

    if [ "$ACTUAL" = "$EXPECTED" ]; then echo "No changes in high risk files." exit 0 else fi
  34. #!/bin/bash set -euo pipefail ACTUAL=$(shasum -a 256 ./Sources/BulletProof.swift) EXPECTED="54a41f19... ./Sources/BulletProof.swift"

    if [ "$ACTUAL" = "$EXPECTED" ]; then echo "No changes in high risk files." exit 0 else text="Changes in high risk files. If your changes are intended please update the sha in ./no-changes-in-high-risk-files.sh." echo "$text" exit 1 fi
  35. Tips and Tricks 1. Better Test Logs 2. Flaky Tests

    3. Flaky CI 4. SwiftLint & Clang-Format 5. Xcode Thread Sanitizer 6. Xcode Analyze 7. Validate your Library 8. Borrowing Tests 9. High-Risk Files
  36. Checkout sentry-cocoa for seeing the CI tips in action. Level

    up your CI for iOS and macOS Philipp Hofmann 1. Better Test Logs 2. Flaky Tests 3. Flaky CI 4. SwiftLint & Clang-Format 5. Xcode Thread Sanitizer 6. Xcode Analyze 7. Validate your Library 8. Borrowing Tests 9. High-Risk Files