* moved CI to Xcode 14.2

* added flaky-tests and clusters api support
* rewrote transport tests. they might work?
* more sendable conformances
This commit is contained in:
Aaron Sky 2023-06-18 10:28:24 -04:00
parent 0a12829df3
commit 23cea0da0e
29 changed files with 1531 additions and 117 deletions

View File

@ -11,15 +11,13 @@ jobs:
strategy: strategy:
matrix: matrix:
xcode: xcode:
- '14.0.1' - '14.2'
env: env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Run tests - name: Run tests
run: make test-library-xcode run: make test-library-xcode
- name: Compile documentation
run: make test-docs
library-linux: library-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -33,8 +31,8 @@ jobs:
examples: examples:
runs-on: macos-12 runs-on: macos-12
env: env:
DEVELOPER_DIR: /Applications/Xcode_14.0.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Run tests - name: Run tests
run: make test-examples run: make build-examples

View File

@ -1,4 +1,5 @@
// swift-tools-version:5.6 // swift-tools-version:5.6
import PackageDescription import PackageDescription
let package = Package( let package = Package(

View File

@ -32,7 +32,8 @@ extension BuildkiteClient: AsyncMiddleware {
} }
guard guard
let secretKey = Environment let secretKey =
Environment
.get("BUILDKITE_WEBHOOK_SECRET")? .get("BUILDKITE_WEBHOOK_SECRET")?
.data(using: .utf8) .data(using: .utf8)
else { else {
@ -42,12 +43,14 @@ extension BuildkiteClient: AsyncMiddleware {
) )
} }
let replayLimit = Environment let replayLimit =
Environment
.get("BUILDKITE_WEBHOOK_REPLAY_LIMIT") .get("BUILDKITE_WEBHOOK_REPLAY_LIMIT")
.flatMap(TimeInterval.init) .flatMap(TimeInterval.init)
if let signature = request.headers.buildkiteSignature, if let signature = request.headers.buildkiteSignature,
let payload = request.body.data { let payload = request.body.data
{
try validateWebhookPayload( try validateWebhookPayload(
signatureHeader: signature, signatureHeader: signature,
body: Data(buffer: payload), body: Data(buffer: payload),

View File

@ -15,7 +15,7 @@ import Vapor
let app = try Application(.detect()) let app = try Application(.detect())
defer { app.shutdown() } defer { app.shutdown() }
let buildkite = BuildkiteClient(/* transport: app.client */) let buildkite = BuildkiteClient( /* transport: app.client */)
app.group(buildkite) { app.group(buildkite) {
$0.post("buildkite_webhook") { req in $0.post("buildkite_webhook") { req in

View File

@ -14,66 +14,75 @@ FORMAT_PATHS := $(GIT_REPO_TOPLEVEL)/Examples $(GIT_REPO_TOPLEVEL)/Package.swift
# Tasks # Tasks
.PHONY: default
default: test-all default: test-all
test-all: test-library test-library-xcode test-examples .PHONY: test-all
test-all: test-library test-library-xcode build-examples
.PHONY: test-library
test-library: test-library:
swift test --parallel swift test --parallel
test-library-xcode: .PHONY: test-library-xcode
test-library-xcode: test-library-xcode-ios test-library-xcode-macos test-library-xcode-tvos test-library-xcode-watchos
.PHONY: test-library-xcode-ios
test-library-xcode-ios:
xcodebuild test \ xcodebuild test \
-scheme Buildkite \ -scheme Buildkite \
-destination "$(DESTINATION_PLATFORM_IOS_SIMULATOR)" \ -destination "$(DESTINATION_PLATFORM_IOS_SIMULATOR)" \
-quiet -quiet
.PHONY: test-library-xcode-macos
test-library-xcode-macos:
xcodebuild test \ xcodebuild test \
-scheme Buildkite \ -scheme Buildkite \
-destination "$(DESTINATION_PLATFORM_MACOS)" \ -destination "$(DESTINATION_PLATFORM_MACOS)" \
-quiet -quiet
.PHONY: test-library-xcode-tvos
test-library-xcode-tvos:
xcodebuild test \ xcodebuild test \
-scheme Buildkite \ -scheme Buildkite \
-destination "$(DESTINATION_PLATFORM_TVOS_SIMULATOR)" \ -destination "$(DESTINATION_PLATFORM_TVOS_SIMULATOR)" \
-quiet -quiet
.PHONY: test-library-xcode-watchos
test-library-xcode-watchos:
xcodebuild \ xcodebuild \
-scheme Buildkite \ -scheme Buildkite \
-destination "$(DESTINATION_PLATFORM_WATCHOS_SIMULATOR)" \ -destination "$(DESTINATION_PLATFORM_WATCHOS_SIMULATOR)" \
-quiet -quiet
test-examples: .PHONY: build-examples
xcodebuild build \ build-examples: build-examples-all
-scheme simple \
-destination "$(DESTINATION_PLATFORM_MACOS)" \
-quiet
xcodebuild build \
-scheme graphql \
-destination "$(DESTINATION_PLATFORM_MACOS)" \
-quiet
xcodebuild build \
-scheme advanced-authorization \
-destination "$(DESTINATION_PLATFORM_MACOS)" \
-quiet
xcodebuild build \
-scheme test-analytics \
-destination "$(DESTINATION_PLATFORM_MACOS)" \
-quiet
xcodebuild build \
-scheme webhooks \
-destination "$(DESTINATION_PLATFORM_MACOS)" \
-quiet
DOC_WARNINGS := $(shell xcodebuild clean docbuild \ .PHONY: build-examples-all
-scheme Buildkite \ build-examples-all:
-destination "$(DESTINATION_PLATFORM_MACOS)" \ swift build --package-path Examples
-quiet \
2>&1 \
| grep "couldn't be resolved to known documentation" \
| sed 's|$(PWD)|.|g' \
| tr '\n' '\1')
test-docs:
@test "$(DOC_WARNINGS)" = "" \
|| (echo "xcodebuild docbuild failed:\n\n$(DOC_WARNINGS)" | tr '\1' '\n' \
&& exit 1)
.PHONY: build-examples-simple
build-examples-simple:
swift build --package-path Examples --product simple
.PHONY: build-examples-graphql
build-examples-graphql:
swift build --package-path Examples --product graphql
.PHONY: build-examples-advanced-authorization
build-examples-advanced-authorization:
swift build --package-path Examples --product advanced-authorization
.PHONY: build-examples-test-analytics
build-examples-test-analytics:
swift build --package-path Examples --product test-analytics
.PHONY: build-examples-webhooks
build-examples-webhooks:
swift build --package-path Examples --product webhooks
.PHONY: format
format: format:
$(SWIFT_FORMAT_BIN) \ $(SWIFT_FORMAT_BIN) \
--configuration $(SWIFT_FORMAT_CONFIG_FILE) \ --configuration $(SWIFT_FORMAT_CONFIG_FILE) \
@ -82,11 +91,10 @@ format:
--recursive \ --recursive \
$(FORMAT_PATHS) $(FORMAT_PATHS)
.PHONY: lint
lint: lint:
$(SWIFT_FORMAT_BIN) lint \ $(SWIFT_FORMAT_BIN) lint \
--configuration $(SWIFT_FORMAT_CONFIG_FILE) \ --configuration $(SWIFT_FORMAT_CONFIG_FILE) \
--ignore-unparsable-files \ --ignore-unparsable-files \
--recursive \ --recursive \
$(FORMAT_PATHS) $(FORMAT_PATHS)
.PHONY: format lint test-all test-library test-library-xcode test-examples test-docs

View File

@ -17,8 +17,7 @@ let package = Package(
) )
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "2.0.0"))
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
], ],
targets: [ targets: [
.target( .target(

View File

@ -73,7 +73,7 @@ public struct Artifact: Codable, Equatable, Hashable, Identifiable, Sendable {
case deleted case deleted
} }
public struct URLs: Codable, Equatable { public struct URLs: Codable, Equatable, Sendable {
public var url: URL public var url: URL
public init( public init(

View File

@ -0,0 +1,90 @@
//
// Cluster.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// A cluster is an isolated set of agents and pipelines within an organization.
public struct Cluster: Codable, Equatable, Hashable, Identifiable, Sendable {
/// ID of the cluster.
public var id: UUID
/// ID of the cluster to be used with the GraphQL API.
public var graphqlId: String
/// ID of the cluster's default queue. Agents that connect to the cluster without specifying a queue will accept jobs from this queue.
public var defaultQueueId: UUID
/// Name of the cluster.
public var name: String
/// Description of the cluster.
public var description: String
/// Emoji for the cluster using the emoji syntax.
public var emoji: String
/// Color hex code for the cluster.
public var color: String
/// Followable URL to fetch this specific cluster.
public var url: Followable<Cluster.Resources.Get>
/// Human-readable URL of this cluster in the Buildkite dashboard.
public var webURL: URL
/// Followable URL to fetch this cluster's queues.
public var queuesURL: Followable<ClusterQueue.Resources.List>
/// Followable URL to fetch this cluster's default queue.
public var defaultQueueURL: Followable<ClusterQueue.Resources.Get>
/// When the cluster was created.
public var createdAt: Date
/// User who created the cluster.
public var createdBy: User?
public init(
id: UUID,
graphqlId: String,
defaultQueueId: UUID,
name: String,
description: String,
emoji: String,
color: String,
url: Followable<Cluster.Resources.Get>,
webURL: URL,
queuesURL: Followable<ClusterQueue.Resources.List>,
defaultQueueURL: Followable<ClusterQueue.Resources.Get>,
createdAt: Date,
createdBy: User? = nil
) {
self.id = id
self.graphqlId = graphqlId
self.defaultQueueId = defaultQueueId
self.name = name
self.description = description
self.emoji = emoji
self.color = color
self.url = url
self.webURL = webURL
self.queuesURL = queuesURL
self.defaultQueueURL = defaultQueueURL
self.createdAt = createdAt
self.createdBy = createdBy
}
private enum CodingKeys: String, CodingKey {
case id
case graphqlId = "graphql_id"
case defaultQueueId = "default_queue_id"
case name
case description
case emoji
case color
case url
case webURL = "web_url"
case queuesURL = "queues_url"
case defaultQueueURL = "default_queue_url"
case createdAt = "created_at"
case createdBy = "created_by"
}
}

View File

@ -0,0 +1,89 @@
//
// ClusterQueue.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// Cluster queues are discrete groups of agents within a cluster. Pipelines in that cluster can target cluster queues to run jobs on agents assigned to those queues.
public struct ClusterQueue: Codable, Equatable, Hashable, Identifiable, Sendable {
/// ID of the cluster.
public var id: UUID
/// ID of the cluster to be used with the GraphQL API.
public var graphqlId: String
/// The queue key.
public var key: String
/// Description of the queue.
public var description: String
/// Followable URL to fetch this specific queue.
public var url: Followable<ClusterQueue.Resources.Get>
/// Human-readable URL of this queue in the Buildkite dashboard.
public var webURL: URL
/// Followable URL to fetch the cluster the queue belongs to.
public var clusterURL: Followable<Cluster.Resources.Get>
/// Indicates whether the queue has paused dispatching jobs to associated agents.
public var dispatchPaused: Bool
/// User who paused the queue.
public var dispatchPausedBy: User?
/// When the queue was paused.
public var dispatchPausedAt: Date?
/// The note left when the queue was paused.
public var dispatchPausedNote: String?
/// When the queue was created.
public var createdAt: Date
/// User who created the queue.
public var createdBy: User?
public init(
id: UUID,
graphqlId: String,
key: String,
description: String,
url: Followable<ClusterQueue.Resources.Get>,
webURL: URL,
clusterURL: Followable<Cluster.Resources.Get>,
dispatchPaused: Bool,
dispatchPausedBy: User? = nil,
dispatchPausedAt: Date? = nil,
dispatchPausedNote: String? = nil,
createdAt: Date,
createdBy: User? = nil
) {
self.id = id
self.graphqlId = graphqlId
self.key = key
self.description = description
self.url = url
self.webURL = webURL
self.clusterURL = clusterURL
self.dispatchPaused = dispatchPaused
self.dispatchPausedBy = dispatchPausedBy
self.dispatchPausedAt = dispatchPausedAt
self.dispatchPausedNote = dispatchPausedNote
self.createdAt = createdAt
self.createdBy = createdBy
}
private enum CodingKeys: String, CodingKey {
case id
case graphqlId = "graphql_id"
case key
case description
case url
case webURL = "web_url"
case clusterURL = "cluster_url"
case dispatchPaused = "dispatch_paused"
case dispatchPausedBy = "dispatch_paused_by"
case dispatchPausedAt = "dispatch_paused_at"
case dispatchPausedNote = "dispatch_paused_note"
case createdAt = "created_at"
case createdBy = "created_by"
}
}

View File

@ -0,0 +1,67 @@
//
// ClusterToken.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// A cluster token is used to connect agents to a cluster.
public struct ClusterToken: Codable, Equatable, Hashable, Identifiable, Sendable {
/// ID of the cluster token.
public var id: UUID
/// ID of the token to be used with the GraphQL API.
public var graphqlId: String
/// Description of the token.
public var description: String
/// Followable URL to fetch this specific cluster token..
public var url: Followable<ClusterToken.Resources.Get>
/// Followable URL to the cluster the token belongs to..
public var clusterURL: Followable<Cluster.Resources.Get>
/// When the token was created.
public var createdAt: Date
/// User who created the token.
public var createdBy: User?
/// The token.
///
/// To ensure the security of tokens, the value is only included in the response for the request to create the token.
/// Subsequent responses do not contain the token value.
public var token: String?
public init(
id: UUID,
graphqlId: String,
description: String,
url: Followable<ClusterToken.Resources.Get>,
clusterURL: Followable<Cluster.Resources.Get>,
createdAt: Date,
createdBy: User? = nil,
token: String? = nil
) {
self.id = id
self.graphqlId = graphqlId
self.description = description
self.url = url
self.clusterURL = clusterURL
self.createdAt = createdAt
self.createdBy = createdBy
self.token = token
}
private enum CodingKeys: String, CodingKey {
case id
case graphqlId = "graphql_id"
case description
case url
case clusterURL = "cluster_url"
case createdAt = "created_at"
case createdBy = "created_by"
case token
}
}

View File

@ -0,0 +1,65 @@
//
// FlakyTest.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
/// Information about a flaky test that has been identified in a Test Analytics test suite.
public struct FlakyTest: Codable, Equatable, Hashable, Identifiable, Sendable {
/// ID of the flaky test.
public var id: UUID
/// ID of the user to be used with the GraphQL API.
public var graphqlId: String
/// Human-readable URL of this agent in the Buildkite dashboard.
public var webURL: URL
/// Scope of the test in the source code.
public var scope: String
/// Name of the test.
public var name: String
/// Path and line number to the test file.
public var location: String
/// Path to the test file.
public var fileName: String
/// Number of instances the test has "flaked".
public var instances: Int
/// The latest occurrence of the flake.
public var mostRecentInstanceAt: Date
public init(
id: UUID,
graphqlId: String,
webURL: URL,
scope: String,
name: String,
location: String,
fileName: String,
instances: Int,
mostRecentInstanceAt: Date
) {
self.id = id
self.graphqlId = graphqlId
self.webURL = webURL
self.scope = scope
self.name = name
self.location = location
self.fileName = fileName
self.instances = instances
self.mostRecentInstanceAt = mostRecentInstanceAt
}
private enum CodingKeys: String, CodingKey {
case id
case graphqlId = "graphql_id"
case webURL = "web_url"
case scope
case name
case location
case fileName = "file_name"
case instances
case mostRecentInstanceAt = "most_recent_instance_at"
}
}

View File

@ -99,7 +99,8 @@ extension BuildkiteClient {
private nonisolated func getTimestampAndSignature(_ header: String) throws -> (timestamp: String, signature: Data) { private nonisolated func getTimestampAndSignature(_ header: String) throws -> (timestamp: String, signature: Data) {
let parts: [String: String] = Dictionary( let parts: [String: String] = Dictionary(
uniqueKeysWithValues: header uniqueKeysWithValues:
header
.split(separator: ",") .split(separator: ",")
.compactMap { kv in .compactMap { kv in
let kvp = kv.split(separator: "=", maxSplits: 2) let kvp = kv.split(separator: "=", maxSplits: 2)
@ -132,11 +133,13 @@ extension BuildkiteClient {
private nonisolated func checkMAC(message: Data, signature: Data, secretKey: Data) throws { private nonisolated func checkMAC(message: Data, signature: Data, secretKey: Data) throws {
let key = SymmetricKey(data: secretKey) let key = SymmetricKey(data: secretKey)
guard HMAC<SHA256>.isValidAuthenticationCode( guard
signature, HMAC<SHA256>
authenticating: message, .isValidAuthenticationCode(
using: key signature,
) authenticating: message,
using: key
)
else { else {
throw WebhookValidationError.signatureRefused throw WebhookValidationError.signatureRefused
} }

View File

@ -52,7 +52,8 @@ extension Date {
init?( init?(
iso8601String: String iso8601String: String
) { ) {
guard let date = Formatters.iso8601WithFractionalSeconds.date(from: iso8601String) guard
let date = Formatters.iso8601WithFractionalSeconds.date(from: iso8601String)
?? Formatters.iso8601WithoutFractionalSeconds.date(from: iso8601String) ?? Formatters.iso8601WithoutFractionalSeconds.date(from: iso8601String)
else { return nil } else { return nil }
self = date self = date

View File

@ -31,7 +31,8 @@ public struct Page: Equatable, Hashable, Sendable {
} }
for link in header.split(separator: ",") { for link in header.split(separator: ",") {
let segments = link let segments =
link
.trimmingCharacters(in: .whitespacesAndNewlines) .trimmingCharacters(in: .whitespacesAndNewlines)
.split(separator: ";") .split(separator: ";")
guard guard

View File

@ -19,7 +19,7 @@ public enum ResourceError: Error, Equatable {
} }
/// Resource describes an endpoint on any of Buildkite's APIs. Do not implement this type. /// Resource describes an endpoint on any of Buildkite's APIs. Do not implement this type.
public protocol Resource { public protocol Resource: Sendable {
/// The input type. /// The input type.
associatedtype Body = Void associatedtype Body = Void
/// The return type. /// The return type.

View File

@ -24,7 +24,7 @@ import FoundationNetworking
/// if you need to read your token dynamically from the keychain, it may be more adventageous to do that as part of a /// if you need to read your token dynamically from the keychain, it may be more adventageous to do that as part of a
/// ``TokenProvider`` implementation rather than tightly-coupling keychain raw string access directly to your /// ``TokenProvider`` implementation rather than tightly-coupling keychain raw string access directly to your
/// client instantiation. /// client instantiation.
public protocol TokenProvider { public protocol TokenProvider: Sendable {
/// Returns a token string for the given ``APIVersion``. This will usually be a fixed constant such as ``APIVersion/REST/v2`` or ``APIVersion/GraphQL/v1``, so you can switch on the values of one of these. /// Returns a token string for the given ``APIVersion``. This will usually be a fixed constant such as ``APIVersion/REST/v2`` or ``APIVersion/GraphQL/v1``, so you can switch on the values of one of these.
func token(for version: APIVersion) async -> String? func token(for version: APIVersion) async -> String?
} }

View File

@ -19,7 +19,7 @@ public enum TransportError: Error {
} }
/// Interface for the asynchronous communication layer the ``BuildkiteClient`` uses. /// Interface for the asynchronous communication layer the ``BuildkiteClient`` uses.
public protocol Transport { public protocol Transport: Sendable {
typealias Output = (data: Data, response: URLResponse) typealias Output = (data: Data, response: URLResponse)
/// Send the request and receive the ``Output`` asynchronously. /// Send the request and receive the ``Output`` asynchronously.
@ -32,12 +32,16 @@ extension URLSession: Transport {
// Task-based API for URLSession. // Task-based API for URLSession.
#if os(Linux) || os(Windows) #if os(Linux) || os(Windows)
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
send(request: request, completion: continuation.resume) send(request: request) {
continuation.resume(with: $0)
}
} }
#else #else
guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else {
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
send(request: request, completion: continuation.resume) send(request: request) {
continuation.resume(with: $0)
}
} }
} }
@ -45,7 +49,7 @@ extension URLSession: Transport {
#endif #endif
} }
private func send(request: URLRequest, completion: @escaping (Result<Output, Error>) -> Void) { private func send(request: URLRequest, completion: @Sendable @escaping (Result<Output, Error>) -> Void) {
let task = dataTask(with: request) { data, response, error in let task = dataTask(with: request) { data, response, error in
if let error = error { if let error = error {
completion(.failure(error)) completion(.failure(error))

View File

@ -0,0 +1,255 @@
//
// ClusterQueues.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension ClusterQueue {
public enum Resources {}
}
extension ClusterQueue.Resources {
/// List cluster.
///
/// Returns a paginated list of an organization's clusters.
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
public typealias Content = [ClusterQueue]
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
public var path: String {
"organizations/\(organization)/clusters\(clusterId)/queues"
}
public init(
organization: String,
clusterId: UUID
) {
self.organization = organization
self.clusterId = clusterId
}
}
/// Get a cluster.
public struct Get: Resource, Equatable, Hashable, Sendable {
public typealias Content = ClusterQueue
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
/// queue ID
public var queueId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)"
}
public init(
organization: String,
clusterId: UUID,
queueId: UUID
) {
self.organization = organization
self.clusterId = clusterId
self.queueId = queueId
}
}
/// Create a cluster queue.
public struct Create: Resource, Equatable, Hashable, Sendable {
public typealias Content = ClusterQueue
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
public var body: Body
public struct Body: Codable, Equatable, Hashable, Sendable {
/// Key for the queue.
public var key: String
/// Description for the queue.
public var description: String?
public init(
key: String,
description: String? = nil
) {
self.key = key
self.description = description
}
}
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/queues"
}
public init(
organization: String,
clusterId: UUID,
body: Body
) {
self.organization = organization
self.clusterId = clusterId
self.body = body
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "POST"
}
}
/// Update a cluster queue.
public struct Update: Resource, Equatable, Hashable, Sendable {
public typealias Content = ClusterQueue
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
/// queue ID
public var queueId: UUID
public var body: Body
public struct Body: Codable, Equatable, Hashable, Sendable {
/// Description for the cluster.
public var description: String
}
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)"
}
public init(
organization: String,
clusterId: UUID,
queueId: UUID,
body: Body
) {
self.organization = organization
self.clusterId = clusterId
self.queueId = queueId
self.body = body
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "PUT"
}
}
/// Delete a cluster queue.
public struct Delete: Resource, Equatable, Hashable, Sendable {
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
/// queue ID
public var queueId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)"
}
public init(
organization: String,
clusterId: UUID,
queueId: UUID
) {
self.organization = organization
self.clusterId = clusterId
self.queueId = queueId
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "DELETE"
}
}
/// Resume a paused cluster queue.
public struct ResumeDispatch: Resource, Equatable, Hashable, Sendable {
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
/// queue ID
public var queueId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)/resume_dispatch"
}
public init(
organization: String,
clusterId: UUID,
queueId: UUID
) {
self.organization = organization
self.clusterId = clusterId
self.queueId = queueId
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "POST"
}
}
}
extension Resource where Self == ClusterQueue.Resources.List {
/// List cluster tokens
///
/// Returns a paginated list of a cluster's tokens.
public static func clusterQueues(in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId)
}
}
extension Resource where Self == ClusterQueue.Resources.Get {
/// Get a cluster token.
public static func clusterQueue(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId, queueId: id)
}
}
extension Resource where Self == ClusterQueue.Resources.Create {
/// Create a cluster token.
public static func createClusterQueue(_ body: Self.Body, in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId, body: body)
}
}
extension Resource where Self == ClusterQueue.Resources.Update {
/// Update a cluster token.
public static func updateClusterQueue(
_ id: UUID,
in organization: String,
clusterId: UUID,
with description: String
) -> Self {
Self(organization: organization, clusterId: clusterId, queueId: id, body: .init(description: description))
}
}
extension Resource where Self == ClusterQueue.Resources.Delete {
/// Delete a cluster token.
public static func deleteClusterQueue(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId, queueId: id)
}
}
extension Resource where Self == ClusterQueue.Resources.ResumeDispatch {
/// Delete a cluster token.
public static func resumeClusterQueueDispatch(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId, queueId: id)
}
}

View File

@ -0,0 +1,221 @@
//
// ClusterTokens.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension ClusterToken {
public enum Resources {}
}
extension ClusterToken.Resources {
/// List cluster tokens
///
/// Returns a paginated list of a cluster's tokens.
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
public typealias Content = [ClusterToken]
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/tokens"
}
public init(
organization: String,
clusterId: UUID
) {
self.organization = organization
self.clusterId = clusterId
}
}
/// Get a cluster token.
public struct Get: Resource, Equatable, Hashable, Sendable {
public typealias Content = ClusterToken
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
/// cluster token ID
public var tokenId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/tokens/\(tokenId)"
}
public init(
organization: String,
clusterId: UUID,
tokenId: UUID
) {
self.organization = organization
self.clusterId = clusterId
self.tokenId = tokenId
}
}
/// Create a cluster token.
public struct Create: Resource, Equatable, Hashable, Sendable {
public typealias Content = ClusterToken
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
public var body: Body
public struct Body: Codable, Equatable, Hashable, Sendable {
/// Description for the token.
public var description: String
public init(
description: String
) {
self.description = description
}
}
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/tokens"
}
public init(
organization: String,
clusterId: UUID,
body: Body
) {
self.organization = organization
self.clusterId = clusterId
self.body = body
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "POST"
}
}
/// Update a cluster token.
public struct Update: Resource, Equatable, Hashable, Sendable {
public typealias Content = ClusterToken
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
/// cluster token ID
public var tokenId: UUID
public var body: Body
public struct Body: Codable, Equatable, Hashable, Sendable {
/// Description for the token.
public var description: String
public init(
description: String
) {
self.description = description
}
}
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/tokens/\(tokenId)"
}
public init(
organization: String,
clusterId: UUID,
tokenId: UUID,
body: Body
) {
self.organization = organization
self.clusterId = clusterId
self.tokenId = tokenId
self.body = body
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "PUT"
}
}
/// Delete a cluster token.
public struct Delete: Resource, Equatable, Hashable, Sendable {
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
/// cluster token ID
public var tokenId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)/tokens/\(tokenId)"
}
public init(
organization: String,
clusterId: UUID,
tokenId: UUID
) {
self.organization = organization
self.clusterId = clusterId
self.tokenId = tokenId
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "DELETE"
}
}
}
extension Resource where Self == ClusterToken.Resources.List {
/// List cluster tokens
///
/// Returns a paginated list of a cluster's tokens.
public static func clusterTokens(in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId)
}
}
extension Resource where Self == ClusterToken.Resources.Get {
/// Get a cluster token.
public static func clusterToken(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId, tokenId: id)
}
}
extension Resource where Self == ClusterToken.Resources.Create {
/// Create a cluster token.
public static func createClusterToken(with description: String, in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId, body: Self.Body(description: description))
}
}
extension Resource where Self == ClusterToken.Resources.Update {
/// Update a cluster token.
public static func updateClusterToken(
_ id: UUID,
in organization: String,
clusterId: UUID,
with description: String
) -> Self {
Self(organization: organization, clusterId: clusterId, tokenId: id, body: Self.Body(description: description))
}
}
extension Resource where Self == ClusterToken.Resources.Delete {
/// Delete a cluster token.
public static func deleteClusterToken(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
Self(organization: organization, clusterId: clusterId, tokenId: id)
}
}

View File

@ -0,0 +1,232 @@
//
// Clusters.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension Cluster {
public enum Resources {}
}
extension Cluster.Resources {
/// List cluster.
///
/// Returns a paginated list of an organization's clusters.
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
public typealias Content = [Cluster]
/// organization slug
public var organization: String
public var path: String {
"organizations/\(organization)/clusters"
}
public init(
organization: String
) {
self.organization = organization
}
}
/// Get a cluster.
public struct Get: Resource, Equatable, Hashable, Sendable {
public typealias Content = Cluster
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)"
}
public init(
organization: String,
clusterId: UUID
) {
self.organization = organization
self.clusterId = clusterId
}
}
/// Create a cluster.
public struct Create: Resource, Equatable, Hashable, Sendable {
public typealias Content = Cluster
/// organization slug
public var organization: String
public var body: Body
public struct Body: Codable, Equatable, Hashable, Sendable {
/// Name for the cluster.
public var name: String
/// Description for the cluster.
public var description: String?
/// Emoji for the cluster using the emoji syntax.
public var emoji: String?
/// Color hex code for the cluster.
public var color: String?
public init(
name: String,
description: String? = nil,
emoji: String? = nil,
color: String? = nil
) {
self.name = name
self.description = description
self.emoji = emoji
self.color = color
}
}
public var path: String {
"organizations/\(organization)/clusters"
}
public init(
organization: String,
body: Body
) {
self.organization = organization
self.body = body
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "POST"
}
}
/// Update a cluster.
public struct Update: Resource, Equatable, Hashable, Sendable {
public typealias Content = Cluster
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
public var body: Body
public struct Body: Codable, Equatable, Hashable, Sendable {
/// Name for the cluster.
public var name: String
/// Description for the cluster.
public var description: String
/// Emoji for the cluster using the emoji syntax.
public var emoji: String
/// Color hex code for the cluster.
public var color: String
/// ID of the queue to set as the cluster's default queue. Agents that connect to the cluster without specifying a queue will accept jobs from this queue.
public var defaultQueueId: UUID
public init(
name: String,
description: String,
emoji: String,
color: String,
defaultQueueId: UUID
) {
self.name = name
self.description = description
self.emoji = emoji
self.color = color
self.defaultQueueId = defaultQueueId
}
private enum CodingKeys: String, CodingKey {
case name
case description
case emoji
case color
case defaultQueueId = "default_queue_id"
}
}
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)"
}
public init(
organization: String,
clusterId: UUID,
body: Body
) {
self.organization = organization
self.clusterId = clusterId
self.body = body
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "PUT"
}
}
/// Delete a cluster.
public struct Delete: Resource, Equatable, Hashable, Sendable {
/// organization slug
public var organization: String
/// cluster ID
public var clusterId: UUID
public var path: String {
"organizations/\(organization)/clusters/\(clusterId)"
}
public init(
organization: String,
clusterId: UUID
) {
self.organization = organization
self.clusterId = clusterId
}
public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "DELETE"
}
}
}
extension Resource where Self == Cluster.Resources.List {
/// List clusters
///
/// Returns a paginated list of an organization's clusters.
public static func clusters(in organization: String) -> Self {
Self(organization: organization)
}
}
extension Resource where Self == Cluster.Resources.Get {
/// Get a cluster.
public static func cluster(_ id: UUID, in organization: String) -> Self {
Self(organization: organization, clusterId: id)
}
}
extension Resource where Self == Cluster.Resources.Create {
/// Create a cluster.
public static func createCluster(_ body: Self.Body, in organization: String) -> Self {
Self(organization: organization, body: body)
}
}
extension Resource where Self == Cluster.Resources.Update {
/// Update a cluster.
public static func updateCluster(_ id: UUID, in organization: String, with body: Self.Body) -> Self {
Self(organization: organization, clusterId: id, body: body)
}
}
extension Resource where Self == Cluster.Resources.Delete {
/// Delete a cluster.
public static func deleteCluster(_ id: UUID, in organization: String) -> Self {
Self(organization: organization, clusterId: id)
}
}

View File

@ -0,0 +1,52 @@
//
// FlakyTests.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension FlakyTest {
/// Resources for performing operations on flaky tests.
public enum Resources {}
}
extension FlakyTest.Resources {
/// Provides information about tests detected as flaky in a test suite.
///
/// Returns a paginated list of the flaky tests detected in a test suite.
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
public typealias Content = [FlakyTest]
/// organization slug
public var organization: String
/// test suite slug
public var suite: String
public var path: String {
"analytics/organizations/\(organization)/suites/\(suite)/flaky-tests"
}
public init(
organization: String,
suite: String
) {
self.organization = organization
self.suite = suite
}
}
}
extension Resource where Self == FlakyTest.Resources.List {
/// List flaky tests
///
/// Returns a paginated list of a test suite's flaky tests.
public static func flakyTests(in organization: String, suite: String) -> Self {
Self(organization: organization, suite: suite)
}
}

View File

@ -15,66 +15,80 @@ import XCTest
import FoundationNetworking import FoundationNetworking
#endif #endif
class TransportTests: XCTestCase { extension URLSession {
private func createSession(testCase: MockURLProtocol.Case = .success) -> URLSession { fileprivate convenience init(
switch testCase { _ testCase: MockURLProtocol.Case
case .success: ) {
MockURLProtocol.requestHandler = MockData.mockingSuccessNoContent
case .error:
MockURLProtocol.requestHandler = MockData.mockingError
}
let config = URLSessionConfiguration.ephemeral let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self] config.protocolClasses = [MockURLProtocol.self]
return URLSession(configuration: config) self.init(configuration: config)
}
private class MockURLProtocol: URLProtocol {
enum Case {
case success
case error
}
typealias RequestHandler = (URLRequest) throws -> (Data, URLResponse)
static var requestHandler: RequestHandler?
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
return
}
do {
let (data, response) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
} }
} }
// MARK: - Async/Await-based Requests private final class MockURLProtocol: URLProtocol {
typealias RequestHandler = @Sendable (URLRequest) throws -> (Data, URLResponse)
extension TransportTests { enum Case {
case success
case error
var handler: RequestHandler {
switch self {
case .success:
return MockData.mockingSuccessNoContent(for:)
case .error:
return MockData.mockingError(for:)
}
}
}
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
guard let client = client, let handler = Self.testCase(for: request)?.handler else { return }
do {
let (data, response) = try handler(request)
client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client.urlProtocol(self, didLoad: data)
client.urlProtocolDidFinishLoading(self)
} catch {
client.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
static func testCase(for request: URLRequest) -> Case? {
property(forKey: "testCase", in: request) as? Case
}
static func setTestCase(_ testCase: Case, for request: inout URLRequest) {
let mutableRequest = (request as NSURLRequest) as! NSMutableURLRequest
setProperty(
testCase,
forKey: "testCase",
in: mutableRequest
)
request = mutableRequest as URLRequest
}
}
class TransportTests: XCTestCase {
func testURLSessionSendRequest() async throws { func testURLSessionSendRequest() async throws {
let request = URLRequest(url: URL()) var request = URLRequest(url: URL())
_ = try await createSession().send(request: request) let session = URLSession(.success)
MockURLProtocol.setTestCase(.success, for: &request)
_ = try await session.send(request: request)
} }
func testURLSessionSendRequestFailure() async { func testURLSessionSendRequestFailure() async {
let request = URLRequest(url: URL()) var request = URLRequest(url: URL())
let session = URLSession(.error)
MockURLProtocol.setTestCase(.error, for: &request)
try await XCTAssertThrowsError( try await XCTAssertThrowsError(
await createSession(testCase: .error).send(request: request) await session.send(request: request)
) )
} }
} }

View File

@ -98,10 +98,11 @@ class GraphQLTests: XCTestCase {
func testGraphQLContentGet() throws { func testGraphQLContentGet() throws {
try XCTAssertNoThrow(GraphQL.Content.data("hello").get()) try XCTAssertNoThrow(GraphQL.Content.data("hello").get())
let errors = GraphQL<String>.Errors( let errors = GraphQL<String>
errors: [], .Errors(
type: nil errors: [],
) type: nil
)
try XCTAssertThrowsError( try XCTAssertThrowsError(
GraphQL<String>.Content.errors(errors) GraphQL<String>.Content.errors(errors)
.get(), .get(),

View File

@ -0,0 +1,90 @@
//
// ClusterQueuesTests.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension ClusterQueue {
init() {
self.init(
id: UUID(),
graphqlId: "",
key: "",
description: "",
url: .init(),
webURL: URL(),
clusterURL: .init(),
dispatchPaused: false,
dispatchPausedBy: nil,
dispatchPausedAt: nil,
dispatchPausedNote: nil,
createdAt: Date(timeIntervalSince1970: 1000),
createdBy: nil
)
}
}
class ClusterQueuesTests: XCTestCase {
func testClusterQueuesList() async throws {
let expected = [ClusterQueue(), ClusterQueue()]
let context = try MockContext(content: expected)
let response = try await context.client.send(.clusterQueues(in: "organization", clusterId: UUID()))
XCTAssertEqual(expected, response.content)
}
func testClusterQueuesGet() async throws {
let expected = ClusterQueue()
let context = try MockContext(content: expected)
let response = try await context.client.send(.clusterQueue(UUID(), in: "organization", clusterId: UUID()))
XCTAssertEqual(expected, response.content)
}
func testClusterQueuesCreate() async throws {
let expected = ClusterQueue()
let context = try MockContext(content: expected)
let response = try await context.client.send(
.createClusterQueue(.init(key: "", description: ""), in: "organization", clusterId: UUID())
)
XCTAssertEqual(expected, response.content)
}
func testClusterQueuesUpdate() async throws {
let expected = ClusterQueue()
let context = try MockContext(content: expected)
let response = try await context.client.send(
.updateClusterQueue(UUID(), in: "organization", clusterId: UUID(), with: "")
)
XCTAssertEqual(expected, response.content)
}
func testClusterQueuesDelete() async throws {
let context = MockContext()
_ = try await context.client.send(.deleteClusterQueue(UUID(), in: "organization", clusterId: UUID()))
}
func testClusterQueuesResumeDispatch() async throws {
let context = MockContext()
_ = try await context.client.send(.resumeClusterQueueDispatch(UUID(), in: "organization", clusterId: UUID()))
}
}

View File

@ -0,0 +1,79 @@
//
// ClusterTokensTests.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension ClusterToken {
init() {
self.init(
id: UUID(),
graphqlId: "",
description: "",
url: .init(),
clusterURL: .init(),
createdAt: Date(timeIntervalSince1970: 1000),
createdBy: nil,
token: nil
)
}
}
class ClusterTokensTests: XCTestCase {
func testClusterTokensList() async throws {
let expected = [ClusterToken(), ClusterToken()]
let context = try MockContext(content: expected)
let response = try await context.client.send(.clusterTokens(in: "organization", clusterId: UUID()))
XCTAssertEqual(expected, response.content)
}
func testClusterTokensGet() async throws {
let expected = ClusterToken()
let context = try MockContext(content: expected)
let response = try await context.client.send(.clusterToken(UUID(), in: "organization", clusterId: UUID()))
XCTAssertEqual(expected, response.content)
}
func testClusterTokensCreate() async throws {
let expected = ClusterToken()
let context = try MockContext(content: expected)
let response = try await context.client.send(
.createClusterToken(with: "", in: "organization", clusterId: UUID())
)
XCTAssertEqual(expected, response.content)
}
func testClusterTokensUpdate() async throws {
let expected = ClusterToken()
let context = try MockContext(content: expected)
let response = try await context.client.send(
.updateClusterToken(UUID(), in: "organization", clusterId: UUID(), with: "")
)
XCTAssertEqual(expected, response.content)
}
func testClusterTokensDelete() async throws {
let context = MockContext()
_ = try await context.client.send(.deleteClusterToken(UUID(), in: "organization", clusterId: UUID()))
}
}

View File

@ -0,0 +1,88 @@
//
// ClustersTests.swift
// Buildkite
//
// Created by Aaron Sky on 6/18/23.
// Copyright © 2023 Aaron Sky. All rights reserved.
//
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension Cluster {
init() {
self.init(
id: UUID(),
graphqlId: "",
defaultQueueId: UUID(),
name: "",
description: "",
emoji: "",
color: "",
url: .init(),
webURL: URL(),
queuesURL: .init(),
defaultQueueURL: .init(),
createdAt: Date(timeIntervalSince1970: 1000),
createdBy: nil
)
}
}
class ClustersTests: XCTestCase {
func testClustersList() async throws {
let expected = [Cluster(), Cluster()]
let context = try MockContext(content: expected)
let response = try await context.client.send(.clusters(in: "buildkite"))
XCTAssertEqual(expected, response.content)
}
func testClustersGet() async throws {
let expected = Cluster()
let context = try MockContext(content: expected)
let response = try await context.client.send(.cluster(UUID(), in: "organization"))
XCTAssertEqual(expected, response.content)
}
func testClustersCreate() async throws {
let expected = Cluster()
let context = try MockContext(content: expected)
let response = try await context.client.send(
.createCluster(.init(name: "", description: nil, emoji: nil, color: nil), in: "organization")
)
XCTAssertEqual(expected, response.content)
}
func testClustersUpdate() async throws {
let expected = Cluster()
let context = try MockContext(content: expected)
let response = try await context.client.send(
.updateCluster(
UUID(),
in: "organization",
with: .init(name: "", description: "", emoji: "", color: "", defaultQueueId: UUID())
)
)
XCTAssertEqual(expected, response.content)
}
func testClustersDelete() async throws {
let context = MockContext()
_ = try await context.client.send(.deleteCluster(UUID(), in: "organization"))
}
}

View File

@ -0,0 +1,43 @@
//
// PipelinesTests.swift
// Buildkite
//
// Created by Aaron Sky on 5/4/20.
// Copyright © 2020 Aaron Sky. All rights reserved.
//
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension FlakyTest {
init() {
self.init(
id: UUID(),
graphqlId: "",
webURL: URL(),
scope: "Test#failure",
name: "failure",
location: "./file:30",
fileName: "./file",
instances: 10,
mostRecentInstanceAt: Date(timeIntervalSince1970: 1000)
)
}
}
class FlakyTestsTests: XCTestCase {
func testFlakyTestsList() async throws {
let expected = [FlakyTest(), FlakyTest()]
let context = try MockContext(content: expected)
let response = try await context.client.send(.flakyTests(in: "buildkite", suite: "my-suite"))
XCTAssertEqual(expected, response.content)
}
}

View File

@ -103,19 +103,23 @@ enum MockData {
} }
extension MockData { extension MockData {
@Sendable
static func mockingSuccess<Content: Codable>(with content: Content, url: URL) throws -> (Data, URLResponse) { static func mockingSuccess<Content: Codable>(with content: Content, url: URL) throws -> (Data, URLResponse) {
let data = try encoder.encode(content) let data = try encoder.encode(content)
return (data, urlResponse(for: url, status: .ok)) return (data, urlResponse(for: url, status: .ok))
} }
@Sendable
static func mockingSuccessNoContent(url: URL) -> (Data, URLResponse) { static func mockingSuccessNoContent(url: URL) -> (Data, URLResponse) {
return (Data(), urlResponse(for: url, status: .ok)) return (Data(), urlResponse(for: url, status: .ok))
} }
@Sendable
static func mockingIncompatibleResponse(for url: URL) -> (Data, URLResponse) { static func mockingIncompatibleResponse(for url: URL) -> (Data, URLResponse) {
return (Data(), urlResponse(for: url, rawStatus: -128)) return (Data(), urlResponse(for: url, rawStatus: -128))
} }
@Sendable
static func mockingUnsuccessfulResponse(for url: URL) -> (Data, URLResponse) { static func mockingUnsuccessfulResponse(for url: URL) -> (Data, URLResponse) {
let json = """ let json = """
{"message":"not found","errors": ["go away"]} {"message":"not found","errors": ["go away"]}
@ -125,18 +129,22 @@ extension MockData {
return (json, urlResponse(for: url, status: .notFound)) return (json, urlResponse(for: url, status: .notFound))
} }
@Sendable
static func mockingSuccessNoContent(for request: URLRequest) throws -> (Data, URLResponse) { static func mockingSuccessNoContent(for request: URLRequest) throws -> (Data, URLResponse) {
return mockingSuccessNoContent(url: request.url!) return mockingSuccessNoContent(url: request.url!)
} }
@Sendable
static func mockingError(for request: URLRequest) throws -> (Data, URLResponse) { static func mockingError(for request: URLRequest) throws -> (Data, URLResponse) {
throw URLError(.notConnectedToInternet) throw URLError(.notConnectedToInternet)
} }
@Sendable
static func mockingError(_ error: Error) throws -> (Data, URLResponse) { static func mockingError(_ error: Error) throws -> (Data, URLResponse) {
throw error throw error
} }
@Sendable
static func mockingUnrecognizedBuildkiteError(for url: URL) -> (Data, URLResponse) { static func mockingUnrecognizedBuildkiteError(for url: URL) -> (Data, URLResponse) {
let json = """ let json = """
{"message":-1000} {"message":-1000}
@ -145,10 +153,12 @@ extension MockData {
return (json, urlResponse(for: url, status: .notFound)) return (json, urlResponse(for: url, status: .notFound))
} }
@Sendable
private static func urlResponse(for url: URL, status: StatusCode) -> URLResponse { private static func urlResponse(for url: URL, status: StatusCode) -> URLResponse {
urlResponse(for: url, rawStatus: status.rawValue) urlResponse(for: url, rawStatus: status.rawValue)
} }
@Sendable
private static func urlResponse(for url: URL, rawStatus status: Int) -> URLResponse { private static func urlResponse(for url: URL, rawStatus status: Int) -> URLResponse {
HTTPURLResponse( HTTPURLResponse(
url: url, url: url,

View File

@ -13,7 +13,7 @@ import Foundation
import FoundationNetworking import FoundationNetworking
#endif #endif
final class MockTransport { actor MockTransport {
enum Error: Swift.Error { enum Error: Swift.Error {
case tooManyRequests case tooManyRequests
} }