├── .editorconfig
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ └── feature_request.md
└── workflows
│ ├── ci.yml
│ └── codeql.yml
├── .gitignore
├── .ruby-version
├── .swiftformat
├── CLI
├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── contents.xcworkspacedata
├── Package.resolved
└── Package.swift
├── Contributing.md
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Package.swift
├── README.md
├── Scripts
├── build.swift
└── prepare-coverage-reports.sh
├── Sources
└── TestingExpectation
│ ├── Expectation.swift
│ └── Expectations.swift
├── TestingExpectation.podspec
├── Tests
└── TestingExpectationTests
│ ├── ExpectationTests.swift
│ └── ExpectationsTests.swift
├── codecov.yml
├── lint.sh
└── pyproject.toml
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | insert_final_newline = true
3 | indent_size = 4
4 |
5 | # swift
6 | [*.swift]
7 | indent_style = tab
8 |
9 | # sh
10 | [*.sh]
11 | indent_style = tab
12 |
13 | # documentation, utils
14 | [*.{md,mdx,diff}]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: dfed
2 | buy_me_a_coffee: dfed
3 | custom: https://cash.app/$dan
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Goals**
11 | What do you want this feature to accomplish? What are the effects of your change?
12 |
13 | **Non-Goals**
14 | What aren’t you trying to accomplish? What are the boundaries of the proposed work?
15 |
16 | **Investigations**
17 | What other solutions (if any) did you investigate? Why didn’t you choose them?
18 |
19 | **Design**
20 | What are you proposing? What are the details of your chosen design? Include an API overview, technical details, and (potentially) some example headers, along with anything else you think will be useful. This is where you sell the design to yourself and project maintainers.
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | schedule:
8 | # Runs at 00:00 UTC every Monday to ensure that CI does not bitrot.
9 | - cron: '0 0 * * 1'
10 | pull_request:
11 |
12 | jobs:
13 | spm-16:
14 | name: Build Xcode 16
15 | runs-on: macos-15
16 | strategy:
17 | matrix:
18 | platforms: [
19 | 'iOS_18,watchOS_11',
20 | 'macOS_15,tvOS_18',
21 | 'visionOS_2'
22 | ]
23 | fail-fast: false
24 | permissions:
25 | contents: read
26 | steps:
27 | - name: Checkout Repo
28 | uses: actions/checkout@v5
29 | - uses: ruby/setup-ruby@v1
30 | with:
31 | ruby-version: '3.3.5'
32 | bundler-cache: true
33 | - name: Select Xcode Version
34 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer
35 | - name: Download visionOS
36 | if: matrix.platforms == 'visionOS_2'
37 | run: |
38 | sudo xcodebuild -runFirstLaunch
39 | sudo xcrun simctl list
40 | sudo xcodebuild -downloadPlatform visionOS
41 | sudo xcodebuild -runFirstLaunch
42 | - name: Build and Test Framework
43 | run: Scripts/build.swift ${{ matrix.platforms }}
44 | - name: Prepare Coverage Reports
45 | run: ./Scripts/prepare-coverage-reports.sh
46 | - name: Upload Coverage Reports
47 | if: success()
48 | uses: codecov/codecov-action@v5
49 | with:
50 | fail_ci_if_error: true
51 | verbose: true
52 | spm-16-swift:
53 | name: Swift Build Xcode 16
54 | runs-on: macos-15
55 | permissions:
56 | contents: read
57 | steps:
58 | - name: Checkout Repo
59 | uses: actions/checkout@v5
60 | - uses: ruby/setup-ruby@v1
61 | with:
62 | ruby-version: '3.3.5'
63 | bundler-cache: true
64 | - name: Select Xcode Version
65 | run: sudo xcode-select --switch /Applications/Xcode_16.app/Contents/Developer
66 | - name: Build and Test Framework
67 | run: xcrun swift test -c release -Xswiftc -enable-testing
68 | pod-lint:
69 | name: Pod Lint
70 | runs-on: macos-15
71 | permissions:
72 | contents: read
73 | steps:
74 | - name: Checkout Repo
75 | uses: actions/checkout@v5
76 | - uses: ruby/setup-ruby@v1
77 | with:
78 | ruby-version: '3.3.5'
79 | bundler-cache: true
80 | - name: Select Xcode Version
81 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer
82 | - name: Download visionOS
83 | run: |
84 | sudo xcodebuild -runFirstLaunch
85 | sudo xcrun simctl list
86 | sudo xcodebuild -downloadPlatform visionOS
87 | sudo xcodebuild -runFirstLaunch
88 | - name: Lint Podspec
89 | run: bundle exec pod lib lint --verbose --fail-fast --swift-version=6.0
90 | linux-6-0:
91 | name: "Build and Test on Linux"
92 | runs-on: ubuntu-24.04
93 | container: swift:6.0
94 | permissions:
95 | contents: read
96 | steps:
97 | - name: Checkout Repo
98 | uses: actions/checkout@v5
99 | - name: Build and Test Framework
100 | run: swift test -c release --enable-code-coverage -Xswiftc -enable-testing
101 | - name: Prepare Coverage Reports
102 | run: |
103 | llvm-cov export -format="lcov" .build/x86_64-unknown-linux-gnu/release/swift-testing-expectationPackageTests.xctest -instr-profile .build/x86_64-unknown-linux-gnu/release/codecov/default.profdata > coverage.lcov
104 | - name: Install curl for Codecov
105 | run: |
106 | apt-get update
107 | apt-get install -y --no-install-recommends curl ca-certificates
108 | - name: Upload Coverage Reports
109 | if: success()
110 | uses: codecov/codecov-action@v5
111 | with:
112 | fail_ci_if_error: true
113 | verbose: true
114 | linux-6-1:
115 | name: "Build and Test on Linux"
116 | runs-on: ubuntu-24.04
117 | container: swift:6.1
118 | permissions:
119 | contents: read
120 | steps:
121 | - name: Checkout Repo
122 | uses: actions/checkout@v5
123 | - name: Build and Test Framework
124 | run: swift test -c release --enable-code-coverage -Xswiftc -enable-testing
125 | - name: Prepare Coverage Reports
126 | run: |
127 | llvm-cov export -format="lcov" .build/x86_64-unknown-linux-gnu/release/swift-testing-expectationPackageTests.xctest -instr-profile .build/x86_64-unknown-linux-gnu/release/codecov/default.profdata > coverage.lcov
128 | - name: Install curl for Codecov
129 | run: |
130 | apt-get update
131 | apt-get install -y --no-install-recommends curl ca-certificates
132 | - name: Upload Coverage Reports
133 | if: success()
134 | uses: codecov/codecov-action@v5
135 | with:
136 | fail_ci_if_error: true
137 | verbose: true
138 | readme-validation:
139 | name: Check Markdown links
140 | runs-on: ubuntu-latest
141 | permissions:
142 | contents: read
143 | steps:
144 | - name: Checkout Repo
145 | uses: actions/checkout@v5
146 | - name: Link Checker
147 | uses: AlexanderDokuchaev/md-dead-link-check@d5a37e0b14e5918605d22b34562532762ccb2e47 # v1.2.0
148 | lint-swift:
149 | name: Lint Swift
150 | runs-on: ubuntu-latest
151 | container: swift:6.0
152 | permissions:
153 | contents: read
154 | steps:
155 | - name: Checkout Repo
156 | uses: actions/checkout@v5
157 | - name: Lint Swift
158 | run: swift run --package-path CLI swiftformat . --lint
159 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL Advanced"
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 | schedule:
9 | - cron: '35 2 * * 0'
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze (${{ matrix.language }})
14 | runs-on: macOS-15
15 | permissions:
16 | security-events: write
17 | packages: read
18 | actions: read
19 | contents: read
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | include:
25 | - language: actions
26 | build-mode: none
27 | - language: ruby
28 | build-mode: none
29 | - language: swift
30 | build-mode: autobuild
31 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
32 | # Use `c-cpp` to analyze code written in C, C++ or both
33 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
34 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
35 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
36 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
37 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
38 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v5
42 | - name: Select Xcode Version
43 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer
44 |
45 | # Initializes the CodeQL tools for scanning.
46 | - name: Initialize CodeQL
47 | uses: github/codeql-action/init@v3
48 | with:
49 | languages: ${{ matrix.language }}
50 | build-mode: ${{ matrix.build-mode }}
51 | # If you wish to specify custom queries, you can do so here or in a config file.
52 | # By default, queries listed here will override any specified in a config file.
53 | # Prefix the list here with "+" to use these queries and those in the config file.
54 |
55 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
56 | # queries: security-extended,security-and-quality
57 |
58 | # If the analyze step fails for one of the languages you are analyzing with
59 | # "We were unable to automatically build your code", modify the matrix above
60 | # to set the build mode to "manual" for that language. Then modify this step
61 | # to build your code.
62 | # ℹ️ Command-line programs to run using the OS shell.
63 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
64 | - if: matrix.build-mode == 'manual'
65 | shell: bash
66 | run: |
67 | echo 'If you are using a "manual" build mode for one or more of the' \
68 | 'languages you are analyzing, replace this with the commands to build' \
69 | 'your code, for example:'
70 | echo ' make bootstrap'
71 | echo ' make release'
72 | exit 1
73 |
74 | - name: Perform CodeQL Analysis
75 | uses: github/codeql-action/analyze@v3
76 | with:
77 | category: "/language:${{matrix.language}}"
78 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | xcuserdata/
3 | .swiftpm
4 | .build/
5 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.3.5
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | # format options
2 | --indent tab
3 | --modifierorder nonisolated,open,public,internal,fileprivate,private,private(set),final,override,required,convenience
4 | --ranges no-space
5 | --extensionacl on-declarations
6 | --funcattributes prev-line
7 | --typeattributes prev-line
8 | --storedvarattrs same-line
9 | --hexgrouping none
10 | --decimalgrouping 3
11 |
12 | # rules
13 | --enable isEmpty
14 | --enable wrapEnumCases
15 | --enable wrapMultilineStatementBraces
16 | --disable consistentSwitchCaseSpacing
17 | --disable blankLineAfterSwitchCase
18 |
19 | # global
20 | --swiftversion 6.0
21 |
--------------------------------------------------------------------------------
/CLI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CLI/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "b2996d8b20521bfede6aa79472b75c70d7ac765721c6e529be7f5e3dc3d95b1d",
3 | "pins" : [
4 | {
5 | "identity" : "swiftformat",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/nicklockwood/SwiftFormat",
8 | "state" : {
9 | "revision" : "1d02d0f54a5123c3ef67084b318f4421427b7a51",
10 | "version" : "0.56.2"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/CLI/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "CLI",
8 | platforms: [
9 | .macOS(.v14),
10 | ],
11 | products: [],
12 | dependencies: [
13 | .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.56.1"),
14 | ],
15 | targets: []
16 | )
17 |
--------------------------------------------------------------------------------
/Contributing.md:
--------------------------------------------------------------------------------
1 | ### One issue or bug per Pull Request
2 |
3 | Keep your Pull Requests small. Small PRs are easier to reason about which makes them significantly more likely to get merged.
4 |
5 | ### Issues before features
6 |
7 | If you want to add a feature, please file an [Issue](https://github.com/dfed/swift-testing-expectation/issues) first. An Issue gives us the opportunity to discuss the requirements and implications of a feature with you before you start writing code.
8 |
9 | ### Backwards compatibility
10 |
11 | Respect the minimum deployment target. If you are adding code that uses new APIs, make sure to prevent older clients from crashing or misbehaving.
12 |
13 | ### Forwards compatibility
14 |
15 | Please do not write new code using deprecated APIs.
16 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | ruby '3.3.5'
2 |
3 | source 'https://rubygems.org'
4 |
5 | gem 'cocoapods', '~> 1.16.0'
6 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.7)
5 | base64
6 | nkf
7 | rexml
8 | activesupport (7.2.1.2)
9 | base64
10 | bigdecimal
11 | concurrent-ruby (~> 1.0, >= 1.3.1)
12 | connection_pool (>= 2.2.5)
13 | drb
14 | i18n (>= 1.6, < 2)
15 | logger (>= 1.4.2)
16 | minitest (>= 5.1)
17 | securerandom (>= 0.3)
18 | tzinfo (~> 2.0, >= 2.0.5)
19 | addressable (2.8.7)
20 | public_suffix (>= 2.0.2, < 7.0)
21 | algoliasearch (1.27.5)
22 | httpclient (~> 2.8, >= 2.8.3)
23 | json (>= 1.5.1)
24 | atomos (0.1.3)
25 | base64 (0.2.0)
26 | bigdecimal (3.1.8)
27 | claide (1.1.0)
28 | cocoapods (1.16.1)
29 | addressable (~> 2.8)
30 | claide (>= 1.0.2, < 2.0)
31 | cocoapods-core (= 1.16.1)
32 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
33 | cocoapods-downloader (>= 2.1, < 3.0)
34 | cocoapods-plugins (>= 1.0.0, < 2.0)
35 | cocoapods-search (>= 1.0.0, < 2.0)
36 | cocoapods-trunk (>= 1.6.0, < 2.0)
37 | cocoapods-try (>= 1.1.0, < 2.0)
38 | colored2 (~> 3.1)
39 | escape (~> 0.0.4)
40 | fourflusher (>= 2.3.0, < 3.0)
41 | gh_inspector (~> 1.0)
42 | molinillo (~> 0.8.0)
43 | nap (~> 1.0)
44 | ruby-macho (>= 2.3.0, < 3.0)
45 | xcodeproj (>= 1.26.0, < 2.0)
46 | cocoapods-core (1.16.1)
47 | activesupport (>= 5.0, < 8)
48 | addressable (~> 2.8)
49 | algoliasearch (~> 1.0)
50 | concurrent-ruby (~> 1.1)
51 | fuzzy_match (~> 2.0.4)
52 | nap (~> 1.0)
53 | netrc (~> 0.11)
54 | public_suffix (~> 4.0)
55 | typhoeus (~> 1.0)
56 | cocoapods-deintegrate (1.0.5)
57 | cocoapods-downloader (2.1)
58 | cocoapods-plugins (1.0.0)
59 | nap
60 | cocoapods-search (1.0.1)
61 | cocoapods-trunk (1.6.0)
62 | nap (>= 0.8, < 2.0)
63 | netrc (~> 0.11)
64 | cocoapods-try (1.2.0)
65 | colored2 (3.1.2)
66 | concurrent-ruby (1.3.4)
67 | connection_pool (2.4.1)
68 | drb (2.2.1)
69 | escape (0.0.4)
70 | ethon (0.16.0)
71 | ffi (>= 1.15.0)
72 | ffi (1.17.0)
73 | ffi (1.17.0-aarch64-linux-gnu)
74 | ffi (1.17.0-aarch64-linux-musl)
75 | ffi (1.17.0-arm-linux-gnu)
76 | ffi (1.17.0-arm-linux-musl)
77 | ffi (1.17.0-arm64-darwin)
78 | ffi (1.17.0-x86-linux-gnu)
79 | ffi (1.17.0-x86-linux-musl)
80 | ffi (1.17.0-x86_64-darwin)
81 | ffi (1.17.0-x86_64-linux-gnu)
82 | ffi (1.17.0-x86_64-linux-musl)
83 | fourflusher (2.3.1)
84 | fuzzy_match (2.0.4)
85 | gh_inspector (1.1.3)
86 | httpclient (2.8.3)
87 | i18n (1.14.6)
88 | concurrent-ruby (~> 1.0)
89 | json (2.7.5)
90 | logger (1.6.1)
91 | minitest (5.25.1)
92 | molinillo (0.8.0)
93 | nanaimo (0.4.0)
94 | nap (1.1.0)
95 | netrc (0.11.0)
96 | nkf (0.2.0)
97 | public_suffix (4.0.7)
98 | rexml (3.4.2)
99 | ruby-macho (2.5.1)
100 | securerandom (0.3.1)
101 | typhoeus (1.4.1)
102 | ethon (>= 0.9.0)
103 | tzinfo (2.0.6)
104 | concurrent-ruby (~> 1.0)
105 | xcodeproj (1.26.0)
106 | CFPropertyList (>= 2.3.3, < 4.0)
107 | atomos (~> 0.1.3)
108 | claide (>= 1.0.2, < 2.0)
109 | colored2 (~> 3.1)
110 | nanaimo (~> 0.4.0)
111 | rexml (>= 3.3.6, < 4.0)
112 |
113 | PLATFORMS
114 | aarch64-linux-gnu
115 | aarch64-linux-musl
116 | arm-linux-gnu
117 | arm-linux-musl
118 | arm64-darwin
119 | ruby
120 | x86-linux-gnu
121 | x86-linux-musl
122 | x86_64-darwin
123 | x86_64-linux-gnu
124 | x86_64-linux-musl
125 |
126 | DEPENDENCIES
127 | cocoapods (~> 1.16.0)
128 |
129 | RUBY VERSION
130 | ruby 3.3.5p100
131 |
132 | BUNDLED WITH
133 | 2.5.16
134 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Dan Federman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "swift-testing-expectation",
8 | platforms: [
9 | .macOS(.v13),
10 | .iOS(.v16),
11 | .tvOS(.v16),
12 | .watchOS(.v9),
13 | .macCatalyst(.v16),
14 | .visionOS(.v1),
15 | ],
16 | products: [
17 | .library(
18 | name: "TestingExpectation",
19 | targets: ["TestingExpectation"]
20 | ),
21 | ],
22 | targets: [
23 | .target(
24 | name: "TestingExpectation",
25 | dependencies: [],
26 | swiftSettings: [
27 | .swiftLanguageMode(.v6),
28 | ]
29 | ),
30 | .testTarget(
31 | name: "TestingExpectationTests",
32 | dependencies: ["TestingExpectation"],
33 | swiftSettings: [
34 | .swiftLanguageMode(.v6),
35 | ]
36 | ),
37 | ]
38 | )
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # swift-testing-expectation
2 | [](https://github.com/dfed/swift-testing-expectation/actions?query=workflow%3ACI+branch%3Amain)
3 | [](https://codecov.io/gh/dfed/swift-testing-expectation)
4 | [](https://spdx.org/licenses/MIT.html)
5 | [](https://swiftpackageindex.com/dfed/swift-testing-expectation)
6 | [](https://swiftpackageindex.com/dfed/swift-testing-expectation)
7 |
8 | Create an asynchronous expectation in Swift Testing
9 |
10 | ## Testing with asynchronous expectations
11 |
12 | The [Swift Testing](https://developer.apple.com/documentation/testing/testing-asynchronous-code) framework vends a [confirmation](https://developer.apple.com/documentation/testing/confirmation(_:expectedcount:isolation:sourcelocation:_:)-5mqz2#) method which enables testing asynchronous code. However unlike [XCTest](https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations)’s [XCTestExpectation](https://developer.apple.com/documentation/xctest/xctestexpectation), this `confirmation` must be confirmed before the confirmation’s `body` completes. Swift Testing has no out-of-the-box way to ensure that an expectation is fulfilled at some indeterminate point in the future.
13 |
14 | The `Expectation` vended from this library fills that gap:
15 |
16 | ```swift
17 | @Test func methodEventuallyTriggersClosure() async {
18 | let expectation = Expectation()
19 |
20 | systemUnderTest.closure = { expectation.fulfill() }
21 | systemUnderTest.method()
22 |
23 | await expectation.fulfillment(within: .seconds(5))
24 | }
25 | ```
26 |
27 | ### Waiting for multiple expectations
28 |
29 | The `Expectations` type vended from this library makes it easy to wait for multiple expectations:
30 |
31 | ```swift
32 | @Test func methodEventuallyTriggersClosures() async {
33 | let expectation1 = Expectation()
34 | let expectation2 = Expectation()
35 | let expectation3 = Expectation()
36 |
37 | systemUnderTest.closure1 = { expectation1.fulfill() }
38 | systemUnderTest.closure2 = { expectation2.fulfill() }
39 | systemUnderTest.closure3 = { expectation3.fulfill() }
40 | systemUnderTest.method()
41 |
42 | await Expectations(expectation1, expectation2, expectation3).fulfillment(within: .seconds(5))
43 | }
44 | ```
45 |
46 | ## Installation
47 |
48 | ### Swift Package Manager
49 |
50 | To install swift-testing-expectation in your project with [Swift Package Manager](https://github.com/apple/swift-package-manager), the following lines can be added to your `Package.swift` file:
51 |
52 | ```swift
53 | dependencies: [
54 | .package(url: "https://github.com/dfed/swift-testing-expectation", from: "0.1.0"),
55 | ]
56 | ```
57 |
58 | ### CocoaPods
59 |
60 | To install swift-testing-expectation in your project with [CocoaPods](https://blog.cocoapods.org/CocoaPods-Specs-Repo), add the following to your `Podfile`:
61 |
62 | ```
63 | pod 'TestingExpectation', '~> 0.1.0'
64 | ```
65 |
66 | ## Contributing
67 |
68 | I’m glad you’re interested in swift-testing-expectation, and I’d love to see where you take it. Please read the [contributing guidelines](Contributing.md) prior to submitting a Pull Request.
69 |
--------------------------------------------------------------------------------
/Scripts/build.swift:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env swift
2 |
3 | import Foundation
4 |
5 | // Usage: build.swift platforms
6 |
7 | func execute(commandPath: String, arguments: [String]) throws {
8 | let task = Process()
9 | task.executableURL = .init(filePath: commandPath)
10 | task.arguments = arguments
11 | print("Launching command: \(commandPath) \(arguments.joined(separator: " "))")
12 | try task.run()
13 | task.waitUntilExit()
14 | guard task.terminationStatus == 0 else {
15 | throw TaskError.code(task.terminationStatus)
16 | }
17 | }
18 |
19 | enum TaskError: Error {
20 | case code(Int32)
21 | }
22 |
23 | enum Platform: String, CaseIterable, CustomStringConvertible {
24 | case iOS_18
25 | case tvOS_18
26 | case macOS_15
27 | case macCatalyst_15
28 | case watchOS_11
29 | case visionOS_2
30 |
31 | var destination: String {
32 | switch self {
33 | case .iOS_18:
34 | "platform=iOS Simulator,OS=18.5,name=iPad (10th generation)"
35 |
36 | case .tvOS_18:
37 | "platform=tvOS Simulator,OS=18.5,name=Apple TV"
38 |
39 | case .macOS_15,
40 | .macCatalyst_15:
41 | "platform=OS X"
42 |
43 | case .watchOS_11:
44 | "OS=11.5,name=Apple Watch Series 10 (46mm)"
45 |
46 | case .visionOS_2:
47 | "OS=2.5,name=Apple Vision Pro"
48 | }
49 | }
50 |
51 | var sdk: String {
52 | switch self {
53 | case .iOS_18:
54 | "iphonesimulator"
55 |
56 | case .tvOS_18:
57 | "appletvsimulator"
58 |
59 | case .macOS_15,
60 | .macCatalyst_15:
61 | "macosx15.5"
62 |
63 | case .watchOS_11:
64 | "watchsimulator"
65 |
66 | case .visionOS_2:
67 | "xrsimulator"
68 | }
69 | }
70 |
71 | var derivedDataPath: String {
72 | ".build/derivedData/" + description
73 | }
74 |
75 | var description: String {
76 | rawValue
77 | }
78 | }
79 |
80 | guard CommandLine.arguments.count > 1 else {
81 | print("Usage: build.swift platforms")
82 | throw TaskError.code(1)
83 | }
84 |
85 | let rawPlatforms = CommandLine.arguments[1].components(separatedBy: ",")
86 |
87 | for rawPlatform in rawPlatforms {
88 | guard let platform = Platform(rawValue: rawPlatform) else {
89 | print("Received unknown platform type \(rawPlatform)")
90 | print("Possible platform types are: \(Platform.allCases)")
91 | throw TaskError.code(1)
92 | }
93 |
94 | var xcodeBuildArguments = [
95 | "-scheme", "swift-testing-expectation",
96 | "-sdk", platform.sdk,
97 | "-derivedDataPath", platform.derivedDataPath,
98 | "-PBXBuildsContinueAfterErrors=0",
99 | "OTHER_SWIFT_FLAGS=-warnings-as-errors",
100 | ]
101 |
102 | if !platform.destination.isEmpty {
103 | xcodeBuildArguments.append("-destination")
104 | xcodeBuildArguments.append(platform.destination)
105 | }
106 | xcodeBuildArguments.append("-enableCodeCoverage")
107 | xcodeBuildArguments.append("YES")
108 | xcodeBuildArguments.append("build")
109 | xcodeBuildArguments.append("test")
110 | xcodeBuildArguments.append("-test-iterations")
111 | xcodeBuildArguments.append("100")
112 | xcodeBuildArguments.append("-run-tests-until-failure")
113 |
114 | try execute(commandPath: "/usr/bin/xcodebuild", arguments: xcodeBuildArguments)
115 | }
116 |
--------------------------------------------------------------------------------
/Scripts/prepare-coverage-reports.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh -l
2 | set -e
3 |
4 | function exportlcov() {
5 | build_type=$1
6 | executable_name=$2
7 |
8 | executable=$(find "${directory}" -type f -name $executable_name)
9 | profile=$(find "${directory}" -type f -name 'Coverage.profdata')
10 | output_file_name="$executable_name.lcov"
11 |
12 | can_proceed=true
13 | if [[ $build_type == watchOS* ]]; then
14 | echo "\tAborting creation of $output_file_name – watchOS not supported."
15 | elif [[ -z $profile ]]; then
16 | echo "\tAborting creation of $output_file_name – no profile found."
17 | elif [[ -z $executable ]]; then
18 | echo "\tAborting creation of $output_file_name – no executable found."
19 | else
20 | output_dir=".build/artifacts/$build_type"
21 | mkdir -p $output_dir
22 |
23 | output_file="$output_dir/$output_file_name"
24 | echo "\tExporting $output_file"
25 | xcrun llvm-cov export -format="lcov" $executable -instr-profile $profile > $output_file
26 | fi
27 | }
28 |
29 | for directory in $(git rev-parse --show-toplevel)/.build/derivedData/*/; do
30 | build_type=$(basename $directory)
31 | echo "Finding coverage information for $build_type"
32 |
33 | exportlcov $build_type 'TestingExpectationTests'
34 | done
35 |
--------------------------------------------------------------------------------
/Sources/TestingExpectation/Expectation.swift:
--------------------------------------------------------------------------------
1 | // MIT License
2 | //
3 | // Copyright (c) 2024 Dan Federman
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import Testing
24 |
25 | public actor Expectation {
26 | // MARK: Initialization
27 |
28 | /// An expected outcome in an asynchronous test.
29 | /// - Parameters:
30 | /// - expectedCount: The number of times `fulfill()` must be called before the expectation is completely fulfilled.
31 | /// - requireAwaitingFulfillment: Controls whether `deinit` requires that a created expectation has its fulfillment awaited.
32 | public init(
33 | expectedCount: UInt = 1,
34 | requireAwaitingFulfillment: Bool = true,
35 | filePath: String = #filePath,
36 | fileID: String = #fileID,
37 | line: Int = #line,
38 | column: Int = #column
39 | ) {
40 | self.init(
41 | expectedCount: expectedCount,
42 | expect: { fulfilledWithExpectedCount, comment, sourceLocation in
43 | #expect(fulfilledWithExpectedCount, comment, sourceLocation: sourceLocation)
44 | },
45 | precondition: requireAwaitingFulfillment ? Swift.precondition : nil,
46 | filePath: filePath,
47 | fileID: fileID,
48 | line: line,
49 | column: column
50 | )
51 | }
52 |
53 | init(
54 | expectedCount: UInt,
55 | expect: @escaping @Sendable (Bool, Comment?, SourceLocation) -> Void,
56 | precondition: (@Sendable (@autoclosure () -> Bool, @autoclosure () -> String, StaticString, UInt) -> Void)? = nil,
57 | filePath: String = #filePath,
58 | fileID: String = #fileID,
59 | line: Int = #line,
60 | column: Int = #column
61 | ) {
62 | self.expectedCount = expectedCount
63 | self.expect = expect
64 | self.precondition = precondition
65 | createdSourceLocation = .init(
66 | fileID: fileID,
67 | filePath: filePath,
68 | line: line,
69 | column: column
70 | )
71 | }
72 |
73 | deinit {
74 | let fulfillmentAwaited = fulfillmentAwaited
75 | if let precondition {
76 | precondition(fulfillmentAwaited, "Expectation created at \(createdSourceLocation) was never awaited", #file, #line)
77 | }
78 | }
79 |
80 | // MARK: Public
81 |
82 | public func fulfillment(
83 | within duration: Duration,
84 | filePath: String = #filePath,
85 | fileID: String = #fileID,
86 | line: Int = #line,
87 | column: Int = #column
88 | ) async {
89 | fulfillmentAwaited = true
90 | guard !isComplete else { return }
91 | let wait = Task {
92 | try await Task.sleep(for: duration)
93 | expect(isComplete, "Expectation not fulfilled within \(duration)", .init(
94 | fileID: fileID,
95 | filePath: filePath,
96 | line: line,
97 | column: column
98 | ))
99 | }
100 | waits.append(wait)
101 | try? await wait.value
102 | }
103 |
104 | @discardableResult
105 | nonisolated
106 | public func fulfill(
107 | filePath: String = #filePath,
108 | fileID: String = #fileID,
109 | line: Int = #line,
110 | column: Int = #column
111 | ) -> Task {
112 | Task {
113 | await self._fulfill(
114 | filePath: filePath,
115 | fileID: fileID,
116 | line: line,
117 | column: column
118 | )
119 | }
120 | }
121 |
122 | // MARK: Private
123 |
124 | private var waits = [Task]()
125 | private var fulfillCount: UInt = 0
126 | private var isComplete: Bool {
127 | expectedCount <= fulfillCount
128 | }
129 |
130 | private var fulfillmentAwaited = false
131 |
132 | private let expectedCount: UInt
133 | private let expect: @Sendable (Bool, Comment?, SourceLocation) -> Void
134 | private let precondition: (@Sendable (@autoclosure () -> Bool, @autoclosure () -> String, StaticString, UInt) -> Void)?
135 | private let createdSourceLocation: SourceLocation
136 |
137 | private func _fulfill(
138 | filePath: String,
139 | fileID: String,
140 | line: Int,
141 | column: Int
142 | ) {
143 | fulfillCount += 1
144 | guard isComplete else { return }
145 | expect(
146 | expectedCount == fulfillCount,
147 | "Expected \(expectedCount) calls to `fulfill()`. Received \(fulfillCount).",
148 | .init(
149 | fileID: fileID,
150 | filePath: filePath,
151 | line: line,
152 | column: column
153 | )
154 | )
155 | for wait in waits {
156 | wait.cancel()
157 | }
158 | waits = []
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/Sources/TestingExpectation/Expectations.swift:
--------------------------------------------------------------------------------
1 | // MIT License
2 | //
3 | // Copyright (c) 2025 Dan Federman
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | public actor Expectations {
24 | // MARK: Initialization
25 |
26 | public init(_ expectations: [Expectation]) {
27 | self.expectations = expectations
28 | }
29 |
30 | public init(_ expectations: Expectation...) {
31 | self.init(expectations)
32 | }
33 |
34 | // MARK: Public
35 |
36 | public func fulfillment(
37 | within duration: Duration,
38 | filePath: String = #filePath,
39 | fileID: String = #fileID,
40 | line: Int = #line,
41 | column: Int = #column
42 | ) async {
43 | await withTaskGroup(of: Void.self) { taskGroup in
44 | for expectation in expectations {
45 | taskGroup.addTask {
46 | await expectation.fulfillment(
47 | within: duration,
48 | filePath: filePath,
49 | fileID: fileID,
50 | line: line,
51 | column: column
52 | )
53 | }
54 | }
55 | await taskGroup.waitForAll()
56 | }
57 | }
58 |
59 | // MARK: Private
60 |
61 | private let expectations: [Expectation]
62 | }
63 |
--------------------------------------------------------------------------------
/TestingExpectation.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'TestingExpectation'
3 | s.version = '0.1.4'
4 | s.license = 'MIT'
5 | s.summary = 'Create an asynchronous expectation in Swift Testing'
6 | s.homepage = 'https://github.com/dfed/swift-testing-expectation'
7 | s.authors = 'Dan Federman'
8 | s.source = { :git => 'https://github.com/dfed/swift-testing-expectation.git', :tag => s.version }
9 | s.swift_version = '6.0'
10 | s.source_files = 'Sources/**/*.{swift}'
11 | s.frameworks = 'Testing'
12 | s.ios.deployment_target = '16.0'
13 | s.tvos.deployment_target = '16.0'
14 | s.watchos.deployment_target = '9.0'
15 | s.macos.deployment_target = '13.0'
16 | s.visionos.deployment_target = '1'
17 | end
18 |
--------------------------------------------------------------------------------
/Tests/TestingExpectationTests/ExpectationTests.swift:
--------------------------------------------------------------------------------
1 | // MIT License
2 | //
3 | // Copyright (c) 2024 Dan Federman
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import Testing
24 |
25 | @testable import TestingExpectation
26 |
27 | struct ExpectationTests {
28 | @Test
29 | func fulfill_triggersExpectation() async {
30 | await confirmation { confirmation in
31 | let systemUnderTest = Expectation(
32 | expectedCount: 1,
33 | expect: { expectation, _, _ in
34 | #expect(expectation)
35 | confirmation()
36 | }
37 | )
38 | await systemUnderTest.fulfill().value
39 | }
40 | }
41 |
42 | @Test
43 | func fulfill_triggersExpectationOnceWhenCalledTwiceAndExpectedCountIsTwo() async {
44 | await confirmation { confirmation in
45 | let systemUnderTest = Expectation(
46 | expectedCount: 2,
47 | expect: { expectation, _, _ in
48 | #expect(expectation)
49 | confirmation()
50 | }
51 | )
52 | await systemUnderTest.fulfill().value
53 | await systemUnderTest.fulfill().value
54 | }
55 | }
56 |
57 | @Test
58 | func fulfill_triggersExpectationWhenExpectedCountIsZero() async {
59 | await confirmation { confirmation in
60 | let systemUnderTest = Expectation(
61 | expectedCount: 0,
62 | expect: { expectation, _, _ in
63 | #expect(!expectation)
64 | confirmation()
65 | }
66 | )
67 | await systemUnderTest.fulfill().value
68 | }
69 | }
70 |
71 | @Test
72 | func fulfillment_doesNotWaitIfAlreadyFulfilled() async {
73 | let systemUnderTest = Expectation(expectedCount: 0)
74 | await systemUnderTest.fulfillment(within: .seconds(10))
75 | }
76 |
77 | @MainActor // Global actor ensures Task ordering.
78 | @Test
79 | func fulfillment_waitsForFulfillment() async {
80 | let systemUnderTest = Expectation(expectedCount: 1, requireAwaitingFulfillment: false)
81 | var hasFulfilled = false
82 | let wait = Task {
83 | await systemUnderTest.fulfillment(within: .seconds(10))
84 | #expect(hasFulfilled)
85 | }
86 | Task {
87 | systemUnderTest.fulfill()
88 | hasFulfilled = true
89 | }
90 | await wait.value
91 | }
92 |
93 | @Test
94 | func fulfillment_triggersFalseExpectationWhenItTimesOut() async {
95 | await confirmation { confirmation in
96 | let systemUnderTest = Expectation(
97 | expectedCount: 1,
98 | expect: { expectation, _, _ in
99 | #expect(!expectation)
100 | confirmation()
101 | }
102 | )
103 | await systemUnderTest.fulfillment(within: .zero)
104 | }
105 | }
106 |
107 | @Test
108 | func deinit_triggersTrueExpectationWhenNotAwaited() async {
109 | let expect: @Sendable (Bool, Comment?, SourceLocation) -> Void = { _, _, _ in }
110 | expect(true, nil, #_sourceLocation) // Force code coverage to cover the empty closure.
111 | await confirmation { confirmation in
112 | let unmanagedSystemUnderTest = Unmanaged.passRetained(Expectation(
113 | expectedCount: 0,
114 | expect: expect,
115 | precondition: { condition, message, _, _ in
116 | _ = message() // Force code coverage to cover the creation of the message.
117 | #expect(condition())
118 | confirmation()
119 | }
120 | ))
121 | await unmanagedSystemUnderTest.takeUnretainedValue().fulfillment(within: .zero)
122 | unmanagedSystemUnderTest.release() // Force the system under test to deinit.
123 | }
124 | }
125 |
126 | @Test
127 | func deinit_triggersFalseExpectationWhenNotAwaited() async {
128 | let expect: @Sendable (Bool, Comment?, SourceLocation) -> Void = { _, _, _ in }
129 | expect(true, nil, #_sourceLocation) // Force code coverage to cover the empty closure.
130 | await confirmation { confirmation in
131 | _ = Expectation(
132 | expectedCount: 1,
133 | expect: expect,
134 | precondition: { condition, message, _, _ in
135 | _ = message() // Force code coverage to cover the creation of the message.
136 | #expect(!condition())
137 | confirmation()
138 | }
139 | )
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/Tests/TestingExpectationTests/ExpectationsTests.swift:
--------------------------------------------------------------------------------
1 | // MIT License
2 | //
3 | // Copyright (c) 2025 Dan Federman
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import Testing
24 |
25 | @testable import TestingExpectation
26 |
27 | struct ExpectationsTests {
28 | @Test
29 | func fulfillment_doesNotWaitIfAlreadyFulfilled() async {
30 | let expectation = Expectation(expectedCount: 0)
31 | await Expectations(expectation).fulfillment(within: .seconds(10))
32 | }
33 |
34 | @MainActor // Global actor ensures Task ordering.
35 | @Test
36 | func fulfillment_waitsForFulfillmentOfSingleExpectation() async {
37 | let expectation = Expectation(expectedCount: 1)
38 | var hasFulfilled = false
39 | let wait = Task {
40 | await Expectations(expectation).fulfillment(within: .seconds(10))
41 | #expect(hasFulfilled)
42 | }
43 | Task {
44 | expectation.fulfill()
45 | hasFulfilled = true
46 | }
47 | await wait.value
48 | }
49 |
50 | @MainActor // Global actor ensures Task ordering.
51 | @Test
52 | func fulfillment_waitsForFulfillmentOfMultipleExpectations() async {
53 | let expectation1 = Expectation(expectedCount: 1)
54 | let expectation2 = Expectation(expectedCount: 1)
55 | let expectation3 = Expectation(expectedCount: 1)
56 | var hasFulfilled = false
57 | let wait = Task {
58 | await Expectations(expectation1, expectation2, expectation3).fulfillment(within: .seconds(10))
59 | #expect(hasFulfilled)
60 | }
61 | Task {
62 | expectation1.fulfill()
63 | expectation2.fulfill()
64 | expectation3.fulfill()
65 | hasFulfilled = true
66 | }
67 | await wait.value
68 | }
69 |
70 | @Test
71 | func fulfillment_triggersFalseExpectationWhenSingleExpectationTimesOut() async {
72 | await confirmation { confirmation in
73 | let expectation = Expectation(
74 | expectedCount: 1,
75 | expect: { expectation, _, _ in
76 | #expect(!expectation)
77 | confirmation()
78 | }
79 | )
80 | let systemUnderTest = Expectations(expectation)
81 | await systemUnderTest.fulfillment(within: .zero)
82 | }
83 | }
84 |
85 | @Test
86 | func fulfillment_triggersFalseExpectationWhenSingleExpectationOfManyTimesOut() async {
87 | await confirmation(expectedCount: 3) { confirmation in
88 | let expectation1 = Expectation(
89 | expectedCount: 1,
90 | expect: { expectation, _, _ in
91 | #expect(!expectation)
92 | confirmation()
93 | }
94 | )
95 | let expectation2 = Expectation(
96 | expectedCount: 1,
97 | expect: { expectation, _, _ in
98 | #expect(expectation)
99 | confirmation()
100 | }
101 | )
102 | await expectation2.fulfill().value
103 | let expectation3 = Expectation(
104 | expectedCount: 1,
105 | expect: { expectation, _, _ in
106 | #expect(expectation)
107 | confirmation()
108 | }
109 | )
110 | await expectation3.fulfill().value
111 |
112 | let systemUnderTest = Expectations(expectation1, expectation2, expectation3)
113 | await systemUnderTest.fulfillment(within: .zero)
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: yes
3 |
4 | comment:
5 | layout: "reach,diff,flags,tree"
6 | behavior: default
7 | require_changes: no
8 |
9 | coverage:
10 | status:
11 | project:
12 | default:
13 | target: 100%
14 | patch: off
15 |
--------------------------------------------------------------------------------
/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | set -e
4 |
5 | pushd $(git rev-parse --show-toplevel)
6 |
7 | swift run --only-use-versions-from-resolved-file --package-path CLI --scratch-path .build -c release swiftformat .
8 |
9 | popd
10 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.md_dead_link_check]
2 | exclude_links = ["https://developer.apple.com/documentation/testing/confirmation(_:expectedcount:isolation:sourcelocation:_:)"]
3 |
--------------------------------------------------------------------------------