Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5/x] Fix pre-built library evolution and testability #265

Merged
merged 3 commits into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Mockingbird.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
28887301278532AE001B92CF /* MockingbirdCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 285C8DF72779E2D200DE525A /* MockingbirdCommon.framework */; };
2894622626A2AF6F00044839 /* Mockingbird.h in Headers */ = {isa = PBXBuildFile; fileRef = 2894622526A2AF6F00044839 /* Mockingbird.h */; settings = {ATTRIBUTES = (Public, ); }; };
28950E2D251C4C82008EEE29 /* ProjectDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28950E2C251C4C82008EEE29 /* ProjectDescription.swift */; };
28967D09278BF3E600E523D9 /* MKBTestExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 28967D07278BF3E600E523D9 /* MKBTestExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; };
28967D0A278BF3E600E523D9 /* MKBTestExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 28967D08278BF3E600E523D9 /* MKBTestExpectation.m */; };
2896E51826A6E93A00124D02 /* StubbingContext+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2896E51726A6E93A00124D02 /* StubbingContext+ObjC.swift */; };
2896E51A26A6E97900124D02 /* DynamicStubbingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2896E51926A6E97900124D02 /* DynamicStubbingManager.swift */; };
2896E51D26A7E94400124D02 /* NSInvocation+MKBErrorObjectType.h in Headers */ = {isa = PBXBuildFile; fileRef = 2896E51B26A7E94400124D02 /* NSInvocation+MKBErrorObjectType.h */; };
Expand Down Expand Up @@ -646,6 +648,8 @@
28874F9226BF828800097529 /* InferableArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferableArgument.swift; sourceTree = "<group>"; };
2894622526A2AF6F00044839 /* Mockingbird.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mockingbird.h; sourceTree = "<group>"; };
28950E2C251C4C82008EEE29 /* ProjectDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectDescription.swift; sourceTree = "<group>"; };
28967D07278BF3E600E523D9 /* MKBTestExpectation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MKBTestExpectation.h; sourceTree = "<group>"; };
28967D08278BF3E600E523D9 /* MKBTestExpectation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MKBTestExpectation.m; sourceTree = "<group>"; };
2896E51726A6E93A00124D02 /* StubbingContext+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StubbingContext+ObjC.swift"; sourceTree = "<group>"; };
2896E51926A6E97900124D02 /* DynamicStubbingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicStubbingManager.swift; sourceTree = "<group>"; };
2896E51B26A7E94400124D02 /* NSInvocation+MKBErrorObjectType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+MKBErrorObjectType.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1226,6 +1230,7 @@
children = (
2894622526A2AF6F00044839 /* Mockingbird.h */,
287C4F4126A3688100A7E0D9 /* MKBMocking.h */,
28967D07278BF3E600E523D9 /* MKBTestExpectation.h */,
28843B2526AE710400AFB8DF /* MKBTestUtils.h */,
287C4F4D26A36DC000A7E0D9 /* MKBTypeFacade.h */,
);
Expand All @@ -1236,6 +1241,7 @@
isa = PBXGroup;
children = (
287C4F4226A3688100A7E0D9 /* MKBMocking.m */,
28967D08278BF3E600E523D9 /* MKBTestExpectation.m */,
28843B2626AE710400AFB8DF /* MKBTestUtils.m */,
287C4F4E26A36DC000A7E0D9 /* MKBTypeFacade.m */,
);
Expand Down Expand Up @@ -2062,6 +2068,7 @@
buildActionMask = 2147483647;
files = (
2894622626A2AF6F00044839 /* Mockingbird.h in Headers */,
28967D09278BF3E600E523D9 /* MKBTestExpectation.h in Headers */,
28571AB826A666070063AB83 /* MKBUnsignedCharInvocationHandler.h in Headers */,
28571A8C26A65F680063AB83 /* MKBBoolInvocationHandler.h in Headers */,
28843B2726AE710400AFB8DF /* MKBTestUtils.h in Headers */,
Expand Down Expand Up @@ -2743,6 +2750,7 @@
28FB7EC32789C14000125FDA /* Synchronized.swift in Sources */,
28571A8526A65D8A0063AB83 /* MKBLongInvocationHandler.m in Sources */,
OBJ_896 /* OrderedVerification.swift in Sources */,
28967D0A278BF3E600E523D9 /* MKBTestExpectation.m in Sources */,
28571AB526A664F70063AB83 /* MKBStructInvocationHandler.m in Sources */,
OBJ_897 /* TestFailure.swift in Sources */,
2896E51826A6E93A00124D02 /* StubbingContext+ObjC.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Mockingbird.xcodeproj/xcconfigs/FrameworkBase.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ENABLE_BITCODE = NO
ENABLE_TESTABILITY = YES
DEFINES_MODULE = YES
SKIP_INSTALL = YES
COPY_PHASE_STRIP = NO

// Compatibility
SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator
Expand Down
2 changes: 2 additions & 0 deletions MockingbirdFramework.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Pod::Spec.new do |s|
s.pod_target_xcconfig = {
'ENABLE_BITCODE' => 'NO',
'ENABLE_TESTABILITY' => 'YES',
'COPY_PHASE_STRIP' => 'NO',
'DEFINES_MODULE' => 'YES',
}

s.source_files = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,20 @@ inOrder(with: .noInvocationsAfter) {

### Verify Asynchronous Calls

Mocked methods that are invoked asynchronously can be verified using an `eventually` block which returns an `XCTestExpectation`.
Mocked methods that are invoked asynchronously can be verified using an `eventually` block which creates an `XCTestExpectation` and attaches it to the current `XCTestCase`.

```swift
DispatchQueue.main.async {
guard bird.canFly else { return }
bird.fly()
}

let expectation =
eventually {
verify(bird.canFly).wasCalled()
verify(bird.fly()).wasCalled()
}
eventually {
verify(bird.canFly).wasCalled()
verify(bird.fly()).wasCalled()
}

wait(for: [expectation], timeout: 1.0)
waitForExpectations(timeout: 1.0)
```

### Verify Overloaded Methods
Expand Down
10 changes: 9 additions & 1 deletion Sources/MockingbirdAutomation/Interop/Carthage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public enum Carthage {
case all = "all"
}

public enum BuildConfiguration: String {
case debug = "Debug"
case release = "Release"
}

public static func update(platforms: [Platform] = [.all], project: Path) throws {
try Subprocess("carthage", [
"update",
Expand All @@ -19,10 +24,13 @@ public enum Carthage {
], workingDirectory: project.parent()).run()
}

public static func build(platforms: [Platform] = [.all], project: Path) throws {
public static func build(platforms: [Platform] = [.all],
configuration: BuildConfiguration = .release,
project: Path) throws {
try Subprocess("carthage", [
"build",
"--platform", platforms.map({ $0.rawValue }).joined(separator: ","),
"--configuration", configuration.rawValue,
"--use-xcframeworks",
"--no-skip-current",
"--cache-builds",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "285C8EA4277DA59E00DE525A"
BuildableName = "MockingbirdAutomationTests.xctest"
BlueprintName = "MockingbirdAutomationTests"
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand All @@ -28,6 +42,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "285C8EA4277DA59E00DE525A"
BuildableName = "MockingbirdAutomationTests.xctest"
BlueprintName = "MockingbirdAutomationTests"
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28E2A567277E8F43002975B3"
BuildableName = "MockingbirdAutomation"
BuildableName = "MockingbirdAutomationCli"
BlueprintName = "MockingbirdAutomationCli"
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
Expand All @@ -28,24 +28,15 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "285C8EA4277DA59E00DE525A"
BuildableName = "MockingbirdAutomationTests.xctest"
BlueprintName = "MockingbirdAutomationTests"
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
useCustomWorkingDirectory = "YES"
customWorkingDirectory = "$(SRCROOT)"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
Expand All @@ -55,7 +46,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28E2A567277E8F43002975B3"
BuildableName = "MockingbirdAutomation"
BuildableName = "MockingbirdAutomationCli"
BlueprintName = "MockingbirdAutomationCli"
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
Expand All @@ -72,7 +63,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28E2A567277E8F43002975B3"
BuildableName = "MockingbirdAutomation"
BuildableName = "MockingbirdAutomationCli"
BlueprintName = "MockingbirdAutomationCli"
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@
ReferencedContainer = "container:Mockingbird.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#import <XCTest/XCTest.h>

NS_ASSUME_NONNULL_BEGIN

/// An expected outcome in an asynchronous test.
///
/// Library evolution as of Swift 5.5.2 breaks when publicly including any types from the `XCTest`
/// framework due to the `XCTest` class declaration. Using a bridged type that can be casted to and
/// from `XCTestExpectation` allows us to avoid a direct reference in the Swift module interface.
/// See https://github.com/birdrides/mockingbird/issues/242 for more information.
NS_SWIFT_NAME(TestExpectation)
@interface MKBTestExpectation : XCTestExpectation

/// Convert an `XCTestExpectation` to a `MKBTestExpectation`.
/// @param expectation An `XCTestExpectation` instance.
+ (instancetype)createFromExpectation:(XCTestExpectation *)expectation;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#import "MKBMocking.h"
#import "MKBTestExpectation.h"
#import "MKBTestUtils.h"
#import "MKBTypeFacade.h"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#import "../include/MKBTestExpectation.h"

@implementation MKBTestExpectation

+ (instancetype)createFromExpectation:(XCTestExpectation *)expectation
{
return (MKBTestExpectation *)expectation;
}

@end
94 changes: 59 additions & 35 deletions Sources/MockingbirdFramework/Verification/AsyncVerification.swift
Original file line number Diff line number Diff line change
@@ -1,50 +1,76 @@
import Foundation
import XCTest

/// Create a deferrable test expectation from a block containing verification calls.
///
/// Mocked methods that are invoked asynchronously can be verified using an `eventually` block which
/// returns an `XCTestExpectation`.
///
/// ```swift
/// DispatchQueue.main.async {
/// Tree(with: bird).shake()
/// }
///
/// let expectation =
/// eventually {
/// verify(bird.fly()).wasCalled()
/// verify(bird.chirp()).wasCalled()
/// }
///
/// wait(for: [expectation], timeout: 1.0)
/// ```
///
/// - Parameters:
/// - description: An optional description for the created `XCTestExpectation`.
/// - block: A block containing verification calls.
/// - Returns: An XCTestExpectation that fulfilles once all verifications in the block are met.
public func eventually(_ description: String? = nil,
_ block: () -> Void) -> XCTestExpectation {
return createAsyncContext(description: description, block: block)
public extension NSObject {
/// Waits for the test to satisfy an array of expectations.
///
/// - Parameters:
/// - expectations: An array of expectations that must be fulfilled.
/// - seconds: The number of seconds within which all expectations must be fulfilled.
/// - enforceOrderOfFulfillment: If `true`, the expectations specified by the expectations
/// parameter must be satisfied in the order they appear in the array.
func wait(for expectations: [TestExpectation],
timeout seconds: TimeInterval,
enforceOrder enforceOrderOfFulfillment: Bool = false) {
guard let testCase = self as? XCTestCase else {
fatalError("Should never be called outside of a test case")
}
testCase.wait(for: expectations.map({ $0 as XCTestExpectation }),
timeout: seconds,
enforceOrder: enforceOrderOfFulfillment)
}

/// Create a deferrable test expectation from a block containing verification calls.
///
/// Mocked methods that are invoked asynchronously can be verified using an `eventually` block
/// which creates an `XCTestExpectation` and attaches it to the current `XCTestCase`.
///
/// ```swift
/// DispatchQueue.main.async {
/// Tree(with: bird).shake()
/// }
///
/// eventually {
/// verify(bird.fly()).wasCalled()
/// verify(bird.chirp()).wasCalled()
/// }
///
/// waitForExpectations(timeout: 1.0)
/// ```
///
/// - Parameters:
/// - description: An optional description for the test expectation.
/// - block: A block containing verification calls.
/// - Returns: An XCTestExpectation that fulfilles once all verifications in the block are met.
@discardableResult
func eventually(_ description: String = "Async verification group",
_ block: () -> Void) -> TestExpectation {
let expectation: XCTestExpectation = {
guard let testCase = self as? XCTestCase else {
return XCTestExpectation(description: description)
}
return testCase.expectation(description: description)
}()
createAsyncContext(expectation: expectation, block: block)
return TestExpectation.create(from: expectation)
}
}

/// Internal helper for `eventually` async verification scopes.
/// 1. Creates an attributed `DispatchQueue` scope which collects all verifications.
/// 2. Observes invocations on each mock and fulfills the test expectation if there is a match.
func createAsyncContext(description: String?, block scope: () -> Void) -> XCTestExpectation {
let testExpectation = XCTestExpectation(description: description ?? "Async verification group")
func createAsyncContext(expectation: XCTestExpectation, block scope: () -> Void) {
let group = ExpectationGroup { group in

testExpectation.expectedFulfillmentCount = group.expectations.count + group.subgroups.count

expectation.expectedFulfillmentCount = group.countExpectations()
print(expectation.expectedFulfillmentCount)
print("asdasfdasfd")
group.expectations.forEach({ capturedExpectation in
let observer = InvocationObserver({ (invocation, mockingContext) -> Bool in
do {
try expect(mockingContext,
handled: capturedExpectation.invocation,
using: capturedExpectation.expectation)
testExpectation.fulfill()
expectation.fulfill()
return true
} catch {
return false
Expand All @@ -58,7 +84,7 @@ func createAsyncContext(description: String?, block scope: () -> Void) -> XCTest
let observer = InvocationObserver({ (invocation, mockingContext) -> Bool in
do {
try subgroup.verify()
testExpectation.fulfill()
expectation.fulfill()
return true
} catch {
return false
Expand All @@ -73,6 +99,4 @@ func createAsyncContext(description: String?, block scope: () -> Void) -> XCTest
queue.sync { scope() }

try? group.verify()

return testExpectation
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ class ExpectationGroup {
func addSubgroup(_ subgroup: ExpectationGroup) {
subgroups.append(subgroup)
}

func countExpectations() -> Int {
return expectations.count + subgroups.reduce(into: 0) { count, subgroup in
count += subgroup.countExpectations()
}
}
}

extension DispatchQueue {
Expand Down
Loading