From f20b5df43a75cf141cef45276ef62623a1623dd5 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Sat, 19 Oct 2024 20:21:13 -0700 Subject: [PATCH] Create Expectation --- .editorconfig | 19 +++ .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE/feature_request.md | 20 +++ .github/workflows/ci.yml | 93 +++++++++++++ .swiftformat | 17 +++ .../contents.xcworkspacedata | 7 + .../contents.xcworkspacedata | 7 + CLI/Package.resolved | 15 +++ CLI/Package.swift | 16 +++ Contributing.md | 15 +++ Package.swift | 38 ++++++ README.md | 46 ++++++- Scripts/build.swift | 115 +++++++++++++++++ Scripts/prepare-coverage-reports.sh | 34 +++++ Sources/TestingExpectation/Expectation.swift | 122 ++++++++++++++++++ .../ExpectationTests.swift | 106 +++++++++++++++ codecov.yml | 14 ++ lint.sh | 9 ++ 18 files changed, 695 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/ci.yml create mode 100644 .swiftformat create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 CLI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 CLI/Package.resolved create mode 100644 CLI/Package.swift create mode 100644 Contributing.md create mode 100644 Package.swift create mode 100755 Scripts/build.swift create mode 100755 Scripts/prepare-coverage-reports.sh create mode 100644 Sources/TestingExpectation/Expectation.swift create mode 100644 Tests/TestingExpectationTests/ExpectationTests.swift create mode 100644 codecov.yml create mode 100755 lint.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..353bf11 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +[*] +insert_final_newline = true +indent_size = 4 # We can remove this line if anyone has strong opinions. + +# swift +[*.swift] +indent_style = tab + +# sh +[*.sh] +indent_style = tab + +# graphql +[*.graphql] +indent_style = tab + +# documentation, utils +[*.{md,mdx,diff}] +trim_trailing_whitespace = false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e4f05ac --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: dfed +buy_me_a_coffee: dfed +custom: https://cash.app/$dan diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..eeeb503 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Goals** +What do you want this feature to accomplish? What are the effects of your change? + +**Non-Goals** +What aren’t you trying to accomplish? What are the boundaries of the proposed work? + +**Investigations** +What other solutions (if any) did you investigate? Why didn’t you choose them? + +**Design** +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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1860b1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + spm-16: + name: Build Xcode 16 + runs-on: macos-14 + strategy: + matrix: + platforms: [ + 'iOS_18,watchOS_11', + 'macOS_15,tvOS_18', + 'visionOS_2' + ] + fail-fast: false + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3.5' + bundler-cache: true + - name: Select Xcode Version + run: sudo xcode-select --switch /Applications/Xcode_16.app/Contents/Developer + - name: Download visionOS + if: matrix.platforms == 'visionOS_2' + run: | + sudo xcodebuild -runFirstLaunch + sudo xcrun simctl list + sudo xcodebuild -downloadPlatform visionOS + sudo xcodebuild -runFirstLaunch + - name: Build and Test Framework + run: Scripts/build.swift ${{ matrix.platforms }} + - name: Prepare Coverage Reports + run: ./Scripts/prepare-coverage-reports.sh + - name: Upload Coverage Reports + if: success() + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + spm-16-swift: + name: Swift Build Xcode 16 + runs-on: macos-14 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3.5' + bundler-cache: true + - name: Select Xcode Version + run: sudo xcode-select --switch /Applications/Xcode_16.app/Contents/Developer + - name: Build and Test Framework + run: xcrun swift test -c release -Xswiftc -enable-testing + linux: + name: "Build and Test on Linux" + runs-on: ubuntu-24.04 + container: swift:6.0 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Build and Test Framework + run: swift test -c release --enable-code-coverage -Xswiftc -enable-testing + - name: Prepare Coverage Reports + run: | + 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 + - name: Upload Coverage Reports + if: success() + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + readme-validation: + name: Check Markdown links + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Validate Markdown + uses: gaurav-nelson/github-action-markdown-link-check@v1 + lint-swift: + name: Lint Swift + runs-on: ubuntu-latest + container: swift:6.0 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Lint Swift + run: swift run --package-path CLI swiftformat --swiftversion 6.0 . --lint diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..0b539aa --- /dev/null +++ b/.swiftformat @@ -0,0 +1,17 @@ +# format options +--indent tab +--modifierorder nonisolated,open,public,internal,fileprivate,private,private(set),final,override,required +--ranges no-space +--extensionacl on-declarations +--funcattributes prev-line +--typeattributes prev-line +--storedvarattrs same-line +--hexgrouping none +--decimalgrouping 3 + +# rules +--enable isEmpty +--enable wrapEnumCases +--enable wrapMultilineStatementBraces +--disable consistentSwitchCaseSpacing +--disable blankLineAfterSwitchCase diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CLI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/CLI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/CLI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CLI/Package.resolved b/CLI/Package.resolved new file mode 100644 index 0000000..8226112 --- /dev/null +++ b/CLI/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "b043e9190ddd3045375d40ccb608edf659c48e3b9f8e2ee40cc489c8ec3f496d", + "pins" : [ + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "ab6844edb79a7b88dc6320e6cee0a0db7674dac3", + "version" : "0.54.5" + } + } + ], + "version" : 3 +} diff --git a/CLI/Package.swift b/CLI/Package.swift new file mode 100644 index 0000000..f2273e5 --- /dev/null +++ b/CLI/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "CLI", + platforms: [ + .macOS(.v14), + ], + products: [], + dependencies: [ + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.5"), + ], + targets: [] +) diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 0000000..2c19c5b --- /dev/null +++ b/Contributing.md @@ -0,0 +1,15 @@ +### One issue or bug per Pull Request + +Keep your Pull Requests small. Small PRs are easier to reason about which makes them significantly more likely to get merged. + +### Issues before features + +If you want to add a feature, please file an [Issue](../../issues) first. An Issue gives us the opportunity to discuss the requirements and implications of a feature with you before you start writing code. + +### Backwards compatibility + +Respect the minimum deployment target. If you are adding code that uses new APIs, make sure to prevent older clients from crashing or misbehaving. Our CI runs against our minimum deployment targets, so you will not get a green build unless your code is backwards compatible. + +### Forwards compatibility + +Please do not write new code using deprecated APIs. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..6e9c292 --- /dev/null +++ b/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "swift-testing-expectation", + platforms: [ + .macOS(.v13), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9), + .macCatalyst(.v16), + .visionOS(.v1), + ], + products: [ + .library( + name: "TestingExpectation", + targets: ["TestingExpectation"] + ), + ], + targets: [ + .target( + name: "TestingExpectation", + dependencies: [], + swiftSettings: [ + .swiftLanguageMode(.v6), + ] + ), + .testTarget( + name: "TestingExpectationTests", + dependencies: ["TestingExpectation"], + swiftSettings: [ + .swiftLanguageMode(.v6), + ] + ), + ] +) diff --git a/README.md b/README.md index 5cddce5..da7a9e6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ # swift-testing-expectation -Makes it easy to create an asynchronous expectation in Swift Testing +[![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) +[![codecov](https://codecov.io/gh/dfed/swift-testing-expectation/branch/main/graph/badge.svg?token=nZBHcZZ63F)](https://codecov.io/gh/dfed/swift-testing-expectation) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://spdx.org/licenses/MIT.html) +[![](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) +[![](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) + +Making it easy to create an asynchronous expectation in Swift Testing + +## Testing with asynchronous expectations + +The [Swift Testing](https://developer.apple.com/documentation/testing/testing-asynchronous-code) vends a [confirmation](https://developer.apple.com/documentation/testing/confirmation(_:expectedcount:isolation:sourcelocation:_:)) 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. + +The `Expectation` vended from this library fills that gap: + +```swift +@Test func testMethodCallEventuallyTriggersClosure() async { + let expectation = Expectation() + + systemUnderTest.closure = { expectation.fulfill() } + systemUnderTest.method() + + await expectation.fulfillment(within: .seconds(5)) +} +``` + +## Requirements + +* Xcode 16.0 or later. +* iOS 16 or later. +* tvOS 16 or later. +* watchOS 9 or later. +* macOS 13 or later. +* Swift 5.10 or later. + +## Installation + +### Swift Package Manager + +To install swift-async-queue 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: + +```swift +dependencies: [ + .package(url: "https://github.com/dfed/swift-testing-expectation", from: "0.1.0"), +] +``` diff --git a/Scripts/build.swift b/Scripts/build.swift new file mode 100755 index 0000000..7eb037c --- /dev/null +++ b/Scripts/build.swift @@ -0,0 +1,115 @@ +#!/usr/bin/env swift + +import Foundation + +// Usage: build.swift platforms + +func execute(commandPath: String, arguments: [String]) throws { + let task = Process() + task.launchPath = commandPath + task.arguments = arguments + print("Launching command: \(commandPath) \(arguments.joined(separator: " "))") + task.launch() + task.waitUntilExit() + guard task.terminationStatus == 0 else { + throw TaskError.code(task.terminationStatus) + } +} + +enum TaskError: Error { + case code(Int32) +} + +enum Platform: String, CaseIterable, CustomStringConvertible { + case iOS_18 + case tvOS_18 + case macOS_15 + case macCatalyst_15 + case watchOS_11 + case visionOS_2 + + var destination: String { + switch self { + case .iOS_18: + "platform=iOS Simulator,OS=18.0,name=iPad Pro (12.9-inch) (6th generation)" + + case .tvOS_18: + "platform=tvOS Simulator,OS=18.0,name=Apple TV" + + case .macOS_15, + .macCatalyst_15: + "platform=OS X" + + case .watchOS_11: + "OS=11.0,name=Apple Watch Series 7 (45mm)" + + case .visionOS_2: + "OS=2.0,name=Apple Vision Pro" + } + } + + var sdk: String { + switch self { + case .iOS_18: + "iphonesimulator" + + case .tvOS_18: + "appletvsimulator" + + case .macOS_15, + .macCatalyst_15: + "macosx15.0" + + case .watchOS_11: + "watchsimulator" + + case .visionOS_2: + "xrsimulator" + } + } + + var derivedDataPath: String { + ".build/derivedData/" + description + } + + var description: String { + rawValue + } +} + +guard CommandLine.arguments.count > 1 else { + print("Usage: build.swift platforms") + throw TaskError.code(1) +} + +let rawPlatforms = CommandLine.arguments[1].components(separatedBy: ",") + +for rawPlatform in rawPlatforms { + guard let platform = Platform(rawValue: rawPlatform) else { + print("Received unknown platform type \(rawPlatform)") + print("Possible platform types are: \(Platform.allCases)") + throw TaskError.code(1) + } + + var xcodeBuildArguments = [ + "-scheme", "swift-testing-expectation", + "-sdk", platform.sdk, + "-derivedDataPath", platform.derivedDataPath, + "-PBXBuildsContinueAfterErrors=0", + "OTHER_SWIFT_FLAGS=-warnings-as-errors", + ] + + if !platform.destination.isEmpty { + xcodeBuildArguments.append("-destination") + xcodeBuildArguments.append(platform.destination) + } + xcodeBuildArguments.append("-enableCodeCoverage") + xcodeBuildArguments.append("YES") + xcodeBuildArguments.append("build") + xcodeBuildArguments.append("test") + xcodeBuildArguments.append("-test-iterations") + xcodeBuildArguments.append("100") + xcodeBuildArguments.append("-run-tests-until-failure") + + try execute(commandPath: "/usr/bin/xcodebuild", arguments: xcodeBuildArguments) +} diff --git a/Scripts/prepare-coverage-reports.sh b/Scripts/prepare-coverage-reports.sh new file mode 100755 index 0000000..5ecdeff --- /dev/null +++ b/Scripts/prepare-coverage-reports.sh @@ -0,0 +1,34 @@ +#!/bin/zsh -l +set -e + +function exportlcov() { + build_type=$1 + executable_name=$2 + + executable=$(find "${directory}" -type f -name $executable_name) + profile=$(find "${directory}" -type f -name 'Coverage.profdata') + output_file_name="$executable_name.lcov" + + can_proceed=true + if [[ $build_type == watchOS* ]]; then + echo "\tAborting creation of $output_file_name – watchOS not supported." + elif [[ -z $profile ]]; then + echo "\tAborting creation of $output_file_name – no profile found." + elif [[ -z $executable ]]; then + echo "\tAborting creation of $output_file_name – no executable found." + else + output_dir=".build/artifacts/$build_type" + mkdir -p $output_dir + + output_file="$output_dir/$output_file_name" + echo "\tExporting $output_file" + xcrun llvm-cov export -format="lcov" $executable -instr-profile $profile > $output_file + fi +} + +for directory in $(git rev-parse --show-toplevel)/.build/derivedData/*/; do + build_type=$(basename $directory) + echo "Finding coverage information for $build_type" + + exportlcov $build_type 'AsyncQueueTests' +done diff --git a/Sources/TestingExpectation/Expectation.swift b/Sources/TestingExpectation/Expectation.swift new file mode 100644 index 0000000..df2ac13 --- /dev/null +++ b/Sources/TestingExpectation/Expectation.swift @@ -0,0 +1,122 @@ +// MIT License +// +// Copyright (c) 2024 Dan Federman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Testing + +public actor Expectation { + // MARK: Initialization + + public init( + expectedCount: UInt = 1 + ) { + self.init( + expectedCount: expectedCount, + expect: { fulfilledWithExpectedCount, comment, sourceLocation in + #expect(fulfilledWithExpectedCount, comment, sourceLocation: sourceLocation) + } + ) + } + + init( + expectedCount: UInt, + expect: @escaping (Bool, Comment?, SourceLocation) -> Void + ) { + self.expectedCount = expectedCount + self.expect = expect + } + + // MARK: Public + + public func fulfillment( + within duration: Duration, + filePath: String = #filePath, + fileID: String = #fileID, + line: Int = #line, + column: Int = #column + ) async { + guard !isComplete else { return } + let wait = Task { + try await Task.sleep(for: duration) + expect(isComplete, "Expectation not fulfilled within \(duration)", .init( + fileID: filePath, + filePath: filePath, + line: line, + column: column + )) + } + waits.append(wait) + try? await wait.value + } + + @discardableResult + nonisolated + public func fulfill( + filePath: String = #filePath, + fileID: String = #fileID, + line: Int = #line, + column: Int = #column + ) -> Task { + Task { + await self._fulfill( + filePath: filePath, + fileID: fileID, + line: line, + column: column + ) + } + } + + // MARK: Private + + private var waits = [Task]() + private var fulfillCount: UInt = 0 + private var isComplete: Bool { + expectedCount <= fulfillCount + } + + private let expectedCount: UInt + private let expect: (Bool, Comment?, SourceLocation) -> Void + + private func _fulfill( + filePath: String, + fileID: String, + line: Int, + column: Int + ) { + fulfillCount += 1 + guard isComplete else { return } + expect( + expectedCount == fulfillCount, + "Expected \(expectedCount) calls to `fulfill()`. Received \(fulfillCount).", + .init( + fileID: filePath, + filePath: filePath, + line: line, + column: column + ) + ) + for wait in waits { + wait.cancel() + } + waits = [] + } +} diff --git a/Tests/TestingExpectationTests/ExpectationTests.swift b/Tests/TestingExpectationTests/ExpectationTests.swift new file mode 100644 index 0000000..d926755 --- /dev/null +++ b/Tests/TestingExpectationTests/ExpectationTests.swift @@ -0,0 +1,106 @@ +// MIT License +// +// Copyright (c) 2024 Dan Federman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Testing + +@testable import TestingExpectation + +struct ExpectationTests { + @Test + func test_fulfill_triggersExpectation() async { + await confirmation { confirmation in + let systemUnderTest = Expectation( + expectedCount: 1, + expect: { expectation, _, _ in + #expect(expectation) + confirmation() + } + ) + await systemUnderTest.fulfill().value + } + } + + @Test + func test_fulfill_triggersExpectationOnceWhenCalledTwiceAndExpectedCountIsTwo() async { + await confirmation { confirmation in + let systemUnderTest = Expectation( + expectedCount: 2, + expect: { expectation, _, _ in + #expect(expectation) + confirmation() + } + ) + await systemUnderTest.fulfill().value + await systemUnderTest.fulfill().value + } + } + + @Test + func test_fulfill_triggersExpectationWhenExpectedCountIsZero() async { + await confirmation { confirmation in + let systemUnderTest = Expectation( + expectedCount: 0, + expect: { expectation, _, _ in + #expect(!expectation) + confirmation() + } + ) + await systemUnderTest.fulfill().value + } + } + + @Test + func test_fulfillment_doesNotWaitIfAlreadyFulfilled() async { + let systemUnderTest = Expectation(expectedCount: 0) + await systemUnderTest.fulfillment(within: .seconds(10)) + } + + @MainActor // Global actor ensures Task ordering. + @Test + func test_fulfillment_waitsForFulfillment() async { + let systemUnderTest = Expectation(expectedCount: 1) + var hasFulfilled = false + let wait = Task { + await systemUnderTest.fulfillment(within: .seconds(10)) + #expect(hasFulfilled) + } + Task { + systemUnderTest.fulfill() + hasFulfilled = true + } + await wait.value + } + + @Test + func test_fulfillment_triggersFalseExpectationWhenItTimesOut() async { + await confirmation { confirmation in + let systemUnderTest = Expectation( + expectedCount: 1, + expect: { expectation, _, _ in + #expect(!expectation) + confirmation() + } + ) + await systemUnderTest.fulfillment(within: .zero) + } + } +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..cc762d7 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,14 @@ +codecov: + require_ci_to_pass: yes + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: no + +coverage: + status: + project: + default: + target: 100% + patch: off diff --git a/lint.sh b/lint.sh new file mode 100755 index 0000000..7a84952 --- /dev/null +++ b/lint.sh @@ -0,0 +1,9 @@ +#!/bin/zsh + +set -e + +pushd $(git rev-parse --show-toplevel) + +swift run --only-use-versions-from-resolved-file --package-path CLI --scratch-path .build -c release swiftformat --swiftversion 6.0 . + +popd