All skills
avdlee avatar

/swift-concurrency

@35095d4

Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurrency-related compiler and linter warnings.

Use this Skill: https://skilld.dev/gh/avdlee/swift-concurrency-agent-skill/swift-concurrency

This session only. Nothing lands on disk.

referencestesting.md

≈3.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Testing Concurrent Code

Use this when:

  • You are writing async tests.
  • A test is flaky because of task scheduling or actor isolation.
  • You need to replace XCTest waiting APIs or verify deallocation.

Skip this file if:

  • You mainly need production ownership guidance. Use actors.md, tasks.md, or memory-management.md.

Jump to:

  • Swift Testing (Recommended)
  • Awaiting Async Callbacks
  • Setup and Teardown
  • Handling Flaky Tests
  • Swift Concurrency Extras
  • XCTest Patterns (Legacy)
  • Memory Management Tests
  • Testing Checklist

Recommendation: Use Swift Testing

Swift Testing is strongly recommended for new projects and tests. It provides:

  • Modern Swift syntax with macros
  • Better concurrency support
  • Cleaner test structure
  • More flexible test organization

XCTest patterns are included for legacy codebases.

Swift Testing Basics

Simple async test

@Test
@MainActor
func emptyQuery() async {
    let searcher = ArticleSearcher()
    await searcher.search("")
    #expect(searcher.results == ArticleSearcher.allArticles)
}

Key differences from XCTest:

  • @Test macro instead of XCTestCase
  • #expect instead of XCTAssert
  • Structs preferred over classes
  • No test prefix required

Testing with actors

@Test
@MainActor
func searchReturnsResults() async {
    let searcher = ArticleSearcher()
    await searcher.search("swift")
    #expect(!searcher.results.isEmpty)
}

Mark test with actor if system under test requires it.

Course Deep Dive: This topic is covered in detail in Lesson 11.2: Testing concurrent code using Swift Testing

Awaiting Async Callbacks

Using continuations

When testing unstructured tasks:

@Test
@MainActor
func searchTaskCompletes() async {
    let searcher = ArticleSearcher()
    
    await withCheckedContinuation { continuation in
        _ = withObservationTracking {
            searcher.results
        } onChange: {
            continuation.resume()
        }
        
        searcher.startSearchTask("swift")
    }
    
    #expect(searcher.results.count > 0)
}

Use when: Testing code that spawns unstructured tasks.

Using confirmations

For structured async code:

@Test
@MainActor
func searchTriggersObservation() async {
    let searcher = ArticleSearcher()
    
    await confirmation { confirm in
        _ = withObservationTracking {
            searcher.results
        } onChange: {
            confirm()
        }
        
        // Must await here for confirmation to work
        await searcher.search("swift")
    }
    
    #expect(!searcher.results.isEmpty)
}

Critical: Must await async work for confirmation to validate.

Setup and Teardown

Using init/deinit

@MainActor
final class DatabaseTests {
    let database: Database
    
    init() async throws {
        database = Database()
        await database.prepare()
    }
    
    deinit {
        // Synchronous cleanup only
    }
    
    @Test
    func insertsData() async throws {
        try await database.insert(item)
        #expect(await database.count() == 1)
    }
}

Limitation: deinit cannot call async methods.

Test Scoping Traits

For async teardown:

@MainActor
struct DatabaseTrait: SuiteTrait, TestTrait, TestScoping {
    func provideScope(
        for test: Test,
        testCase: Test.Case?,
        performing function: () async throws -> Void
    ) async throws {
        let database = Database()
        
        try await Environment.$database.withValue(database) {
            await database.prepare()
            try await function()
            await database.cleanup() // Async teardown
        }
    }
}

// Environment for task-local storage
@MainActor
struct Environment {
    @TaskLocal static var database = Database()
}

// Apply to suite
@Suite(DatabaseTrait())
@MainActor
final class DatabaseTests {
    @Test
    func insertsData() async throws {
        try await Environment.database.insert(item)
    }
}

// Or apply to individual test
@Test(DatabaseTrait())
func specificTest() async throws {
    // Test code
}

Use when: Need async cleanup after each test.

Handling Flaky Tests

Problem: Race conditions

@Test
@MainActor
func isLoadingState() async throws {
    let fetcher = ImageFetcher()
    
    let task = Task { try await fetcher.fetch(url) }
    
    // ❌ Flaky - may pass or fail
    #expect(fetcher.isLoading == true)
    
    try await task.value
    #expect(fetcher.isLoading == false)
}

Issue: Task may complete before we check isLoading.

Solution: Swift Concurrency Extras

import ConcurrencyExtras

@Test
@MainActor
func isLoadingState() async throws {
    try await withMainSerialExecutor {
        let fetcher = ImageFetcher { url in
            await Task.yield() // Allow test to check state
            return Data()
        }
        
        let task = Task { try await fetcher.fetch(url) }
        
        await Task.yield() // Switch to task
        
        #expect(fetcher.isLoading == true) // ✅ Reliable
        
        try await task.value
        #expect(fetcher.isLoading == false)
    }
}

Add package: https://github.com/pointfreeco/swift-concurrency-extras.git

Course Deep Dive: This topic is covered in detail in Lesson 11.3: Using Swift Concurrency Extras by Point-Free

Serial execution required

@Suite(.serialized)
@MainActor
final class ImageFetcherTests {
    // Tests run serially when using withMainSerialExecutor
}

Critical: Main serial executor doesn't work with parallel test execution.

XCTest Patterns (Legacy)

Basic async test

final class ArticleSearcherTests: XCTestCase {
    @MainActor
    func testEmptyQuery() async {
        let searcher = ArticleSearcher()
        await searcher.search("")
        XCTAssertEqual(searcher.results, ArticleSearcher.allArticles)
    }
}

Using expectations

@MainActor
func testSearchTask() async {
    let searcher = ArticleSearcher()
    let expectation = expectation(description: "Search complete")
    
    _ = withObservationTracking {
        searcher.results
    } onChange: {
        expectation.fulfill()
    }
    
    searcher.startSearchTask("swift")
    
    // Use fulfillment, not wait
    await fulfillment(of: [expectation], timeout: 10)
    
    XCTAssertEqual(searcher.results.count, 1)
}

Critical: Use await fulfillment(of:), not wait(for:) to avoid deadlocks.

Setup and teardown

final class DatabaseTests: XCTestCase {
    override func setUp() async throws {
        // Async setup
    }
    
    override func tearDown() async throws {
        // Async teardown
    }
}

Mark as async throws to call async methods.

Course Deep Dive: This topic is covered in detail in Lesson 11.1: Testing concurrent code using XCTest

Main serial executor for all tests

final class MyTests: XCTestCase {
    override func invokeTest() {
        withMainSerialExecutor {
            super.invokeTest()
        }
    }
}

Common Patterns

Testing @MainActor code

@Test
@MainActor
func viewModelUpdates() async {
    let viewModel = ViewModel()
    await viewModel.loadData()
    #expect(viewModel.items.count > 0)
}

Testing actors

@Test
func actorIsolation() async {
    let store = DataStore()
    await store.insert(item)
    let count = await store.count()
    #expect(count == 1)
}

Testing cancellation

@Test
func cancellationStopsWork() async throws {
    let processor = DataProcessor()
    
    let task = Task {
        try await processor.processLargeDataset()
    }
    
    task.cancel()
    
    do {
        try await task.value
        Issue.record("Should have thrown cancellation error")
    } catch is CancellationError {
        // Expected
    }
}

Testing with delays

@Test
func debouncedSearch() async throws {
    try await withMainSerialExecutor {
        let searcher = DebouncedSearcher()
        
        searcher.search("a")
        await Task.yield()
        
        searcher.search("ab")
        await Task.yield()
        
        searcher.search("abc")
        
        // Wait for debounce
        try await Task.sleep(for: .milliseconds(600))
        
        #expect(searcher.searchCount == 1) // Only last search executed
    }
}

Testing task groups

@Test
func taskGroupProcessesAll() async throws {
    let processor = BatchProcessor()
    
    let results = await withTaskGroup(of: Int.self) { group in
        for i in 1...5 {
            group.addTask { await processor.process(i) }
        }
        
        var collected: [Int] = []
        for await result in group {
            collected.append(result)
        }
        return collected
    }
    
    #expect(results.count == 5)
}

Testing Memory Management

Verify deallocation

@Test
func viewModelDeallocates() async {
    var viewModel: ViewModel? = ViewModel()
    weak var weakViewModel = viewModel
    
    viewModel?.startWork()
    viewModel = nil
    
    try? await Task.sleep(for: .milliseconds(100))
    
    #expect(weakViewModel == nil)
}

Detect retain cycles

@Test
func noRetainCycle() async {
    var manager: Manager? = Manager()
    weak var weakManager = manager
    
    manager?.startLongRunningTask()
    manager = nil
    
    #expect(weakManager == nil)
}

Best Practices

  1. Use Swift Testing for new code - modern, better concurrency support
  2. Mark tests with correct isolation - @MainActor when needed
  3. Use confirmations over continuations - when structured concurrency allows
  4. Serialize tests with main serial executor - avoid flaky tests
  5. Test cancellation explicitly - ensure proper cleanup
  6. Verify deallocation - catch retain cycles early
  7. Use Task.yield() strategically - control execution in tests
  8. Avoid sleep in tests - use continuations/confirmations instead
  9. Test actor isolation - verify thread safety
  10. Keep tests deterministic - avoid timing dependencies

Migration from XCTest

XCTest → Swift Testing

// XCTest
final class MyTests: XCTestCase {
    func testExample() async {
        XCTAssertEqual(value, expected)
    }
}

// Swift Testing
@Suite
struct MyTests {
    @Test
    func example() async {
        #expect(value == expected)
    }
}

Expectations → Confirmations

// XCTest
let expectation = expectation(description: "Done")
doWork { expectation.fulfill() }
await fulfillment(of: [expectation])

// Swift Testing
await confirmation { confirm in
    await doWork { confirm() }
}

Setup/Teardown → Traits

// XCTest
override func setUp() async throws {
    await prepare()
}

// Swift Testing
struct SetupTrait: TestTrait, TestScoping {
    func provideScope(
        for test: Test,
        testCase: Test.Case?,
        performing function: () async throws -> Void
    ) async throws {
        await prepare()
        try await function()
    }
}

Troubleshooting

Test hangs

Cause: Waiting for expectation that never fulfills.

Solution: Add timeout, verify observation tracking.

Flaky test

Cause: Race condition in unstructured task.

Solution: Use main serial executor + Task.yield().

Deadlock

Cause: Using wait(for:) in async context.

Solution: Use await fulfillment(of:) instead.

Confirmation fails

Cause: Not awaiting async work in confirmation block.

Solution: Add await before async calls.

Actor isolation error

Cause: Test not marked with required actor.

Solution: Add @MainActor or appropriate actor to test.

Common Mistakes Agents Make

  • Flaky intermediate-state assertions: Asserting isLoading == true immediately after creating a Task is a race condition — the task may not have started yet. Use withMainSerialExecutor + Task.yield() to control scheduling before asserting intermediate state.
  • Using Task.sleep as a synchronization primitive in tests instead of deterministic scheduling.
  • Asserting intermediate state without controlling scheduling: Always use withMainSerialExecutor when you need to observe state between task creation and completion. Note: withMainSerialExecutor does not work with parallel test execution — mark the suite @Suite(.serialized).
  • Reaching into isolated internals instead of testing public behavior.
  • Keeping both Swift Testing and XCTest versions of the same example unless they teach different migration paths.

Testing Checklist

  • Tests marked with correct isolation
  • Using Swift Testing (recommended)
  • Async methods properly awaited
  • Cancellation tested
  • Memory leaks checked
  • Race conditions handled
  • Timeouts appropriate
  • Flaky tests fixed with serial executor
  • Actor isolation verified
  • Cleanup in traits (not deinit)

Further Learning

For advanced testing patterns, real-world examples, and migration strategies:

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive instructions and reference material for diagnosing and fixing Swift Concurrency issues. It includes detailed guidance on Swift 6 migration, actor isolation, and task management. No security risks or malicious patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    1/16 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 35095d4. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
  • swift
  • concurrency
  • async-await
  • actors
  • sendable
  • main-actor
  • swift-6
  • migration
  • data-races
  • thread-safety

README badge

README badge for avdlee/swift-concurrency-agent-skill

Diagnose and fix Swift Concurrency issues including data races, isolation violations, and Sendable conformance problems. Refactors callback-based code to async/await and guides Swift 6 migration by analyzing project settings, determining isolation boundaries, and applying the smallest safe changes.

Generated from the current SKILL.md.

Does this skill help with Swift 6 migration?
Yes. The skill guides Swift 6 migration by analyzing language mode and strict concurrency settings from Package.swift or .pbxproj, then walking through diagnostics related to tasks, actors, @MainActor, Sendable, and data races.
What project settings does this skill check?
The skill analyzes Swift language mode, strict concurrency level, default isolation, and upcoming features from both SwiftPM (Package.swift) and Xcode (.pbxproj) to determine the correct fix for each diagnostic.
Does this skill refactor callback-based code to async/await?
Yes. The skill helps refactor legacy callback patterns to async/await, and routes to references/migration.md for detailed closure-to-async conversion strategies.
Will this skill recommend @MainActor as a blanket fix?
No. The skill requires justification for @MainActor isolation and avoids recommending it unless the code is truly UI-bound; it also favors structured concurrency over unstructured tasks.
Does this skill handle Core Data concurrency issues?
Yes. The skill addresses NSManagedObject sendability warnings and crossing context/actor boundaries, with routes to references/core-data.md for detailed solutions.

Generated from the current SKILL.md. These answers refresh after source changes.