Skip to content

Commit

Permalink
Merge pull request #69 from finestructure/load-testing
Browse files Browse the repository at this point in the history
Load testing
  • Loading branch information
finestructure authored May 21, 2019
2 parents ea4a582 + cfd8a41 commit e87c828
Show file tree
Hide file tree
Showing 55 changed files with 2,257 additions and 1,046 deletions.
6 changes: 4 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"isDefault": true
},
"args": [
"test"
"test",
"--parallel",
"--enable-code-coverage"
]
},
{
Expand All @@ -41,7 +43,7 @@
"run",
"Rester",
// "test.yml"
"./Tests/ResterTests/TestData/httpbin.yml"
"./examples/basic.yml"
],
"problemMatcher": []
}
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile.app
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ FROM finestructure/rester:base-${VERSION} as build
RUN make version

RUN mkdir -p /build/lib && cp -R /usr/lib/swift/linux/*.so* /build/lib
RUN swift build -c release && mv `swift build -c release --show-bin-path` /build/bin
# Can't use -c release for the moment: https://github.com/pointfreeco/swift-gen/issues/8
# RUN swift build -c release && mv `swift build -c release --show-bin-path` /build/bin
RUN swift build && mv `swift build --show-bin-path` /build/bin

# deployment image

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
export VERSION=$(shell git rev-parse HEAD)

clean:
swift package clean

force-clean:
rm -rf .build

xcodeproj:
Expand Down Expand Up @@ -42,7 +45,8 @@ release-macos:
swift build --static-swift-stdlib -c release

release-linux: build-docker-base
docker run --rm -v $(PWD):/host -w /host rester-base swift build --static-swift-stdlib -c release
docker run --rm -v $(PWD):/host -w /host rester-base swift build --static-swift-stdlib
# -c release

version:
echo "public let ResterVersion = \"$(VERSION)\"" > Sources/ResterCore/Version.swift
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@
"version": "0.9.0"
}
},
{
"package": "Gen",
"repositoryURL": "https://github.com/pointfreeco/swift-gen.git",
"state": {
"branch": null,
"revision": "b5e9f0130dcf1ba3dcbb2a64c322e1556ca71eca",
"version": "0.2.0"
}
},
{
"package": "SnapshotTesting",
"repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git",
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let package = Package(
.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.0.0"),
.package(url: "https://github.com/mxcl/PromiseKit", from: "6.0.0"),
.package(url: "https://github.com/onevcat/Rainbow.git", from: "3.0.0"),
.package(url: "https://github.com/pointfreeco/swift-gen.git", from: "0.2.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.3.0"),
.package(url: "https://github.com/PromiseKit/Foundation.git", from: "3.0.0"),
],
Expand All @@ -26,7 +27,7 @@ let package = Package(
dependencies: ["ResterCore"]),
.target(
name: "ResterCore",
dependencies: ["Commander", "LegibleError", "PMKFoundation", "Path", "PromiseKit", "Rainbow", "Regex", "ValueCodable", "Yams"]),
dependencies: ["Commander", "Gen", "LegibleError", "Path", "PMKFoundation", "PromiseKit", "Rainbow", "Regex", "ValueCodable", "Yams"]),
.testTarget(
name: "ResterTests",
dependencies: ["ResterCore", "SnapshotTesting"]),
Expand Down
94 changes: 52 additions & 42 deletions Sources/ResterCore/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,28 @@ func before(name: Request.Name) {
}


func after(name: Request.Name, response: Response, result: ValidationResult) -> Bool {
func after(name: Request.Name, result: TestResult) -> TestResult {
switch result {
case .valid:
case .success(let response):
let duration = format(response.elapsed).map { " (\($0)s)" } ?? ""
Current.console.display("\(name.blue) \("PASSED".green.bold)\(duration)\n")
if statistics != nil {
statistics?[name, default: Stats()].add(response.elapsed)
Current.console.display(statistics)
}
return true
case let .invalid(message):
case let .failure(response, reason):
Current.console.display(verbose: "Response:".bold)
Current.console.display(verbose: "\(response)\n")
Current.console.display("\(name.blue) \("FAILED".red.bold) : \(message.red)\n")
return false
Current.console.display("\(name.blue) \("FAILED".red.bold) : \(reason.red)\n")
case .skipped:
Current.console.display("↪️ \(name.blue) \("SKIPPED".yellow)\n")
}
return result
}


func process(_ filename: String, insecure: Bool, timeout: TimeInterval, verbose: Bool, workdir: String) -> Promise<[Bool]> {
Current.console.display("🚀 Resting \(filename.bold) ...\n")

let restfilePath = Path(filename) ?? Path.cwd/filename
func read(restfile: String, timeout: TimeInterval, verbose: Bool, workdir: String) throws -> Rester {
let restfilePath = Path(restfile) ?? Path.cwd/restfile
Current.workDir = getWorkDir(input: workdir) ?? (restfilePath).parent

if verbose {
Expand All @@ -53,36 +52,35 @@ func process(_ filename: String, insecure: Bool, timeout: TimeInterval, verbose:
Current.console.display(verbose: "Request timeout: \(timeout)s\n")
}

let rester: Rester
do {
rester = try Rester(path: restfilePath, workDir: Current.workDir)
} catch {
return Promise(error: error)
}
let rester = try Rester(path: restfilePath, workDir: Current.workDir)

if verbose {
Current.console.display(variables: rester.allVariables)
Current.console.display(variables: rester.variables)
}

guard rester.requestCount > 0 else {
Current.console.display("⚠️ no requests defined in \(filename.bold)!")
return .value([Bool]())
guard rester.requests.count > 0 else {
throw ResterError.genericError("⚠️ no requests defined in \(restfile.bold)!")
}

return rester.test(before: before, after: after, timeout: timeout, validateCertificate: !insecure)
return rester
}


public let app = command(
Flag("insecure", default: false, description: "do not validate SSL certificate (macOS only)"),
Option<Int?>("duration", default: .none, flag: "d", description: "duration <seconds> to loop for"),
Option<Int?>("loop", default: .none, flag: "l", description: "keep executing file every <loop> seconds"),
Option<Int?>("count", default: .none, flag: "c",
description: "number of iterations to loop for (implies `--loop 0`)"),
Option<Double?>("duration", default: .none, flag: "d",
description: "duration <seconds> to loop for (implies `--loop 0`"),
Option<Double?>("loop", default: .none, flag: "l",
description: "keep executing file every <loop> seconds"),
Flag("stats", flag: "s", description: "Show stats"),
Option<TimeInterval>("timeout", default: Request.defaultTimeout, flag: "t", description: "Request timeout"),
Flag("verbose", flag: "v", description: "Verbose output"),
Option<String>("workdir", default: "", flag: "w", description: "Working directory (for the purpose of resolving relative paths in Restfiles)"),
Option<String>("workdir", default: "", flag: "w",
description: "Working directory (for the purpose of resolving relative paths in Restfiles)"),
Argument<String>("filename", description: "A Restfile")
) { insecure, duration, loop, stats, timeout, verbose, workdir, filename in
) { insecure, count, duration, loop, stats, timeout, verbose, workdir, filename in

signal(SIGINT) { s in
print("\nInterrupted by user, terminating ...")
Expand All @@ -100,37 +98,49 @@ public let app = command(
statistics = [:]
}

if let loop = loop {
print("Running every \(loop) seconds ...\n")
var grandTotal = 0
var failedTotal = 0
let rester: Rester
do {
rester = try read(restfile: filename, timeout: timeout, verbose: verbose, workdir: workdir)
} catch {
Current.console.display(error)
exit(1)
}

if count != nil && duration != nil {
Current.console.display("⚠️ Both count and duration specified, using count.\n")
}

if let loop = loopParameters(count: count, duration: duration, loop: loop) {
print("Running every \(loop.delay) seconds ...\n")
var globalResults = [TestResult]()
var runSetup = true

let until = duration.map { Duration.seconds($0) } ?? .forever
run(loop.iteration, interval: loop.delay.seconds) {
Current.console.display("🚀 Resting \(filename.bold) ...\n")

run(until, interval: .seconds(loop)) {
process(filename, insecure: insecure, timeout: timeout, verbose: verbose, workdir: workdir)
return rester.test(before: before, after: after, timeout: timeout, validateCertificate: !insecure, runSetup: runSetup)
.done { results in
let failureCount = results.filter { !$0 }.count
grandTotal += results.count
failedTotal += failureCount
Current.console.display(summary: results.count, failed: failureCount)
globalResults += results
Current.console.display(results: results)
Current.console.display("")
Current.console.display("TOTAL: ", terminator: "")
Current.console.display(summary: grandTotal, failed: failedTotal)
Current.console.display(results: globalResults)
Current.console.display("")
runSetup = false
}
}.done {
exit(failedTotal == 0 ? 0 : 1)
exit(globalResults.failureCount == 0 ? 0 : 1)
}.catch { error in
Current.console.display(error)
exit(1)
}
} else {
_ = process(filename, insecure: insecure, timeout: timeout, verbose: verbose, workdir: workdir)
Current.console.display("🚀 Resting \(filename.bold) ...\n")

_ = rester.test(before: before, after: after, timeout: timeout, validateCertificate: !insecure)
.done { results in
let failureCount = results.filter { !$0 }.count
Current.console.display(summary: results.count, failed: failureCount)
exit(failureCount == 0 ? 0 : 1)
Current.console.display(results: results)
exit(results.failureCount == 0 ? 0 : 1)
}.catch { error in
Current.console.display(error)
exit(1)
Expand Down
11 changes: 9 additions & 2 deletions Sources/ResterCore/Console.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ extension Console {
display(verbose: "")
}

mutating func display(summary total: Int, failed: Int) {
mutating func display(results: [TestResult]) {
let total = results.count
let failed = results.failureCount
let skipped = results.skippedCount

let testLabel = (total == 1) ? "test" : "tests"
let failure = failed == 0 ? "0".green.bold : String(failed).red.bold
let failureLabel = (failed == 1) ? "failure" : "failures"
display("Executed \(String(total).bold) \(testLabel), with \(failure) \(failureLabel)")
display(
"Executed \(String(total).bold) \(testLabel), with \(failure) \(failureLabel)"
+ ((skipped == 0) ? "" : ", \(String(skipped).yellow) skipped")
)
}

mutating func display(_ stats: [Request.Name: Stats]?) {
Expand Down
21 changes: 21 additions & 0 deletions Sources/ResterCore/Extensions/Data+ext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Data-ext.swift
// ResterCore
//
// Created by Sven A. Schmidt on 14/04/2019.
//

import Foundation


extension Data {
var json: Value? {
if let data = try? JSONDecoder().decode([Key: Value].self, from: self) {
return .dictionary(data)
} else if let data = try? JSONDecoder().decode([Value].self, from: self) {
return .array(data)
} else {
return nil
}
}
}
40 changes: 40 additions & 0 deletions Sources/ResterCore/Extensions/Dictionary+ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,43 @@ extension Dictionary: MultipartEncoding where Key == ResterCore.Key, Value == Re
return payloads[0] + tail + lineBreak + boundary + endMarker
}
}


extension Dictionary where Key == ResterCore.Key, Value == ResterCore.Value {
/// Process mutations to array values of the same key if the values are
/// defined as `.append(value)` or `.remove(value)`.
///
/// - Parameter variables: Dictionary to search for mutation values
/// - Returns: Dictionary with mutated values
public func processMutations(variables: [Key: Value]) -> [Key: Value] {
return Dictionary(uniqueKeysWithValues:
map { (item) -> (Key, Value) in
if let value = variables[item.key], case var .array(arr) = item.value {
if let appendValue = value.appendValue {
return (item.key, .array(arr + [.string(appendValue)]))
}
if let removeValue = value.removeValue {
if let idx = arr.firstIndex(of: .string(removeValue)) {
arr.remove(at: idx)
return (item.key, .array(arr))
}
}
}
return (item.key, item.value)
}
)

}

/// Process mutations to array values of the same key if the values are
/// defined as `.append(value)` or `.remove(value)`.
///
/// - Parameter values: Value object to search for mutation values. Ignored
/// if `nil` or not a `Value.dictionary`.
/// - Returns: Dictionary with mutated values
public func processMutations(values: Value?) -> [Key: Value] {
guard case let .dictionary(dict)? = values else { return self }
return processMutations(variables: dict)
}

}
15 changes: 15 additions & 0 deletions Sources/ResterCore/Extensions/Double+ext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Double+ext.swift
// ResterCore
//
// Created by Sven A. Schmidt on 12/04/2019.
//

import Foundation


extension Double {
public var seconds: DispatchTimeInterval {
return .nanoseconds(Int(self * 1e9))
}
}
24 changes: 24 additions & 0 deletions Sources/ResterCore/Extensions/KeyedDecodingContainer+ext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// KeyedDecodingContainer+ext.swift
// ResterCore
//
// Created by Sven A. Schmidt on 11/04/2019.
//

import Foundation


extension KeyedDecodingContainer {
func decodeRequests(for key: KeyedDecodingContainer.Key) throws -> [Request] {
if contains(key) {
do {
let req = try decode(OrderedDict<Request.Name, Request.Details>.self, forKey: key)
return req.items.compactMap { $0.first }.map { Request(name: $0.key, details: $0.value) }
} catch let DecodingError.keyNotFound(key, _) {
throw ResterError.keyNotFound(key.stringValue)
}
} else {
return []
}
}
}
Loading

0 comments on commit e87c828

Please sign in to comment.