├── .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 | [![CI Status](https://img.shields.io/github/actions/workflow/status/dfed/swift-testing-expectation/ci.yml?branch=main)](https://github.com/dfed/swift-testing-expectation/actions?query=workflow%3ACI+branch%3Amain) 3 | [![codecov](https://codecov.io/gh/dfed/swift-testing-expectation/branch/main/graph/badge.svg?token=nZBHcZZ63F)](https://codecov.io/gh/dfed/swift-testing-expectation) 4 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://spdx.org/licenses/MIT.html) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdfed%2Fswift-testing-expectation%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/dfed/swift-testing-expectation) 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdfed%2Fswift-testing-expectation%2Fbadge%3Ftype%3Dplatforms)](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 | --------------------------------------------------------------------------------