Skip to content

Commit

Permalink
Merge pull request #16 from RedMadRobot/session_id
Browse files Browse the repository at this point in the history
Support for parallel test runs
  • Loading branch information
Alexander-Ignition authored Dec 8, 2021
2 parents 554d68b + cad01c7 commit c25aef5
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 12 deletions.
8 changes: 6 additions & 2 deletions Packages/CatbirdAPI/Sources/CatbirdAPI/Catbird.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ public final class Catbird {
/// Network session.
private let session: URLSession

public init(url: URL = localhost, session: URLSession = session) {
/// Unique catbird id for parallel test running.
public var parallelId: String?

public init(url: URL = localhost, session: URLSession = session, parallelId: String? = nil) {
self.url = url
self.session = session
self.parallelId = parallelId
}

// MARK: - Public
Expand All @@ -36,7 +40,7 @@ public final class Catbird {
/// - Returns: Session task.
@discardableResult
public func send(_ action: CatbirdAction, completion: @escaping (Error?) -> Void) -> URLSessionTask {
let request = try! action.makeRequest(to: url)
let request = try! action.makeRequest(to: url, parallelId: parallelId)
return dataTask(request, completion: completion)
}

Expand Down
14 changes: 10 additions & 4 deletions Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,22 @@ extension CatbirdAction {
// MARK: - CatbirdAction + URLRequest

extension CatbirdAction {
/// Header name for parallel ID.
public static let parallelIdHeaderField = "X-Catbird-Parallel-Id"

private static let encoder = JSONEncoder()

/// Create a new `URLRequest`.
///
/// - Parameter url: Catbird server base url.
/// - Returns: Request to mock server.
func makeRequest(to url: URL) throws -> URLRequest {
func makeRequest(to url: URL, parallelId: String? = nil) throws -> URLRequest {
var request = URLRequest(url: url.appendingPathComponent("catbird/api/mocks"))
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
if let parallelId = parallelId {
request.addValue(parallelId, forHTTPHeaderField: CatbirdAction.parallelIdHeaderField)
}
request.httpBody = try CatbirdAction.encoder.encode(self)
return request
}
Expand All @@ -70,14 +76,14 @@ enum CatbirdActionType: String, Codable {
}

extension CatbirdAction: Codable {
enum CondingKeys: String, CodingKey {
enum CodingKeys: String, CodingKey {
case type
case pattern
case response
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CondingKeys.self)
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(CatbirdActionType.self, forKey: .type)

switch type {
Expand All @@ -94,7 +100,7 @@ extension CatbirdAction: Codable {
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CondingKeys.self)
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .update(let pattern, let response):
try container.encode(CatbirdActionType.update, forKey: .type)
Expand Down
55 changes: 55 additions & 0 deletions Packages/CatbirdAPI/Tests/CatbirdAPITests/CatbirdActionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import XCTest

final class CatbirdActionTests: XCTestCase {
private let decoder = JSONDecoder()
private var parallelId: String { name }
private let baseURL = URL(string: "https://example.com")!
private var expectedURL: URL {
baseURL.appendingPathComponent("catbird/api/mocks")
Expand All @@ -20,6 +21,25 @@ final class CatbirdActionTests: XCTestCase {
// Then
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, expectedURL)
XCTAssertNil(request.value(forHTTPHeaderField: CatbirdAction.parallelIdHeaderField))
XCTAssertEqual(try request.httpBody.map {
try decoder.decode(CatbirdAction.self, from: $0)
}, action)
}

func testUpdateWithParallelId() throws {
// Given
let pattern = RequestPattern(method: .GET, url: "/about")
let response = ResponseMock(status: 200, body: Data("hello".utf8))
let action = CatbirdAction.update(pattern, response)

// When
let request = try XCTUnwrap(try action.makeRequest(to: baseURL, parallelId: parallelId))

// Then
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, expectedURL)
XCTAssertEqual(request.value(forHTTPHeaderField: CatbirdAction.parallelIdHeaderField), parallelId)
XCTAssertEqual(try request.httpBody.map {
try decoder.decode(CatbirdAction.self, from: $0)
}, action)
Expand All @@ -36,6 +56,24 @@ final class CatbirdActionTests: XCTestCase {
// Then
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, expectedURL)
XCTAssertNil(request.value(forHTTPHeaderField: CatbirdAction.parallelIdHeaderField))
XCTAssertEqual(try request.httpBody.map {
try decoder.decode(CatbirdAction.self, from: $0)
}, action)
}

func testRemoveWithParallelId() throws {
// Given
let pattern = RequestPattern(method: .GET, url: "/about")
let action = CatbirdAction.remove(pattern)

// When
let request = try XCTUnwrap(try action.makeRequest(to: baseURL, parallelId: parallelId))

// Then
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, expectedURL)
XCTAssertEqual(request.value(forHTTPHeaderField: CatbirdAction.parallelIdHeaderField), parallelId)
XCTAssertEqual(try request.httpBody.map {
try decoder.decode(CatbirdAction.self, from: $0)
}, action)
Expand All @@ -51,6 +89,23 @@ final class CatbirdActionTests: XCTestCase {
// Then
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, expectedURL)
XCTAssertNil(request.value(forHTTPHeaderField: CatbirdAction.parallelIdHeaderField))
XCTAssertEqual(try request.httpBody.map {
try decoder.decode(CatbirdAction.self, from: $0)
}, action)
}

func testRemoveAllWithParallelId() throws {
// Given
let action = CatbirdAction.removeAll

// When
let request = try XCTUnwrap(try action.makeRequest(to: baseURL, parallelId: parallelId))

// Then
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, expectedURL)
XCTAssertEqual(request.value(forHTTPHeaderField: CatbirdAction.parallelIdHeaderField), parallelId)
XCTAssertEqual(try request.httpBody.map {
try decoder.decode(CatbirdAction.self, from: $0)
}, action)
Expand Down
23 changes: 23 additions & 0 deletions Packages/CatbirdAPI/Tests/CatbirdAPITests/CatbirdTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ final class CatbirdTests: XCTestCase {
XCTAssertEqual(requests.first, try action.makeRequest(to: url))
}

func testParallelId() {
// Given
let parallelId = name
catbird.parallelId = parallelId
let actions: [CatbirdAction] = [
CatbirdAction.remove(BookMock.first),
CatbirdAction.remove(BookMock.first),
CatbirdAction.removeAll
]
Network.result = .success(response(status: 200))

// When
for action in actions {
XCTAssertNoThrow(try catbird.send(action))
}

// Then
XCTAssertEqual(requests.count, 3)
XCTAssertEqual(requests, try actions.map { action in
try action.makeRequest(to: url, parallelId: parallelId)
})
}

@available(iOS 7, macOS 10.13, *)
func testURLError() {
// Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,46 @@ final class InMemoryResponseStore: ResponseStore {
}

func perform(_ action: CatbirdAction, for request: Request) -> EventLoopFuture<Response> {
let parallelId = request.headers.first(name: CatbirdAction.parallelIdHeaderField)

let status = _queue.sync { () -> HTTPStatus in
switch action {
case .update(let pattern, let mock):
case .update(var pattern, let mock):
pattern.setParallelId(parallelId)
let item = ResponseStoreItem(pattern: pattern, mock: mock)
if let index = _items.firstIndex(where: { $0.pattern == pattern }) {
_items[index] = item
} else {
_items.append(item)
}
return .created
case .remove(let pattern):
case .remove(var pattern):
pattern.setParallelId(parallelId)
_items.removeAll(where: { $0.pattern == pattern })
return .noContent
case .removeAll:
_items.removeAll(keepingCapacity: true)
_removeAll(parallelId: parallelId)
return .noContent
}
}
return request.eventLoop.makeSucceededFuture(Response(status: status))
}

// MARK: - Private

private func _removeAll(parallelId: String?) {
if let parallelId = parallelId {
_items.removeAll(where: { item in
item.pattern.headers[CatbirdAction.parallelIdHeaderField]?.match(parallelId) == true
})
} else {
_items.removeAll(keepingCapacity: true)
}
}
}

extension RequestPattern {
fileprivate mutating func setParallelId(_ parallelId: String?) {
headers[CatbirdAction.parallelIdHeaderField] = parallelId.map(PatternMatch.equal)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ class RequestTestCase: AppTestCase {
let eventLoop = app.eventLoopGroup.next()
request = Request(application: app, on: eventLoop)
}

func makeRequest() -> Request {
Request(application: app, on: app.eventLoopGroup.next())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import XCTVapor

extension Application {

func perform(_ action: CatbirdAction, file: StaticString = #file, line: UInt = #line) throws {
let request = try action.makeRequest(to: URL(string: "/")!)
func perform(_ action: CatbirdAction, parallelId: String? = nil, file: StaticString = #file, line: UInt = #line) throws {
let request = try action.makeRequest(to: URL(string: "/")!, parallelId: parallelId)
let method = try XCTUnwrap(request.httpMethod.map { HTTPMethod(rawValue: $0) }, file: file, line: line)
let path = try XCTUnwrap(request.url?.path, file: file, line: line)
var headers = HTTPHeaders()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,10 @@ enum BookMock: CatbirdMockConvertible {
var item: ResponseStoreItem {
ResponseStoreItem(pattern: pattern, mock: response)
}

func item(parallelId: String) -> ResponseStoreItem {
var pattern = self.pattern
pattern.headers["X-Catbird-Parallel-Id"] = .equal(parallelId)
return ResponseStoreItem(pattern: pattern, mock: response)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import XCTest
final class InMemoryResponseStoreTests: RequestTestCase {

private var store: InMemoryResponseStore!
private var parallelId: String { name }

override func setUp() {
super.setUp()
store = InMemoryResponseStore()
XCTAssertEqual(store.items, [])
}

private func perform(_ action: CatbirdAction, file: StaticString = #file, line: UInt = #line) {
private func perform(_ action: CatbirdAction, parallelId: String? = nil, file: StaticString = #file, line: UInt = #line) {
let request = makeRequest()
if let parallelId = parallelId {
request.headers.add(name: "X-Catbird-Parallel-Id", value: parallelId)
}
let future = store.perform(action, for: request)
XCTAssertEqual(try future.wait().status, action.expectedStatus, file: file, line: line)
}
Expand All @@ -32,6 +37,22 @@ final class InMemoryResponseStoreTests: RequestTestCase {
XCTAssertEqual(store.items, mocks.map(\.item))
}

func testPerformAddWithParallelId() throws {
// Given
let mocks = BookMock.mocks
mocks.forEach { perform(.add($0)) }
var items = BookMock.mocks.map(\.item)

// When
let createA = BookMock.create(name: "A")
perform(.add(createA), parallelId: parallelId)
items.append(createA.item(parallelId: parallelId))

// Then
XCTAssertEqual(store.items.count, 5)
XCTAssertEqual(store.items, items)
}

func testPerformUpdate() {
// Given
let createA = BookMock.create(name: "A")
Expand All @@ -45,6 +66,24 @@ final class InMemoryResponseStoreTests: RequestTestCase {
XCTAssertEqual(store.items, [createA.item])
}

func testPerformUpdateWithParallelId() {
// Given
let createA = BookMock.create(name: "A")
let createB = BookMock.create(name: "B")
perform(.add(createA))
perform(.add(createB), parallelId: parallelId)


// When
perform(.add(createA), parallelId: parallelId)

// Then
XCTAssertEqual(store.items, [
createA.item,
createA.item(parallelId: name)
])
}

func testPerformRemove() {
// Given
let first = BookMock.first
Expand All @@ -59,9 +98,23 @@ final class InMemoryResponseStoreTests: RequestTestCase {
XCTAssertEqual(store.items, [second.item])
}

func testPerformRemoveWithParallelId() {
// Given
let first = BookMock.first
perform(.add(first))
perform(.add(first), parallelId: parallelId)

// When
perform(.remove(first), parallelId: parallelId)

// Then
XCTAssertEqual(store.items, [first.item])
}

func testPerformRemoveAll() {
// Given
BookMock.mocks.forEach { perform(.add($0)) }
perform(.add(BookMock.create(name: "Z")), parallelId: parallelId)

// When
perform(.removeAll)
Expand All @@ -70,6 +123,19 @@ final class InMemoryResponseStoreTests: RequestTestCase {
XCTAssertEqual(store.items, [])
}

func testPerformRemoveAllWithParallelId() {
// Given
BookMock.mocks.forEach { perform(.add($0)) }
perform(.add(BookMock.create(name: "1")), parallelId: parallelId)
perform(.add(BookMock.create(name: "2")), parallelId: parallelId)

// When
perform(.removeAll, parallelId: parallelId)

// Then
XCTAssertEqual(store.items.count, BookMock.mocks.count)
}

// MARK: - Response for Request

func testResponseForRequest() throws {
Expand Down
Loading

0 comments on commit c25aef5

Please sign in to comment.