Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.0
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/onevcat/Rainbow.git", from: "3.0.0"),
.package(url: "https://github.com/kareman/SwiftShell.git", from: "4.0.0"),
.package(url: "https://github.com/harlanhaskins/ShellOut.git", .branch("on-your-mark-get-set-go")),
],
targets: [
.target(
name: "LiteSupport",
dependencies: ["Rainbow", "SwiftShell"]),
dependencies: ["Rainbow", "ShellOut"]),

// This needs to be named `lite-test` instead of `lite` because consumers
// of `lite` should use the target name `lite`.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@ depends on the Lite support library, `LiteSupport`.
### Making a `lite` Target

From that target's `main.swift`, make a call to
`runLite(substitutions:pathExtensions:testDirPath:testLinePrefix:)`. This call
`runLite(substitutions:pathExtensions:testDirPath:testLinePrefix:parallelismLevel:)`. This call
is the main entry point to `lite`'s test running.

It takes 4 arguments:
It takes 5 arguments:

| Argument | Description |
|----------|-------------|
| `substitutions` | The mapping of substitutions to make inside each run line. A substitution looks for a string beginning with `'%'` and replaces that whole string with the substituted value. |
| `pathExtensions` | The set of path extensions that Lite should search for when discovering tests. |
| `testDirPath` | The directory in which Lite should look for tests. Lite will perform a deep search through this directory for all files whose extension exists in `pathExtensions` and which have valid RUN lines. |
| `testLinePrefix` | The prefix before `RUN:` in a file. This is almost always your specific langauge's line comment syntax. |
| `parallelismLevel` | Specifies the amount of parallelism to apply to the test running process. Default value is `.none`, but you can provide `.automatic` to use the available machine cores, or `.explicit(n)` to specify an explicit number of parallel tests |

> Note: An example consumer of `Lite` exists in this repository as `lite-test`.

Expand Down
74 changes: 74 additions & 0 deletions Sources/LiteSupport/ParallelExecutor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/// ParallelExecutor.swift
///
/// Copyright 2017, The Silt Language Project.
///
/// This project is released under the MIT license, a copy of which is
/// available in the repository.

import Foundation
import Dispatch

/// A class that handles executing tasks in a round-robin fashion among
/// a fixed number of workers. It uses GCD to split the work among a fixed
/// set of queues and automatically manages balancing workloads between workers.
final class ParallelExecutor<TaskResult> {
/// The set of worker queues on which to add tasks.
private let queues: [DispatchQueue]

/// The dispatch group on which to synchronize the workers.
private let group = DispatchGroup()

/// The results from each task executed on the workers, in non-deterministic
/// order.
private var results = [TaskResult]()

/// The queue on which to protect the results array.
private let resultQueue = DispatchQueue(label: "parallel-results")

/// The current number of tasks, used for round-robin dispatch.
private var taskCount = 0

/// Creates an executor that splits tasks among the provided number of
/// workers.
/// - parameter numberOfWorkers: The number of workers to spawn. This number
/// should be <= the number of hyperthreaded
/// cores on your machine, to avoid excessive
/// context switching.
init(numberOfWorkers: Int) {
self.queues = (0..<numberOfWorkers).map {
DispatchQueue(label: "parallel-worker-\($0)")
}
}

/// Adds the provided result to the result array, synchronized on the result
/// queue.
private func addResult(_ result: TaskResult) {
resultQueue.sync {
results.append(result)
}
}

/// Synchronized on the result queue, gets a unique counter for the total
/// next task to add to the queues.
private var nextTask: Int {
return resultQueue.sync {
defer { taskCount += 1 }
return taskCount
}
}

/// Adds a task to run asynchronously on the next worker. Workers are chosen
/// in a round-robin fashion.
func addTask(_ work: @escaping () -> TaskResult) {
queues[nextTask % queues.count].async(group: group) {
self.addResult(work())
}
}

/// Blocks until all workers have finished executing their tasks, then returns
/// the set of results.
func waitForResults() -> [TaskResult] {
group.wait()
return resultQueue.sync { results }
}
}
13 changes: 11 additions & 2 deletions Sources/LiteSupport/Run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,24 @@ import Foundation
/// which have valid RUN lines.
/// - testLinePrefix: The prefix before `RUN:` in a file. This is almost
/// always your specific langauge's line comment syntax.
/// - parallelismLevel: Specifies the amount of parallelism to apply to the
/// test running process. Default value is `.none`, but
/// you can provide `.automatic` to use the available
/// machine cores, or `.explicit(n)` to specify an
/// explicit number of parallel tests. This value should
/// not exceed the number of hyperthreaded cores on your
/// machine, to avoid excessive context switching.
/// - Returns: `true` if all tests passed, `false` if any failed.
/// - Throws: `LiteError` if there was any issue running tests.
public func runLite(substitutions: [(String, String)],
pathExtensions: Set<String>,
testDirPath: String?,
testLinePrefix: String) throws -> Bool {
testLinePrefix: String,
parallelismLevel: ParallelismLevel = .none) throws -> Bool {
let testRunner = try TestRunner(testDirPath: testDirPath,
substitutions: substitutions,
pathExtensions: pathExtensions,
testLinePrefix: testLinePrefix)
testLinePrefix: testLinePrefix,
parallelismLevel: parallelismLevel)
return try testRunner.run()
}
12 changes: 8 additions & 4 deletions Sources/LiteSupport/Substitutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/// available in the repository.

import Foundation
import Dispatch

public extension String {
public var quoted: String {
Expand All @@ -31,6 +32,7 @@ class Substitutor {
let tmpFileRegex = try! NSRegularExpression(pattern: "%t")
let tmpDirectoryRegex = try! NSRegularExpression(pattern: "%T")

private let tmpDirQueue = DispatchQueue(label: "tmp-dir-queue")
private var tmpDirMap = [URL: URL]()

init(substitutions: [(String, String)]) throws {
Expand All @@ -47,10 +49,12 @@ class Substitutor {
}

func tempFile(for file: URL) -> URL {
if let tmpFile = tmpDirMap[file] { return tmpFile }
let tmpFile = tempDir.appendingPathComponent(UUID().uuidString)
tmpDirMap[file] = tmpFile
return tmpFile
return tmpDirQueue.sync {
if let tmpFile = tmpDirMap[file] { return tmpFile }
let tmpFile = tempDir.appendingPathComponent(UUID().uuidString)
tmpDirMap[file] = tmpFile
return tmpFile
}
}

func substitute(_ line: String, in file: URL) -> String {
Expand Down
8 changes: 5 additions & 3 deletions Sources/LiteSupport/TestFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
/// available in the repository.

import Foundation
import SwiftShell

/// Represents a test that either passed or failed, and contains the run line
/// that triggered the result.
struct TestResult {
/// The run line comprising this test.
let line: RunLine

/// The output from running this test.
let output: RunOutput
/// The stdout output from running this test.
let stdout: String

/// The stderr output from running this test.
let stderr: String

/// The time it took to execute this test from start to finish.
let executionTime: TimeInterval
Expand Down
Loading