From b885baa7042df8db9146ab98df0739f52fd55254 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Wed, 4 Jan 2017 10:49:18 +0100 Subject: [PATCH 01/11] Add basic interaction recording --- Dobby.xcodeproj/project.pbxproj | 12 +++++++ Dobby/Recorder.swift | 63 +++++++++++++++++++++++++++++++++ DobbyTests/RecorderSpec.swift | 30 ++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 Dobby/Recorder.swift create mode 100644 DobbyTests/RecorderSpec.swift diff --git a/Dobby.xcodeproj/project.pbxproj b/Dobby.xcodeproj/project.pbxproj index 2886fc3..fcbda15 100644 --- a/Dobby.xcodeproj/project.pbxproj +++ b/Dobby.xcodeproj/project.pbxproj @@ -12,6 +12,10 @@ DC3CFE791ADE895E004728C2 /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3CFE771ADE895E004728C2 /* SwiftExtensions.swift */; }; DC457AD91B5552CA00919032 /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC457AD81B5552CA00919032 /* Matcher.swift */; }; DC457ADA1B5552CA00919032 /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC457AD81B5552CA00919032 /* Matcher.swift */; }; + DC4D88381E1CECE100FAE93F /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88371E1CECE100FAE93F /* Recorder.swift */; }; + DC4D88391E1CECE100FAE93F /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88371E1CECE100FAE93F /* Recorder.swift */; }; + DC4D883B1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */; }; + DC4D883C1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */; }; DC7F6AFE1AD8291800CCBF6D /* Dobby.h in Headers */ = {isa = PBXBuildFile; fileRef = DC7F6AFD1AD8291800CCBF6D /* Dobby.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC7F6B041AD8291800CCBF6D /* Dobby.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7F6AF81AD8291800CCBF6D /* Dobby.framework */; }; DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */; }; @@ -72,6 +76,8 @@ /* Begin PBXFileReference section */ DC3CFE771ADE895E004728C2 /* SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; DC457AD81B5552CA00919032 /* Matcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; + DC4D88371E1CECE100FAE93F /* Recorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = ""; }; + DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecorderSpec.swift; sourceTree = ""; }; DC7F6AF81AD8291800CCBF6D /* Dobby.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Dobby.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC7F6AFC1AD8291800CCBF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DC7F6AFD1AD8291800CCBF6D /* Dobby.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Dobby.h; sourceTree = ""; }; @@ -188,6 +194,7 @@ DC7F6AFD1AD8291800CCBF6D /* Dobby.h */, DC457AD81B5552CA00919032 /* Matcher.swift */, DC7F6B1F1AD82D3800CCBF6D /* Mock.swift */, + DC4D88371E1CECE100FAE93F /* Recorder.swift */, DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */, DC3CFE761ADE895E004728C2 /* Extensions */, DC7F6AFB1AD8291800CCBF6D /* Supporting Files */, @@ -208,6 +215,7 @@ children = ( DCE4080D1B6161000009E0B4 /* MatcherSpec.swift */, DC7F6B241AD830A500CCBF6D /* MockSpec.swift */, + DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */, DC7F6B261AD830B500CCBF6D /* StubSpec.swift */, DC7F6B081AD8291800CCBF6D /* Supporting Files */, ); @@ -497,6 +505,7 @@ DC83356C1B56661900569709 /* Disposable.swift in Sources */, DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */, DC7F6B201AD82D3800CCBF6D /* Mock.swift in Sources */, + DC4D88391E1CECE100FAE93F /* Recorder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -506,6 +515,7 @@ files = ( DCE4080E1B6161000009E0B4 /* MatcherSpec.swift in Sources */, DC7F6B251AD830A500CCBF6D /* MockSpec.swift in Sources */, + DC4D883C1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, DC7F6B271AD830B500CCBF6D /* StubSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -519,6 +529,7 @@ DC83356D1B56661900569709 /* Disposable.swift in Sources */, DC8452401ADBF73C006AC5D4 /* Stub.swift in Sources */, DC84523F1ADBF73C006AC5D4 /* Mock.swift in Sources */, + DC4D88381E1CECE100FAE93F /* Recorder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -528,6 +539,7 @@ files = ( DCE4080F1B6161000009E0B4 /* MatcherSpec.swift in Sources */, DC8452421ADBF743006AC5D4 /* MockSpec.swift in Sources */, + DC4D883B1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, DC8452431ADBF743006AC5D4 /* StubSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Dobby/Recorder.swift b/Dobby/Recorder.swift new file mode 100644 index 0000000..5b85f9e --- /dev/null +++ b/Dobby/Recorder.swift @@ -0,0 +1,63 @@ +/// A timestamp in a logical clock. +public typealias Timestamp = UInt64 + +/// The current timestamp of the global clock. +internal var currentTimestamp: Timestamp = 0 + +/// A type that provides chronological access to recorded timestamps. +public protocol TimestampRecording: class { + /// Returns the recorded timestamps in chronological order. + var timestamps: [Timestamp] { get } +} + +/// An interaction, consisting of a timestamp and a value. +public struct Interaction { + /// The time when the interaction occurred. + public let timestamp: Timestamp + + /// The value of the interaction. + public let value: Value + + /// Creates a new interaction with the given value at the specified time. + public init(value: Value, at timestamp: Timestamp) { + self.timestamp = timestamp + self.value = value + } +} + +/// A type that provides chronological access to recorded interactions. +public protocol InteractionRecording: TimestampRecording { + /// The value type of recorded interactions. + associatedtype Value + + /// Returns the recorded interactions in chronological order. + var interactions: [Interaction] { get } +} + +public extension InteractionRecording { + public var timestamps: [Timestamp] { + return interactions.map({ interaction in + return interaction.timestamp + }) + } +} + +/// A recorder for interactions of the specified value type. +public final class Recorder: InteractionRecording { + public private(set) var interactions: [Interaction] = [] + + /// Creates a new recorder. + public init() { + + } + + /// Records an interaction with the given value at the current timestamp of + /// the global clock. + public func record(_ value: Value) { + let interaction = Interaction(value: value, at: currentTimestamp) + interactions.append(interaction) + + // Advance the global clock. + currentTimestamp += 1 + } +} diff --git a/DobbyTests/RecorderSpec.swift b/DobbyTests/RecorderSpec.swift new file mode 100644 index 0000000..1e28ec7 --- /dev/null +++ b/DobbyTests/RecorderSpec.swift @@ -0,0 +1,30 @@ +import Quick +import Nimble + +@testable import Dobby + +class RecorderSpec: QuickSpec { + override func spec() { + var recorder: Recorder! + + beforeEach { + recorder = Recorder() + } + + describe("Recording") { + it("appends an interaction to the recorder") { + let currentTimestamp = Dobby.currentTimestamp + + recorder.record(1) + + // Verify that the interaction has been appended. + expect(recorder.interactions).to(haveCount(1)) + expect(recorder.interactions[0].timestamp).to(equal(currentTimestamp)) + expect(recorder.interactions[0].value).to(equal(1)) + + // Verify that the global clock is advanced. + expect(Dobby.currentTimestamp).to(equal(currentTimestamp + 1)) + } + } + } +} From 8b2f9f6d06dae2fb5786246d56be89a6812e8d4f Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Thu, 5 Jan 2017 09:32:32 +0100 Subject: [PATCH 02/11] Record the source location for interactions --- Dobby/Recorder.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Dobby/Recorder.swift b/Dobby/Recorder.swift index 5b85f9e..c5d09d1 100644 --- a/Dobby/Recorder.swift +++ b/Dobby/Recorder.swift @@ -18,10 +18,19 @@ public struct Interaction { /// The value of the interaction. public let value: Value + /// The file in which the interaction occurred. + public let file: StaticString + + /// The line at which the interaction occurred. + public let line: UInt + /// Creates a new interaction with the given value at the specified time. - public init(value: Value, at timestamp: Timestamp) { + public init(value: Value, timestamp: Timestamp, file: StaticString, line: UInt) { self.timestamp = timestamp self.value = value + + self.file = file + self.line = line } } @@ -53,8 +62,8 @@ public final class Recorder: InteractionRecording { /// Records an interaction with the given value at the current timestamp of /// the global clock. - public func record(_ value: Value) { - let interaction = Interaction(value: value, at: currentTimestamp) + public func record(_ value: Value, file: StaticString = #file, line: UInt = #line) { + let interaction = Interaction(value: value, timestamp: currentTimestamp, file: file, line: line) interactions.append(interaction) // Advance the global clock. From 1782725d5b43c8171adc7cfab2b8c385f7ae6c89 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Thu, 5 Jan 2017 09:35:24 +0100 Subject: [PATCH 03/11] Make recording thread-safe --- Dobby/Recorder.swift | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Dobby/Recorder.swift b/Dobby/Recorder.swift index c5d09d1..6d05fcd 100644 --- a/Dobby/Recorder.swift +++ b/Dobby/Recorder.swift @@ -1,8 +1,23 @@ /// A timestamp in a logical clock. public typealias Timestamp = UInt64 -/// The current timestamp of the global clock. -internal var currentTimestamp: Timestamp = 0 +private var timestamp: Timestamp = 0 +private let timestampQueue = DispatchQueue(label: "com.trivago.dobby-timestampQueue", attributes: .concurrent) + +/// Returns the current timestamp of the global clock. +public var currentTimestamp: Timestamp { + return timestampQueue.sync(execute: { + return timestamp + }) +} + +/// Advances the current timestamp of the global clock and returns it. +public func nextTimestamp() -> Timestamp { + return timestampQueue.sync(flags: .barrier, execute: { + timestamp += 1 + return timestamp + }) +} /// A type that provides chronological access to recorded timestamps. public protocol TimestampRecording: class { @@ -53,7 +68,14 @@ public extension InteractionRecording { /// A recorder for interactions of the specified value type. public final class Recorder: InteractionRecording { - public private(set) var interactions: [Interaction] = [] + private var _interactions: [Interaction] = [] + private let _interactionsQueue = DispatchQueue(label: "com.trivago.dobby.recorder-interactionsQueue", attributes: .concurrent) + + public var interactions: [Interaction] { + return _interactionsQueue.sync(execute: { + return _interactions + }) + } /// Creates a new recorder. public init() { @@ -63,10 +85,9 @@ public final class Recorder: InteractionRecording { /// Records an interaction with the given value at the current timestamp of /// the global clock. public func record(_ value: Value, file: StaticString = #file, line: UInt = #line) { - let interaction = Interaction(value: value, timestamp: currentTimestamp, file: file, line: line) - interactions.append(interaction) - - // Advance the global clock. - currentTimestamp += 1 + _interactionsQueue.sync(flags: .barrier, execute: { + let interaction = Interaction(value: value, timestamp: nextTimestamp(), file: file, line: line) + _interactions.append(interaction) + }) } } From 2f474e47cf725a4708f6fb06276bbfb472eca635 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Fri, 6 Jan 2017 09:51:52 +0100 Subject: [PATCH 04/11] Rework interaction and value recording - `TimestampRecording` has been renamed to `InteractionRecording`, which now provides access to more than just the timestamp. It also includes a textual representation of each interaction as well as the file and line of occurrence. - `IntractionRecording` has been renamed to `ValueRecording`, providing chronological access to recorded interactions and their corresponding values. - The global logical clock is now thread-safe. - The recorder is now thread-safe. --- Dobby/Recorder.swift | 156 +++++++++++++++++++++++++--------- DobbyTests/RecorderSpec.swift | 12 +-- 2 files changed, 122 insertions(+), 46 deletions(-) diff --git a/Dobby/Recorder.swift b/Dobby/Recorder.swift index 6d05fcd..aab6bfd 100644 --- a/Dobby/Recorder.swift +++ b/Dobby/Recorder.swift @@ -1,8 +1,9 @@ /// A timestamp in a logical clock. public typealias Timestamp = UInt64 -private var timestamp: Timestamp = 0 -private let timestampQueue = DispatchQueue(label: "com.trivago.dobby-timestampQueue", attributes: .concurrent) +/// A thread-safe global logical clock. +fileprivate var timestamp: Timestamp = 0 +fileprivate let timestampQueue = DispatchQueue(label: "com.trivago.dobby-timestampQueue", attributes: .concurrent) /// Returns the current timestamp of the global clock. public var currentTimestamp: Timestamp { @@ -19,30 +20,24 @@ public func nextTimestamp() -> Timestamp { }) } -/// A type that provides chronological access to recorded timestamps. -public protocol TimestampRecording: class { - /// Returns the recorded timestamps in chronological order. - var timestamps: [Timestamp] { get } -} +/// A recorded interaction. +public struct Interaction: CustomStringConvertible { + public let description: String -/// An interaction, consisting of a timestamp and a value. -public struct Interaction { - /// The time when the interaction occurred. + /// The time when this interaction was recorded. public let timestamp: Timestamp - /// The value of the interaction. - public let value: Value - - /// The file in which the interaction occurred. + /// The file in which this interaction was recorded. public let file: StaticString - /// The line at which the interaction occurred. + /// The line at which this interaction was recorded. public let line: UInt - /// Creates a new interaction with the given value at the specified time. - public init(value: Value, timestamp: Timestamp, file: StaticString, line: UInt) { + /// Creates a new interaction with the given textual representation at the + /// specified time. + public init(description: String, timestamp: Timestamp, file: StaticString, line: UInt) { + self.description = description self.timestamp = timestamp - self.value = value self.file = file self.line = line @@ -50,44 +45,123 @@ public struct Interaction { } /// A type that provides chronological access to recorded interactions. -public protocol InteractionRecording: TimestampRecording { - /// The value type of recorded interactions. - associatedtype Value +public protocol InteractionRecording: class { + /// Returns the unique identifier for this object. + var objectIdentifier: ObjectIdentifier { get } - /// Returns the recorded interactions in chronological order. - var interactions: [Interaction] { get } + /// Returns a random access collection that provides access to the recorded + /// interactions in chronological order. + var interactions: AnyRandomAccessCollection { get } } -public extension InteractionRecording { - public var timestamps: [Timestamp] { - return interactions.map({ interaction in - return interaction.timestamp - }) +extension InteractionRecording { + public var objectIdentifier: ObjectIdentifier { + return ObjectIdentifier(self) } } -/// A recorder for interactions of the specified value type. -public final class Recorder: InteractionRecording { - private var _interactions: [Interaction] = [] - private let _interactionsQueue = DispatchQueue(label: "com.trivago.dobby.recorder-interactionsQueue", attributes: .concurrent) +/// A type-erased, hashable interaction recorder. The hash value and equality +/// operator are implemented using object identifiers. +internal final class AnyInteractionRecording: InteractionRecording, Hashable { + /// The wrapped interaction recorder. + private let base: InteractionRecording - public var interactions: [Interaction] { - return _interactionsQueue.sync(execute: { - return _interactions - }) + internal var objectIdentifier: ObjectIdentifier { + return base.objectIdentifier } + internal var interactions: AnyRandomAccessCollection { + return base.interactions + } + + internal var hashValue: Int { + return objectIdentifier.hashValue + } + + /// Creates a type-erased, hashable interaction recorder that wraps the + /// given instance. + internal init(_ base: InteractionRecording) { + self.base = base + } +} + +internal func == (lhs: AnyInteractionRecording, rhs: AnyInteractionRecording) -> Bool { + return lhs.objectIdentifier == rhs.objectIdentifier +} + +/// A type that provides chronological access to recorded interactions and +/// corresponding values. +public protocol ValueRecording: InteractionRecording { + /// The type of recorded values. + associatedtype Value + + /// Returns the value corresponding to the interaction at the given index. + func valueForInteraction(at index: Int) -> Value +} + +/// A recorded entry. +fileprivate struct Entry { + /// The recorded interaction. + fileprivate let interaction: Interaction + + /// The recorded value. + fileprivate let value: Value + + /// Creates a new entry with the given interaction and value. + fileprivate init(interaction: Interaction, value: Value) { + self.interaction = interaction + self.value = value + } + + /// Creates a new entry using the value's textual representation as + /// description for the interaction. + fileprivate init(value: Value, timestamp: Timestamp, file: StaticString, line: UInt) { + let interaction = Interaction(description: String(describing: value), timestamp: timestamp, file: file, line: line) + + self.init(interaction: interaction, value: value) + } +} + +/// A thread-safe recorder for interactions of the specified value type. +public final class Recorder { + fileprivate var entries: [Entry] = [] + fileprivate let entriesQueue = DispatchQueue(label: "com.trivago.dobby.recorder-entriesQueue", attributes: .concurrent) + /// Creates a new recorder. public init() { } - /// Records an interaction with the given value at the current timestamp of - /// the global clock. + /// Records an interaction with the given value. public func record(_ value: Value, file: StaticString = #file, line: UInt = #line) { - _interactionsQueue.sync(flags: .barrier, execute: { - let interaction = Interaction(value: value, timestamp: nextTimestamp(), file: file, line: line) - _interactions.append(interaction) + entriesQueue.sync(flags: .barrier, execute: { + // Get the next timestamp while executing on the entries queue to + // guarantee that entries are appended in chronological order. + let entry = Entry(value: value, timestamp: nextTimestamp(), file: file, line: line) + + entries.append(entry) + }) + } +} + +extension Recorder: ValueRecording { + public var interactions: AnyRandomAccessCollection { + let entries = entriesQueue.sync(execute: { + return self.entries + }) + + let interactions = entries.lazy.map({ entry in + return entry.interaction }) + + return AnyRandomAccessCollection(interactions) + } + + public func valueForInteraction(at index: Int) -> Value { + let entries = entriesQueue.sync(execute: { + return self.entries + }) + + return entries[index].value } } diff --git a/DobbyTests/RecorderSpec.swift b/DobbyTests/RecorderSpec.swift index 1e28ec7..33c15e6 100644 --- a/DobbyTests/RecorderSpec.swift +++ b/DobbyTests/RecorderSpec.swift @@ -12,17 +12,19 @@ class RecorderSpec: QuickSpec { } describe("Recording") { - it("appends an interaction to the recorder") { + it("appends an interaction and its corresponding value to the recorder") { let currentTimestamp = Dobby.currentTimestamp recorder.record(1) - // Verify that the interaction has been appended. + // Verify that the interaction has been recorded. expect(recorder.interactions).to(haveCount(1)) - expect(recorder.interactions[0].timestamp).to(equal(currentTimestamp)) - expect(recorder.interactions[0].value).to(equal(1)) + expect(recorder.interactions[AnyIndex(0)].timestamp).to(equal(currentTimestamp + 1)) - // Verify that the global clock is advanced. + // Verify that the corresponding value has been recorded. + expect(recorder.valueForInteraction(at: 0)).to(equal(1)) + + // Verify that the global clock has been advanced. expect(Dobby.currentTimestamp).to(equal(currentTimestamp + 1)) } } From faed0958dc83a2d9c1c273abc0c93ffc13b04906 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Fri, 6 Jan 2017 10:08:14 +0100 Subject: [PATCH 05/11] Return only if the expectation isn't negative If the expectation is negative and the mock is ordered (and nice), attempt to fulfill the next expectation. Fixes #38. --- Dobby/Mock.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dobby/Mock.swift b/Dobby/Mock.swift index 62a3723..5805f07 100644 --- a/Dobby/Mock.swift +++ b/Dobby/Mock.swift @@ -79,7 +79,9 @@ public final class Mock { fail("Interaction <\(interaction)> does not match expectation <\(expectation)>", file, line) } - return + if expectation.negative == false { + return + } } } From 3d0238a807e719d6ac483a902a298a631de450ad Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Fri, 6 Jan 2017 10:26:08 +0100 Subject: [PATCH 06/11] Rename stub behaviors to reactions --- Dobby/Stub.swift | 78 ++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/Dobby/Stub.swift b/Dobby/Stub.swift index f3639d1..c314ccc 100644 --- a/Dobby/Stub.swift +++ b/Dobby/Stub.swift @@ -1,89 +1,89 @@ -/// A matcher-based behavior with closure-based handling. -fileprivate struct Behavior { - /// The matcher of this behavior. - fileprivate let matcher: Matcher +/// A matcher-based reaction with closure-based handling. +fileprivate struct Reaction { + /// The matcher of this reaction. + fileprivate let matcher: Matcher - /// The handler of this behavior. - fileprivate let handler: (Interaction) -> ReturnValue + /// The handler of this reaction. + fileprivate let handler: (Value) -> ReturnValue - /// Initializes a new behavior with the given matcher and handler. - fileprivate init(matcher: M, handler: @escaping (Interaction) -> ReturnValue) where M.ValueType == Interaction { + /// Initializes a new reaction with the given matcher and handler. + fileprivate init(matcher: Matcher, handler: @escaping (Value) -> ReturnValue) where Matcher.ValueType == Value { self.matcher = matcher.matcher() self.handler = handler } } /// A stub error. -public enum StubError: Error { - /// The associated interaction was unexpected. - case unexpectedInteraction(Interaction) +public enum StubError: Error { + /// An interaction with the associated value was unexpected. + case unexpectedInteraction(Value) } -/// A stub that, when invoked, returns a value based on the set up behavior, or, -/// if an interaction is unexpected, throws an error. -public final class Stub { - /// The current (next) identifier for behaviors. +/// A stub that, when invoked, returns a value based on the set up reactions, +/// or, if an interaction is unexpected, throws an error. +public final class Stub { + /// The current (next) identifier for reactions. private var currentIdentifier: UInt = 0 - /// The behaviors of this stub. - private var behaviors: [(identifier: UInt, behavior: Behavior)] = [] + /// The reactions of this stub. + private var reactions: [(identifier: UInt, reaction: Reaction)] = [] - /// Initializes a new stub. + /// Creates a new stub. public init() { } - /// Modifies the behavior of this stub, forwarding invocations to the given - /// function and returning its return value if the given matcher does match - /// an interaction. + /// Modifies the reactions of this stub, forwarding invocations to the + /// given handler and returning its return value if the given matcher + /// does match an interaction. /// - /// Returns a disposable that, when disposed, removes this behavior. + /// Returns a disposable that, when disposed, removes this reaction. @discardableResult - public func on(_ matcher: M, invoke handler: @escaping (Interaction) -> ReturnValue) -> Disposable where M.ValueType == Interaction { + public func on(_ matcher: Matcher, invoke handler: @escaping (Value) -> ReturnValue) -> Disposable where Matcher.ValueType == Value { currentIdentifier += 1 let identifier = currentIdentifier - let behavior = Behavior(matcher: matcher, handler: handler) - behaviors.append((identifier: identifier, behavior: behavior)) + let reaction = Reaction(matcher: matcher, handler: handler) + reactions.append((identifier: identifier, reaction: reaction)) return Disposable { [weak self] in - let index = self?.behaviors.index { otherIdentifier, _ in + let index = self?.reactions.index { otherIdentifier, _ in return otherIdentifier == identifier } if let index = index { - self?.behaviors.remove(at: index) + self?.reactions.remove(at: index) } } } - /// Modifies the behavior of this stub, returning the given value upon + /// Modifies the reactions of this stub, returning the given value upon /// invocation if the given matcher does match an interaction. /// - /// Returns a disposable that, when disposed, removes this behavior. + /// Returns a disposable that, when disposed, removes this reaction. /// - /// - SeeAlso: `Stub.on(matcher: M, invoke: Interaction -> ReturnValue) -> Disposable` + /// - SeeAlso: `Stub.on(matcher: Matcher, invoke: (Value) -> ReturnValue) -> Disposable` @discardableResult - public func on(_ matcher: M, return value: ReturnValue) -> Disposable where M.ValueType == Interaction { + public func on(_ matcher: Matcher, return value: ReturnValue) -> Disposable where Matcher.ValueType == Value { return on(matcher) { _ in value } } - /// Invokes this stub, returning a value based on the set up behavior, or, + /// Invokes this stub, returning a value based on the set up reactions, or, /// if the given interaction is unexpected, throwing an error. /// - /// Behavior is matched in order, i.e., the function associated with the + /// Reactions are matched in order, i.e., the handler associated with the /// first matcher that matches the given interaction is invoked. /// - /// - Throws: `StubError.unexpectedInteraction(Interaction)` if the given + /// - Throws: `StubError.unexpectedInteraction(Value)` if the given /// interaction is unexpected. @discardableResult - public func invoke(_ interaction: Interaction) throws -> ReturnValue { - for (_, behavior) in behaviors { - if behavior.matcher.matches(interaction) { - return behavior.handler(interaction) + public func invoke(_ value: Value) throws -> ReturnValue { + for (_, reaction) in reactions { + if reaction.matcher.matches(value) { + return reaction.handler(value) } } - throw StubError.unexpectedInteraction(interaction) + throw StubError.unexpectedInteraction(value) } } From 9516bd8e22c609933895d635c8009aa599d9e631 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Fri, 6 Jan 2017 11:08:08 +0100 Subject: [PATCH 07/11] Add multiple interaction recorder verification using behaviors --- Dobby.xcodeproj/project.pbxproj | 12 ++ Dobby/Behavior.swift | 284 ++++++++++++++++++++++++++++++++ DobbyTests/BehaviorSpec.swift | 254 ++++++++++++++++++++++++++++ 3 files changed, 550 insertions(+) create mode 100644 Dobby/Behavior.swift create mode 100644 DobbyTests/BehaviorSpec.swift diff --git a/Dobby.xcodeproj/project.pbxproj b/Dobby.xcodeproj/project.pbxproj index fcbda15..02f1364 100644 --- a/Dobby.xcodeproj/project.pbxproj +++ b/Dobby.xcodeproj/project.pbxproj @@ -16,6 +16,10 @@ DC4D88391E1CECE100FAE93F /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88371E1CECE100FAE93F /* Recorder.swift */; }; DC4D883B1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */; }; DC4D883C1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */; }; + DC4D883E1E1CFCAE00FAE93F /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */; }; + DC4D883F1E1CFCAE00FAE93F /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */; }; + DC4D88441E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */; }; + DC4D88451E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */; }; DC7F6AFE1AD8291800CCBF6D /* Dobby.h in Headers */ = {isa = PBXBuildFile; fileRef = DC7F6AFD1AD8291800CCBF6D /* Dobby.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC7F6B041AD8291800CCBF6D /* Dobby.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7F6AF81AD8291800CCBF6D /* Dobby.framework */; }; DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */; }; @@ -78,6 +82,8 @@ DC457AD81B5552CA00919032 /* Matcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; DC4D88371E1CECE100FAE93F /* Recorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = ""; }; DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecorderSpec.swift; sourceTree = ""; }; + DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Behavior.swift; sourceTree = ""; }; + DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BehaviorSpec.swift; sourceTree = ""; }; DC7F6AF81AD8291800CCBF6D /* Dobby.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Dobby.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC7F6AFC1AD8291800CCBF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DC7F6AFD1AD8291800CCBF6D /* Dobby.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Dobby.h; sourceTree = ""; }; @@ -190,6 +196,7 @@ DC7F6AFA1AD8291800CCBF6D /* Dobby */ = { isa = PBXGroup; children = ( + DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */, DC83356B1B56661900569709 /* Disposable.swift */, DC7F6AFD1AD8291800CCBF6D /* Dobby.h */, DC457AD81B5552CA00919032 /* Matcher.swift */, @@ -213,6 +220,7 @@ DC7F6B071AD8291800CCBF6D /* DobbyTests */ = { isa = PBXGroup; children = ( + DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */, DCE4080D1B6161000009E0B4 /* MatcherSpec.swift */, DC7F6B241AD830A500CCBF6D /* MockSpec.swift */, DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */, @@ -506,6 +514,7 @@ DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */, DC7F6B201AD82D3800CCBF6D /* Mock.swift in Sources */, DC4D88391E1CECE100FAE93F /* Recorder.swift in Sources */, + DC4D883F1E1CFCAE00FAE93F /* Behavior.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -516,6 +525,7 @@ DCE4080E1B6161000009E0B4 /* MatcherSpec.swift in Sources */, DC7F6B251AD830A500CCBF6D /* MockSpec.swift in Sources */, DC4D883C1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, + DC4D88451E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */, DC7F6B271AD830B500CCBF6D /* StubSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -530,6 +540,7 @@ DC8452401ADBF73C006AC5D4 /* Stub.swift in Sources */, DC84523F1ADBF73C006AC5D4 /* Mock.swift in Sources */, DC4D88381E1CECE100FAE93F /* Recorder.swift in Sources */, + DC4D883E1E1CFCAE00FAE93F /* Behavior.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -540,6 +551,7 @@ DCE4080F1B6161000009E0B4 /* MatcherSpec.swift in Sources */, DC8452421ADBF743006AC5D4 /* MockSpec.swift in Sources */, DC4D883B1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, + DC4D88441E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */, DC8452431ADBF743006AC5D4 /* StubSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Dobby/Behavior.swift b/Dobby/Behavior.swift new file mode 100644 index 0000000..6a2d358 --- /dev/null +++ b/Dobby/Behavior.swift @@ -0,0 +1,284 @@ +import XCTest + +/// A value type-erased expectation. +fileprivate struct Expectation: CustomStringConvertible { + fileprivate let description: String + + /// The matching closure of this expectation. + /// + /// Given the index of a recorded interaction, the closure is supposed to + /// check whether the corresponding value fulfills this expectation. + fileprivate let matches: (Int) -> Bool + + /// The recorder that is expected (not) to record an interaction with a + /// corresponding matching value. + fileprivate let recorder: InteractionRecording + + /// Whether this expectation is negative. + fileprivate let negative: Bool + + /// The file in which this expectation was set up. + fileprivate let file: StaticString + + /// The line at which this expectation was set up. + fileprivate let line: UInt + + /// Creates a new value type-erased expectation with the given description, + /// matching closure, recorder, and negative flag. + fileprivate init(description: String, matches: @escaping (Int) -> Bool, recorder: InteractionRecording, negative: Bool, file: StaticString, line: UInt) { + self.description = description + self.matches = matches + + self.recorder = recorder + self.negative = negative + + self.file = file + self.line = line + } + + /// Creates a new value type-erased expectation with the given matcher, + /// recorder, and negative flag using the matcher's textual representation + /// as description. + fileprivate init(matcher: Matcher, recorder: Recorder, negative: Bool, file: StaticString, line: UInt) where Matcher.ValueType == Recorder.Value { + let actualMatcher = matcher.matcher() + + // The matching closure captures the value type of the recorder. + let matches = { (index: Int) -> Bool in + let actualValue = recorder.valueForInteraction(at: index) + return actualMatcher.matches(actualValue) + } + + self.init(description: actualMatcher.description, matches: matches, recorder: recorder, negative: negative, file: file, line: line) + } +} + +/// An iterator over a given set of interaction recorders, producing their +/// interactions in chronological order. +fileprivate struct InteractionRecordingIterator: IteratorProtocol { + fileprivate struct Element: CustomStringConvertible { + fileprivate let recorder: InteractionRecording + + fileprivate var index: Int + + fileprivate var interaction: Interaction { + return recorder.interactions[AnyIndex(index)] + } + + fileprivate var description: String { + return interaction.description + } + + fileprivate var timestamp: Timestamp { + return interaction.timestamp + } + + fileprivate var file: StaticString { + return interaction.file + } + + fileprivate var line: UInt { + return interaction.line + } + } + + private var elements: [Element] + + private mutating func heapify(at index: Int) { + var parentIndex = index + + while parentIndex < elements.count { + let leftChildIndex = 2 * parentIndex + 1 + let rightChildIndex = 2 * parentIndex + 2 + + var smallestIndex = parentIndex + + if leftChildIndex < elements.count && elements[leftChildIndex].timestamp < elements[smallestIndex].timestamp { + smallestIndex = leftChildIndex + } + + if rightChildIndex < elements.count && elements[rightChildIndex].timestamp < elements[smallestIndex].timestamp { + smallestIndex = rightChildIndex + } + + if smallestIndex == parentIndex { + break + } + + let parent = elements[parentIndex] + elements[parentIndex] = elements[smallestIndex] + elements[smallestIndex] = parent + + parentIndex = smallestIndex + } + } + + fileprivate init(recorders: Set) { + elements = recorders.filter({ recorder in + return !recorder.interactions.isEmpty + }).map({ recorder in + return Element(recorder: recorder, index: 0) + }) + + for index in stride(from: (elements.count / 2 - 1), through: 0, by: -1) { + heapify(at: index) + } + } + + fileprivate mutating func next() -> Element? { + guard let element = elements.first else { + return nil + } + + if element.index + 1 == element.recorder.interactions.count { + if elements.count == 1 { + elements.removeLast() + } else { + elements[0] = elements.removeLast() + } + } else { + elements[0].index += 1 + } + + heapify(at: 0) + + return element + } +} + +/// A thread-safe behavior that can verify set up expectations with multiple +/// interaction recorders. +public final class Behavior { + /// Whether this behavior is strict (or nice). + private let strict: Bool + + /// Whether the order of expectations matters. + private let ordered: Bool + + /// All set up expectations. + private var expectations: [Expectation] = [] + private let expectationsQueue = DispatchQueue(label: "com.trivago.dobby.behavior-expectationsQueue", attributes: .concurrent) + + /// Creates a new behavior with the given strict and ordered flags. + public init(strict: Bool = true, ordered: Bool = true) { + self.strict = strict + self.ordered = ordered + } + + /// Creates a new behavior with the given nice and ordered flags. + public convenience init(nice: Bool, ordered: Bool = true) { + self.init(strict: nice == false, ordered: ordered) + } + + /// Sets up the given matcher as expectation for the given recorder. + public func expect(_ matcher: Matcher, in recorder: Recorder, file: StaticString = #file, line: UInt = #line) where Matcher.ValueType == Recorder.Value { + let expectation = Expectation(matcher: matcher, recorder: recorder, negative: false, file: file, line: line) + + expectationsQueue.sync(flags: .barrier, execute: { + expectations.append(expectation) + }) + } + + /// Sets up the given matcher as negative expectation for the given + /// recorder, meaning any matching value will be rejected. + /// + /// - Note: Negative expectations are restricted to nice behaviors. + public func reject(_ matcher: Matcher, in recorder: Recorder, file: StaticString = #file, line: UInt = #line) where Matcher.ValueType == Recorder.Value { + precondition(strict == false, "Only nice behaviors may have negative expectations.") + + let expectation = Expectation(matcher: matcher, recorder: recorder, negative: true, file: file, line: line) + + expectationsQueue.sync(flags: .barrier, execute: { + expectations.append(expectation) + }) + } + + /// Verifies that all set up expectations are fulfilled by continuously + /// checking at each poll interval until the timeout is reached. + public func verify(poll: TimeInterval = 0.01, timeout: TimeInterval = 0) { + verify(poll: poll, timeout: timeout, fail: XCTFail) + } + + internal func verify(poll: TimeInterval, timeout: TimeInterval, fail: (String, StaticString, UInt) -> ()) { + var failures: [(String, StaticString, UInt)] + + // Set the initial limit date to now. + var limitDate = Date() + + // Set the timeout date using the specified timeout. + let timeoutDate = Date(timeInterval: timeout, since: limitDate) + + // Repetitively attempt to verify that all set up expectations are + // fulfilled until the timeout is reached. + repeat { + // Discard any previous failures. + failures = [] + + // Attempt to verify that all set up expectations are fulfilled. + verify(fail: { message, file, line in + failures.append((message, file, line)) + }) + + // Finish if verification was successful. + if failures.isEmpty { + return + } + + // Add the poll time interval to the limit date. + limitDate = Date(timeInterval: poll, since: limitDate) + + // Run the current loop until the limit date, at most until the + // timeout date. Nimble uses a more advanced technique based on + // dispatch sources, we should take a look. + let currentLoop: RunLoop = .current + currentLoop.run(until: min(limitDate, timeoutDate)) + } while Date() < timeoutDate + + for (message, file, line) in failures { + fail(message, file, line) + } + } + + internal func verify(fail: (String, StaticString, UInt) -> ()) { + var expectations = expectationsQueue.sync(execute: { + return self.expectations + }) + + let recorders = Set(expectations.map({ expectation in + return AnyInteractionRecording(expectation.recorder) + })) + + let iterator = InteractionRecordingIterator(recorders: recorders) + + replay: for interaction in IteratorSequence(iterator) { + for (index, expectation) in expectations.enumerated() { + if expectation.recorder.objectIdentifier == interaction.recorder.objectIdentifier && expectation.matches(interaction.index) { + if expectation.negative == false { + expectations.remove(at: index) + } else { + fail("Interaction <\(interaction)> not allowed", interaction.file, interaction.line) + } + + continue replay + } else if ordered { + if strict { + fail("Interaction <\(interaction)> does not match expectation <\(expectation)>", interaction.file, interaction.line) + } + + if expectation.negative == false { + continue replay + } + } + } + + if strict { + fail("Interaction <\(interaction)> not expected", interaction.file, interaction.line) + } + } + + for expectation in expectations { + if expectation.negative == false { + fail("Expectation <\(expectation)> not fulfilled", expectation.file, expectation.line) + } + } + } +} diff --git a/DobbyTests/BehaviorSpec.swift b/DobbyTests/BehaviorSpec.swift new file mode 100644 index 0000000..9828c64 --- /dev/null +++ b/DobbyTests/BehaviorSpec.swift @@ -0,0 +1,254 @@ +import Quick +import Nimble + +@testable import Dobby + +class BehaviorSpec: QuickSpec { + override func spec() { + var behavior: Behavior! + + var recorder1: Recorder! + var recorder2: Recorder! + + beforeEach { + recorder1 = Recorder() + recorder2 = Recorder() + } + + describe("Verification") { + context("when the behavior is strict and the order of expectations does matter") { + beforeEach { + behavior = Behavior(strict: true, ordered: true) + } + + it("succeeds if all set up expectations are fulfilled") { + recorder1.record(1) + recorder2.record(2) + + behavior.expect(1, in: recorder1) + behavior.expect(2, in: recorder2) + behavior.verify() + } + + it("fails if an interaction does not match the current expectation") { + recorder1.record(1) + recorder2.record(2) + + behavior.expect(1, in: recorder1) + behavior.expect(3, in: recorder2) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Interaction <2> does not match expectation <3>", + "Expectation <3> not fulfilled" + ])) + } + + it("fails if an interaction is not expected") { + recorder1.record(1) + recorder1.record(2) + + behavior.expect(1, in: recorder1) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Interaction <2> not expected" + ])) + } + + it("fails if any expectation is not fulfilled") { + recorder1.record(1) + + behavior.expect(1, in: recorder1) + behavior.expect(2, in: recorder2) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Expectation <2> not fulfilled" + ])) + } + } + + context("when the behavior is strict and the order of expectations does not matter") { + beforeEach { + behavior = Behavior(strict: true, ordered: false) + } + + it("succeeds if all set up expectations are fulfilled") { + recorder2.record(2) + recorder1.record(1) + + behavior.expect(1, in: recorder1) + behavior.expect(2, in: recorder2) + behavior.verify() + } + + it("fails if an interaction is not expected") { + recorder1.record(2) + recorder1.record(1) + + behavior.expect(1, in: recorder1) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Interaction <2> not expected" + ])) + } + + it("fails if any expectation is not fulfilled") { + recorder1.record(1) + + behavior.expect(2, in: recorder2) + behavior.expect(1, in: recorder1) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Expectation <2> not fulfilled" + ])) + } + } + + context("when the behavior is nice and the order of expectations does matter") { + beforeEach { + behavior = Behavior(nice: true, ordered: true) + } + + it("succeeds if all set up expectations are fulfilled") { + recorder1.record(1) + recorder2.record(2) + recorder1.record(3) + recorder2.record(4) + + behavior.expect(1, in: recorder1) + behavior.expect(2, in: recorder2) + behavior.verify() + } + + it("fails if expectations are not fulfilled in order") { + recorder1.record(3) + recorder1.record(2) + recorder1.record(1) + + behavior.expect(1, in: recorder1) + behavior.expect(2, in: recorder1) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Expectation <2> not fulfilled" + ])) + } + + it("fails if any expectation is not fulfilled") { + recorder1.record(1) + + behavior.expect(1, in: recorder1) + behavior.expect(2, in: recorder2) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Expectation <2> not fulfilled" + ])) + } + } + + context("when the behavior is nice and the order of expectations does not matter") { + beforeEach { + behavior = Behavior(nice: true, ordered: false) + } + + it("succeeds if all set up expectations are fulfilled") { + recorder2.record(4) + recorder1.record(3) + recorder2.record(2) + recorder1.record(1) + + behavior.expect(1, in: recorder1) + behavior.expect(2, in: recorder2) + behavior.verify() + } + + it("fails if any expectation is not fulfilled") { + recorder1.record(1) + + behavior.expect(2, in: recorder2) + behavior.expect(1, in: recorder1) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Expectation <2> not fulfilled" + ])) + } + } + } + + describe("Verification with a timeout") { + beforeEach { + behavior = Behavior() + } + + it("succeeds if all expectations are fulfilled before the timeout is reached") { + let mainQueue: DispatchQueue = .main + mainQueue.asyncAfter(deadline: .now() + 0.25) { + recorder1.record(1) + } + + behavior.expect(1, in: recorder1) + behavior.verify(timeout: 0.5) + } + + it("fails if any expectation is not fulfilled before the timeout is reached") { + behavior.expect(1, in: recorder1) + + var failureMessages: [String] = [] + + behavior.verify(fail: { message, _, _ in + failureMessages.append(message) + }) + + expect(failureMessages).to(equal([ + "Expectation <1> not fulfilled" + ])) + } + } + } +} From dfb5d6961814f31daa03e7bd7ce464db65298b48 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Thu, 16 Mar 2017 14:06:09 +0100 Subject: [PATCH 08/11] Rename behavior to pattern --- Dobby.xcodeproj/project.pbxproj | 24 ++--- Dobby/{Behavior.swift => Pattern.swift} | 16 +-- .../{BehaviorSpec.swift => PatternSpec.swift} | 100 +++++++++--------- 3 files changed, 70 insertions(+), 70 deletions(-) rename Dobby/{Behavior.swift => Pattern.swift} (94%) rename DobbyTests/{BehaviorSpec.swift => PatternSpec.swift} (66%) diff --git a/Dobby.xcodeproj/project.pbxproj b/Dobby.xcodeproj/project.pbxproj index 02f1364..15eab97 100644 --- a/Dobby.xcodeproj/project.pbxproj +++ b/Dobby.xcodeproj/project.pbxproj @@ -16,10 +16,10 @@ DC4D88391E1CECE100FAE93F /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88371E1CECE100FAE93F /* Recorder.swift */; }; DC4D883B1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */; }; DC4D883C1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */; }; - DC4D883E1E1CFCAE00FAE93F /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */; }; - DC4D883F1E1CFCAE00FAE93F /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */; }; - DC4D88441E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */; }; - DC4D88451E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */; }; + DC4D883E1E1CFCAE00FAE93F /* Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883D1E1CFCAE00FAE93F /* Pattern.swift */; }; + DC4D883F1E1CFCAE00FAE93F /* Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D883D1E1CFCAE00FAE93F /* Pattern.swift */; }; + DC4D88441E1F9A5500FAE93F /* PatternSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88431E1F9A5500FAE93F /* PatternSpec.swift */; }; + DC4D88451E1F9A5500FAE93F /* PatternSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88431E1F9A5500FAE93F /* PatternSpec.swift */; }; DC7F6AFE1AD8291800CCBF6D /* Dobby.h in Headers */ = {isa = PBXBuildFile; fileRef = DC7F6AFD1AD8291800CCBF6D /* Dobby.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC7F6B041AD8291800CCBF6D /* Dobby.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7F6AF81AD8291800CCBF6D /* Dobby.framework */; }; DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */; }; @@ -82,8 +82,8 @@ DC457AD81B5552CA00919032 /* Matcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; DC4D88371E1CECE100FAE93F /* Recorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = ""; }; DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecorderSpec.swift; sourceTree = ""; }; - DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Behavior.swift; sourceTree = ""; }; - DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BehaviorSpec.swift; sourceTree = ""; }; + DC4D883D1E1CFCAE00FAE93F /* Pattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pattern.swift; sourceTree = ""; }; + DC4D88431E1F9A5500FAE93F /* PatternSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatternSpec.swift; sourceTree = ""; }; DC7F6AF81AD8291800CCBF6D /* Dobby.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Dobby.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC7F6AFC1AD8291800CCBF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DC7F6AFD1AD8291800CCBF6D /* Dobby.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Dobby.h; sourceTree = ""; }; @@ -196,11 +196,11 @@ DC7F6AFA1AD8291800CCBF6D /* Dobby */ = { isa = PBXGroup; children = ( - DC4D883D1E1CFCAE00FAE93F /* Behavior.swift */, DC83356B1B56661900569709 /* Disposable.swift */, DC7F6AFD1AD8291800CCBF6D /* Dobby.h */, DC457AD81B5552CA00919032 /* Matcher.swift */, DC7F6B1F1AD82D3800CCBF6D /* Mock.swift */, + DC4D883D1E1CFCAE00FAE93F /* Pattern.swift */, DC4D88371E1CECE100FAE93F /* Recorder.swift */, DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */, DC3CFE761ADE895E004728C2 /* Extensions */, @@ -220,9 +220,9 @@ DC7F6B071AD8291800CCBF6D /* DobbyTests */ = { isa = PBXGroup; children = ( - DC4D88431E1F9A5500FAE93F /* BehaviorSpec.swift */, DCE4080D1B6161000009E0B4 /* MatcherSpec.swift */, DC7F6B241AD830A500CCBF6D /* MockSpec.swift */, + DC4D88431E1F9A5500FAE93F /* PatternSpec.swift */, DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */, DC7F6B261AD830B500CCBF6D /* StubSpec.swift */, DC7F6B081AD8291800CCBF6D /* Supporting Files */, @@ -514,7 +514,7 @@ DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */, DC7F6B201AD82D3800CCBF6D /* Mock.swift in Sources */, DC4D88391E1CECE100FAE93F /* Recorder.swift in Sources */, - DC4D883F1E1CFCAE00FAE93F /* Behavior.swift in Sources */, + DC4D883F1E1CFCAE00FAE93F /* Pattern.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -525,7 +525,7 @@ DCE4080E1B6161000009E0B4 /* MatcherSpec.swift in Sources */, DC7F6B251AD830A500CCBF6D /* MockSpec.swift in Sources */, DC4D883C1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, - DC4D88451E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */, + DC4D88451E1F9A5500FAE93F /* PatternSpec.swift in Sources */, DC7F6B271AD830B500CCBF6D /* StubSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -540,7 +540,7 @@ DC8452401ADBF73C006AC5D4 /* Stub.swift in Sources */, DC84523F1ADBF73C006AC5D4 /* Mock.swift in Sources */, DC4D88381E1CECE100FAE93F /* Recorder.swift in Sources */, - DC4D883E1E1CFCAE00FAE93F /* Behavior.swift in Sources */, + DC4D883E1E1CFCAE00FAE93F /* Pattern.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -551,7 +551,7 @@ DCE4080F1B6161000009E0B4 /* MatcherSpec.swift in Sources */, DC8452421ADBF743006AC5D4 /* MockSpec.swift in Sources */, DC4D883B1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, - DC4D88441E1F9A5500FAE93F /* BehaviorSpec.swift in Sources */, + DC4D88441E1F9A5500FAE93F /* PatternSpec.swift in Sources */, DC8452431ADBF743006AC5D4 /* StubSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Dobby/Behavior.swift b/Dobby/Pattern.swift similarity index 94% rename from Dobby/Behavior.swift rename to Dobby/Pattern.swift index 6a2d358..9761d4c 100644 --- a/Dobby/Behavior.swift +++ b/Dobby/Pattern.swift @@ -145,10 +145,10 @@ fileprivate struct InteractionRecordingIterator: IteratorProtocol { } } -/// A thread-safe behavior that can verify set up expectations with multiple +/// A thread-safe pattern that can verify set up expectations with multiple /// interaction recorders. -public final class Behavior { - /// Whether this behavior is strict (or nice). +public final class Pattern { + /// Whether this pattern is strict (or nice). private let strict: Bool /// Whether the order of expectations matters. @@ -156,15 +156,15 @@ public final class Behavior { /// All set up expectations. private var expectations: [Expectation] = [] - private let expectationsQueue = DispatchQueue(label: "com.trivago.dobby.behavior-expectationsQueue", attributes: .concurrent) + private let expectationsQueue = DispatchQueue(label: "com.trivago.dobby.pattern-expectationsQueue", attributes: .concurrent) - /// Creates a new behavior with the given strict and ordered flags. + /// Creates a new pattern with the given strict and ordered flags. public init(strict: Bool = true, ordered: Bool = true) { self.strict = strict self.ordered = ordered } - /// Creates a new behavior with the given nice and ordered flags. + /// Creates a new pattern with the given nice and ordered flags. public convenience init(nice: Bool, ordered: Bool = true) { self.init(strict: nice == false, ordered: ordered) } @@ -181,9 +181,9 @@ public final class Behavior { /// Sets up the given matcher as negative expectation for the given /// recorder, meaning any matching value will be rejected. /// - /// - Note: Negative expectations are restricted to nice behaviors. + /// - Note: Negative expectations are restricted to nice patterns. public func reject(_ matcher: Matcher, in recorder: Recorder, file: StaticString = #file, line: UInt = #line) where Matcher.ValueType == Recorder.Value { - precondition(strict == false, "Only nice behaviors may have negative expectations.") + precondition(strict == false, "Only nice patterns may have negative expectations.") let expectation = Expectation(matcher: matcher, recorder: recorder, negative: true, file: file, line: line) diff --git a/DobbyTests/BehaviorSpec.swift b/DobbyTests/PatternSpec.swift similarity index 66% rename from DobbyTests/BehaviorSpec.swift rename to DobbyTests/PatternSpec.swift index 9828c64..b9e697c 100644 --- a/DobbyTests/BehaviorSpec.swift +++ b/DobbyTests/PatternSpec.swift @@ -1,11 +1,11 @@ import Quick import Nimble -@testable import Dobby +@testable import Dobby; import class Dobby.Pattern -class BehaviorSpec: QuickSpec { +class PatternSpec: QuickSpec { override func spec() { - var behavior: Behavior! + var pattern: Pattern! var recorder1: Recorder! var recorder2: Recorder! @@ -16,30 +16,30 @@ class BehaviorSpec: QuickSpec { } describe("Verification") { - context("when the behavior is strict and the order of expectations does matter") { + context("when the pattern is strict and the order of expectations does matter") { beforeEach { - behavior = Behavior(strict: true, ordered: true) + pattern = Pattern(strict: true, ordered: true) } it("succeeds if all set up expectations are fulfilled") { recorder1.record(1) recorder2.record(2) - behavior.expect(1, in: recorder1) - behavior.expect(2, in: recorder2) - behavior.verify() + pattern.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) + pattern.verify() } it("fails if an interaction does not match the current expectation") { recorder1.record(1) recorder2.record(2) - behavior.expect(1, in: recorder1) - behavior.expect(3, in: recorder2) + pattern.expect(1, in: recorder1) + pattern.expect(3, in: recorder2) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -53,11 +53,11 @@ class BehaviorSpec: QuickSpec { recorder1.record(1) recorder1.record(2) - behavior.expect(1, in: recorder1) + pattern.expect(1, in: recorder1) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -69,12 +69,12 @@ class BehaviorSpec: QuickSpec { it("fails if any expectation is not fulfilled") { recorder1.record(1) - behavior.expect(1, in: recorder1) - behavior.expect(2, in: recorder2) + pattern.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -84,29 +84,29 @@ class BehaviorSpec: QuickSpec { } } - context("when the behavior is strict and the order of expectations does not matter") { + context("when the pattern is strict and the order of expectations does not matter") { beforeEach { - behavior = Behavior(strict: true, ordered: false) + pattern = Pattern(strict: true, ordered: false) } it("succeeds if all set up expectations are fulfilled") { recorder2.record(2) recorder1.record(1) - behavior.expect(1, in: recorder1) - behavior.expect(2, in: recorder2) - behavior.verify() + pattern.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) + pattern.verify() } it("fails if an interaction is not expected") { recorder1.record(2) recorder1.record(1) - behavior.expect(1, in: recorder1) + pattern.expect(1, in: recorder1) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -118,12 +118,12 @@ class BehaviorSpec: QuickSpec { it("fails if any expectation is not fulfilled") { recorder1.record(1) - behavior.expect(2, in: recorder2) - behavior.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) + pattern.expect(1, in: recorder1) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -133,9 +133,9 @@ class BehaviorSpec: QuickSpec { } } - context("when the behavior is nice and the order of expectations does matter") { + context("when the pattern is nice and the order of expectations does matter") { beforeEach { - behavior = Behavior(nice: true, ordered: true) + pattern = Pattern(nice: true, ordered: true) } it("succeeds if all set up expectations are fulfilled") { @@ -144,9 +144,9 @@ class BehaviorSpec: QuickSpec { recorder1.record(3) recorder2.record(4) - behavior.expect(1, in: recorder1) - behavior.expect(2, in: recorder2) - behavior.verify() + pattern.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) + pattern.verify() } it("fails if expectations are not fulfilled in order") { @@ -154,12 +154,12 @@ class BehaviorSpec: QuickSpec { recorder1.record(2) recorder1.record(1) - behavior.expect(1, in: recorder1) - behavior.expect(2, in: recorder1) + pattern.expect(1, in: recorder1) + pattern.expect(2, in: recorder1) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -171,12 +171,12 @@ class BehaviorSpec: QuickSpec { it("fails if any expectation is not fulfilled") { recorder1.record(1) - behavior.expect(1, in: recorder1) - behavior.expect(2, in: recorder2) + pattern.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -186,9 +186,9 @@ class BehaviorSpec: QuickSpec { } } - context("when the behavior is nice and the order of expectations does not matter") { + context("when the pattern is nice and the order of expectations does not matter") { beforeEach { - behavior = Behavior(nice: true, ordered: false) + pattern = Pattern(nice: true, ordered: false) } it("succeeds if all set up expectations are fulfilled") { @@ -197,20 +197,20 @@ class BehaviorSpec: QuickSpec { recorder2.record(2) recorder1.record(1) - behavior.expect(1, in: recorder1) - behavior.expect(2, in: recorder2) - behavior.verify() + pattern.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) + pattern.verify() } it("fails if any expectation is not fulfilled") { recorder1.record(1) - behavior.expect(2, in: recorder2) - behavior.expect(1, in: recorder1) + pattern.expect(2, in: recorder2) + pattern.expect(1, in: recorder1) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) @@ -223,7 +223,7 @@ class BehaviorSpec: QuickSpec { describe("Verification with a timeout") { beforeEach { - behavior = Behavior() + pattern = Pattern() } it("succeeds if all expectations are fulfilled before the timeout is reached") { @@ -232,16 +232,16 @@ class BehaviorSpec: QuickSpec { recorder1.record(1) } - behavior.expect(1, in: recorder1) - behavior.verify(timeout: 0.5) + pattern.expect(1, in: recorder1) + pattern.verify(timeout: 0.5) } it("fails if any expectation is not fulfilled before the timeout is reached") { - behavior.expect(1, in: recorder1) + pattern.expect(1, in: recorder1) var failureMessages: [String] = [] - behavior.verify(fail: { message, _, _ in + pattern.verify(fail: { message, _, _ in failureMessages.append(message) }) From f100acb7e14ed2d05761ecf53ec89099c9e7ac09 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Thu, 16 Mar 2017 14:09:53 +0100 Subject: [PATCH 09/11] Rename stub to behavior --- Dobby.xcodeproj/project.pbxproj | 24 +++++++------- Dobby/{Stub.swift => Behavior.swift} | 32 +++++++++---------- .../{StubSpec.swift => BehaviorSpec.swift} | 24 +++++++------- 3 files changed, 40 insertions(+), 40 deletions(-) rename Dobby/{Stub.swift => Behavior.swift} (73%) rename DobbyTests/{StubSpec.swift => BehaviorSpec.swift} (54%) diff --git a/Dobby.xcodeproj/project.pbxproj b/Dobby.xcodeproj/project.pbxproj index 15eab97..dd3fc04 100644 --- a/Dobby.xcodeproj/project.pbxproj +++ b/Dobby.xcodeproj/project.pbxproj @@ -22,18 +22,18 @@ DC4D88451E1F9A5500FAE93F /* PatternSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D88431E1F9A5500FAE93F /* PatternSpec.swift */; }; DC7F6AFE1AD8291800CCBF6D /* Dobby.h in Headers */ = {isa = PBXBuildFile; fileRef = DC7F6AFD1AD8291800CCBF6D /* Dobby.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC7F6B041AD8291800CCBF6D /* Dobby.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7F6AF81AD8291800CCBF6D /* Dobby.framework */; }; - DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */; }; + DC7F6B1E1AD82B8C00CCBF6D /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1D1AD82B8C00CCBF6D /* Behavior.swift */; }; DC7F6B201AD82D3800CCBF6D /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1F1AD82D3800CCBF6D /* Mock.swift */; }; DC7F6B251AD830A500CCBF6D /* MockSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B241AD830A500CCBF6D /* MockSpec.swift */; }; - DC7F6B271AD830B500CCBF6D /* StubSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B261AD830B500CCBF6D /* StubSpec.swift */; }; + DC7F6B271AD830B500CCBF6D /* BehaviorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B261AD830B500CCBF6D /* BehaviorSpec.swift */; }; DC83356C1B56661900569709 /* Disposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC83356B1B56661900569709 /* Disposable.swift */; }; DC83356D1B56661900569709 /* Disposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC83356B1B56661900569709 /* Disposable.swift */; }; DC8452231ADBF5CC006AC5D4 /* Dobby.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC8452181ADBF5CC006AC5D4 /* Dobby.framework */; }; DC84523E1ADBF73C006AC5D4 /* Dobby.h in Headers */ = {isa = PBXBuildFile; fileRef = DC7F6AFD1AD8291800CCBF6D /* Dobby.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC84523F1ADBF73C006AC5D4 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1F1AD82D3800CCBF6D /* Mock.swift */; }; - DC8452401ADBF73C006AC5D4 /* Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */; }; + DC8452401ADBF73C006AC5D4 /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B1D1AD82B8C00CCBF6D /* Behavior.swift */; }; DC8452421ADBF743006AC5D4 /* MockSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B241AD830A500CCBF6D /* MockSpec.swift */; }; - DC8452431ADBF743006AC5D4 /* StubSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B261AD830B500CCBF6D /* StubSpec.swift */; }; + DC8452431ADBF743006AC5D4 /* BehaviorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7F6B261AD830B500CCBF6D /* BehaviorSpec.swift */; }; DCDDA9301BED650100BB5228 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCDDA92F1BED650100BB5228 /* Nimble.framework */; }; DCDDA9321BED655500BB5228 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCDDA9311BED655500BB5228 /* Quick.framework */; }; DCDDA9351BED657C00BB5228 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCDDA92F1BED650100BB5228 /* Nimble.framework */; }; @@ -89,10 +89,10 @@ DC7F6AFD1AD8291800CCBF6D /* Dobby.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Dobby.h; sourceTree = ""; }; DC7F6B031AD8291800CCBF6D /* DobbyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DobbyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DC7F6B091AD8291800CCBF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stub.swift; sourceTree = ""; }; + DC7F6B1D1AD82B8C00CCBF6D /* Behavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Behavior.swift; sourceTree = ""; }; DC7F6B1F1AD82D3800CCBF6D /* Mock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; DC7F6B241AD830A500CCBF6D /* MockSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSpec.swift; sourceTree = ""; }; - DC7F6B261AD830B500CCBF6D /* StubSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubSpec.swift; sourceTree = ""; }; + DC7F6B261AD830B500CCBF6D /* BehaviorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BehaviorSpec.swift; sourceTree = ""; }; DC83356B1B56661900569709 /* Disposable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Disposable.swift; sourceTree = ""; }; DC8452181ADBF5CC006AC5D4 /* Dobby.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Dobby.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC8452221ADBF5CC006AC5D4 /* DobbyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DobbyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -196,13 +196,13 @@ DC7F6AFA1AD8291800CCBF6D /* Dobby */ = { isa = PBXGroup; children = ( + DC7F6B1D1AD82B8C00CCBF6D /* Behavior.swift */, DC83356B1B56661900569709 /* Disposable.swift */, DC7F6AFD1AD8291800CCBF6D /* Dobby.h */, DC457AD81B5552CA00919032 /* Matcher.swift */, DC7F6B1F1AD82D3800CCBF6D /* Mock.swift */, DC4D883D1E1CFCAE00FAE93F /* Pattern.swift */, DC4D88371E1CECE100FAE93F /* Recorder.swift */, - DC7F6B1D1AD82B8C00CCBF6D /* Stub.swift */, DC3CFE761ADE895E004728C2 /* Extensions */, DC7F6AFB1AD8291800CCBF6D /* Supporting Files */, ); @@ -220,11 +220,11 @@ DC7F6B071AD8291800CCBF6D /* DobbyTests */ = { isa = PBXGroup; children = ( + DC7F6B261AD830B500CCBF6D /* BehaviorSpec.swift */, DCE4080D1B6161000009E0B4 /* MatcherSpec.swift */, DC7F6B241AD830A500CCBF6D /* MockSpec.swift */, DC4D88431E1F9A5500FAE93F /* PatternSpec.swift */, DC4D883A1E1CF8D700FAE93F /* RecorderSpec.swift */, - DC7F6B261AD830B500CCBF6D /* StubSpec.swift */, DC7F6B081AD8291800CCBF6D /* Supporting Files */, ); path = DobbyTests; @@ -511,7 +511,7 @@ DC457AD91B5552CA00919032 /* Matcher.swift in Sources */, DC3CFE781ADE895E004728C2 /* SwiftExtensions.swift in Sources */, DC83356C1B56661900569709 /* Disposable.swift in Sources */, - DC7F6B1E1AD82B8C00CCBF6D /* Stub.swift in Sources */, + DC7F6B1E1AD82B8C00CCBF6D /* Behavior.swift in Sources */, DC7F6B201AD82D3800CCBF6D /* Mock.swift in Sources */, DC4D88391E1CECE100FAE93F /* Recorder.swift in Sources */, DC4D883F1E1CFCAE00FAE93F /* Pattern.swift in Sources */, @@ -526,7 +526,7 @@ DC7F6B251AD830A500CCBF6D /* MockSpec.swift in Sources */, DC4D883C1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, DC4D88451E1F9A5500FAE93F /* PatternSpec.swift in Sources */, - DC7F6B271AD830B500CCBF6D /* StubSpec.swift in Sources */, + DC7F6B271AD830B500CCBF6D /* BehaviorSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -537,7 +537,7 @@ DC457ADA1B5552CA00919032 /* Matcher.swift in Sources */, DC3CFE791ADE895E004728C2 /* SwiftExtensions.swift in Sources */, DC83356D1B56661900569709 /* Disposable.swift in Sources */, - DC8452401ADBF73C006AC5D4 /* Stub.swift in Sources */, + DC8452401ADBF73C006AC5D4 /* Behavior.swift in Sources */, DC84523F1ADBF73C006AC5D4 /* Mock.swift in Sources */, DC4D88381E1CECE100FAE93F /* Recorder.swift in Sources */, DC4D883E1E1CFCAE00FAE93F /* Pattern.swift in Sources */, @@ -552,7 +552,7 @@ DC8452421ADBF743006AC5D4 /* MockSpec.swift in Sources */, DC4D883B1E1CF8D700FAE93F /* RecorderSpec.swift in Sources */, DC4D88441E1F9A5500FAE93F /* PatternSpec.swift in Sources */, - DC8452431ADBF743006AC5D4 /* StubSpec.swift in Sources */, + DC8452431ADBF743006AC5D4 /* BehaviorSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Dobby/Stub.swift b/Dobby/Behavior.swift similarity index 73% rename from Dobby/Stub.swift rename to Dobby/Behavior.swift index c314ccc..b344cbe 100644 --- a/Dobby/Stub.swift +++ b/Dobby/Behavior.swift @@ -13,29 +13,29 @@ fileprivate struct Reaction { } } -/// A stub error. -public enum StubError: Error { +/// A behavior error. +public enum BehaviorError: Error { /// An interaction with the associated value was unexpected. case unexpectedInteraction(Value) } -/// A stub that, when invoked, returns a value based on the set up reactions, -/// or, if an interaction is unexpected, throws an error. -public final class Stub { +/// A behavior that, when invoked, returns a value based on the set up +/// reactions, or, if an interaction is unexpected, throws an error. +public final class Behavior { /// The current (next) identifier for reactions. private var currentIdentifier: UInt = 0 - /// The reactions of this stub. + /// The reactions of this behavior. private var reactions: [(identifier: UInt, reaction: Reaction)] = [] - /// Creates a new stub. + /// Creates a new behavior. public init() { } - /// Modifies the reactions of this stub, forwarding invocations to the - /// given handler and returning its return value if the given matcher - /// does match an interaction. + /// Modifies the reactions of this behavior, forwarding invocations to the + /// given handler and returning its return value if the given matcher does + /// match an interaction. /// /// Returns a disposable that, when disposed, removes this reaction. @discardableResult @@ -57,24 +57,24 @@ public final class Stub { } } - /// Modifies the reactions of this stub, returning the given value upon + /// Modifies the reactions of this behavior, returning the given value upon /// invocation if the given matcher does match an interaction. /// /// Returns a disposable that, when disposed, removes this reaction. /// - /// - SeeAlso: `Stub.on(matcher: Matcher, invoke: (Value) -> ReturnValue) -> Disposable` + /// - SeeAlso: `Behavior.on(matcher: Matcher, invoke: (Value) -> ReturnValue) -> Disposable` @discardableResult public func on(_ matcher: Matcher, return value: ReturnValue) -> Disposable where Matcher.ValueType == Value { return on(matcher) { _ in value } } - /// Invokes this stub, returning a value based on the set up reactions, or, - /// if the given interaction is unexpected, throwing an error. + /// Invokes this behavior, returning a value based on the set up reactions, + /// or, if the given interaction is unexpected, throwing an error. /// /// Reactions are matched in order, i.e., the handler associated with the /// first matcher that matches the given interaction is invoked. /// - /// - Throws: `StubError.unexpectedInteraction(Value)` if the given + /// - Throws: `BehaviorError.unexpectedInteraction(Value)` if the given /// interaction is unexpected. @discardableResult public func invoke(_ value: Value) throws -> ReturnValue { @@ -84,6 +84,6 @@ public final class Stub { } } - throw StubError.unexpectedInteraction(value) + throw BehaviorError.unexpectedInteraction(value) } } diff --git a/DobbyTests/StubSpec.swift b/DobbyTests/BehaviorSpec.swift similarity index 54% rename from DobbyTests/StubSpec.swift rename to DobbyTests/BehaviorSpec.swift index 054fc27..05c552a 100644 --- a/DobbyTests/StubSpec.swift +++ b/DobbyTests/BehaviorSpec.swift @@ -3,38 +3,38 @@ import Nimble import Dobby -class StubSpec: QuickSpec { +class BehaviorSpec: QuickSpec { override func spec() { - var stub: Stub<(Int, Int), Int>! + var behavior: Behavior<(Int, Int), Int>! beforeEach { - stub = Stub() + behavior = Behavior() } describe("Invocation") { it("returns the correct value") { - stub.on(matches((4, 3)), return: 9) - stub.on(matches((any(), any()))) { $0.0 + $0.1 } - expect(try! stub.invoke((4, 3))).to(equal(9)) - expect(try! stub.invoke((4, 4))).to(equal(8)) + behavior.on(matches((4, 3)), return: 9) + behavior.on(matches((any(), any()))) { $0.0 + $0.1 } + expect(try! behavior.invoke((4, 3))).to(equal(9)) + expect(try! behavior.invoke((4, 4))).to(equal(8)) } it("returns the correct value after disposal") { - let disposable1 = stub.on(matches((4, 3)), return: 9) - let disposable2 = stub.on(matches((any(), any()))) { $0.0 + $0.1 } + let disposable1 = behavior.on(matches((4, 3)), return: 9) + let disposable2 = behavior.on(matches((any(), any()))) { $0.0 + $0.1 } disposable1.dispose() expect(disposable1.disposed).to(beTrue()) expect(disposable2.disposed).to(beFalse()) - expect(try! stub.invoke((4, 3))).to(equal(7)) + expect(try! behavior.invoke((4, 3))).to(equal(7)) } it("throws an exception if an interaction is unexpected") { do { - try stub.invoke((5, 6)) - } catch StubError<(Int, Int), Int>.unexpectedInteraction(let interaction) { + try behavior.invoke((5, 6)) + } catch BehaviorError<(Int, Int), Int>.unexpectedInteraction(let interaction) { expect(interaction.0).to(equal(5)) expect(interaction.1).to(equal(6)) return; From 0e6443900b241de067e10524c3886c5fc5362901 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Thu, 16 Mar 2017 14:46:17 +0100 Subject: [PATCH 10/11] Add value recording to behavior --- Dobby/Behavior.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Dobby/Behavior.swift b/Dobby/Behavior.swift index b344cbe..638b291 100644 --- a/Dobby/Behavior.swift +++ b/Dobby/Behavior.swift @@ -28,6 +28,9 @@ public final class Behavior { /// The reactions of this behavior. private var reactions: [(identifier: UInt, reaction: Reaction)] = [] + /// The recorder of this behavior. + fileprivate let recorder: Recorder = Recorder() + /// Creates a new behavior. public init() { @@ -78,6 +81,8 @@ public final class Behavior { /// interaction is unexpected. @discardableResult public func invoke(_ value: Value) throws -> ReturnValue { + recorder.record(value) + for (_, reaction) in reactions { if reaction.matcher.matches(value) { return reaction.handler(value) @@ -87,3 +92,13 @@ public final class Behavior { throw BehaviorError.unexpectedInteraction(value) } } + +extension Behavior: ValueRecording { + public var interactions: AnyRandomAccessCollection { + return recorder.interactions + } + + public func valueForInteraction(at index: Int) -> Value { + return recorder.valueForInteraction(at: index) + } +} From d0b46075d49866f184cf2b649ba910d6e9581058 Mon Sep 17 00:00:00 2001 From: Felix Jendrusch Date: Thu, 16 Mar 2017 16:20:00 +0100 Subject: [PATCH 11/11] Allow throwing reaction handlers --- Dobby/Behavior.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dobby/Behavior.swift b/Dobby/Behavior.swift index 638b291..964fa6a 100644 --- a/Dobby/Behavior.swift +++ b/Dobby/Behavior.swift @@ -4,10 +4,10 @@ fileprivate struct Reaction { fileprivate let matcher: Matcher /// The handler of this reaction. - fileprivate let handler: (Value) -> ReturnValue + fileprivate let handler: (Value) throws -> ReturnValue /// Initializes a new reaction with the given matcher and handler. - fileprivate init(matcher: Matcher, handler: @escaping (Value) -> ReturnValue) where Matcher.ValueType == Value { + fileprivate init(matcher: Matcher, handler: @escaping (Value) throws -> ReturnValue) where Matcher.ValueType == Value { self.matcher = matcher.matcher() self.handler = handler } @@ -42,7 +42,7 @@ public final class Behavior { /// /// Returns a disposable that, when disposed, removes this reaction. @discardableResult - public func on(_ matcher: Matcher, invoke handler: @escaping (Value) -> ReturnValue) -> Disposable where Matcher.ValueType == Value { + public func on(_ matcher: Matcher, invoke handler: @escaping (Value) throws -> ReturnValue) -> Disposable where Matcher.ValueType == Value { currentIdentifier += 1 let identifier = currentIdentifier @@ -85,7 +85,7 @@ public final class Behavior { for (_, reaction) in reactions { if reaction.matcher.matches(value) { - return reaction.handler(value) + return try reaction.handler(value) } }