Skip to content

Commit 1456b14

Browse files
authored
reduce duplicate code in test tool (#3772)
motivation: code cleanup changes: remove almost identitical code in the two variants of the test method, and replace with a shared implementation that takes an output redirection and either prints or collects the output
1 parent 8a5c51d commit 1456b14

File tree

1 file changed

+86
-106
lines changed

1 file changed

+86
-106
lines changed

Sources/Commands/SwiftTestTool.swift

Lines changed: 86 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@ See http://swift.org/LICENSE.txt for license information
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11-
import class Foundation.ProcessInfo
12-
1311
import ArgumentParser
1412
import Basics
15-
import TSCBasic
16-
import SPMBuildCore
1713
import Build
18-
import TSCUtility
14+
import class Foundation.ProcessInfo
1915
import PackageGraph
20-
import Workspace
21-
16+
import SPMBuildCore
17+
import TSCBasic
2218
import func TSCLibc.exit
19+
import TSCUtility
20+
import Workspace
2321

2422
private enum TestError: Swift.Error {
2523
case invalidListTestJSONData
@@ -290,18 +288,20 @@ public struct SwiftTestTool: SwiftCommand {
290288
}
291289
}
292290

291+
let testEnv = try constructTestEnvironment(toolchain: toolchain, options: swiftOptions, buildParameters: buildParameters)
292+
293293
let runner = TestRunner(
294294
bundlePaths: testProducts.map { $0.bundlePath },
295295
xctestArg: xctestArg,
296296
processSet: swiftTool.processSet,
297297
toolchain: toolchain,
298-
diagnostics: ObservabilitySystem.topScope.makeDiagnosticsEngine(),
299-
options: swiftOptions,
300-
buildParameters: buildParameters
298+
testEnv: testEnv,
299+
outputStream: swiftTool.outputStream,
300+
diagnostics: ObservabilitySystem.topScope.makeDiagnosticsEngine()
301301
)
302302

303303
// Finally, run the tests.
304-
let ranSuccessfully: Bool = runner.test()
304+
let (ranSuccessfully, _) = runner.test(writeToOutputStream: true)
305305
if !ranSuccessfully {
306306
swiftTool.executionStatus = .failure
307307
}
@@ -576,12 +576,13 @@ final class TestRunner {
576576
// The toolchain to use.
577577
private let toolchain: UserToolchain
578578

579-
/// Diagnostics Engine to emit diagnostics.
580-
let diagnostics: DiagnosticsEngine
579+
private let testEnv: [String: String]
581580

582-
private let options: SwiftToolOptions
581+
/// Output stream for test results
582+
private let outputStream: OutputByteStream
583583

584-
private let buildParameters: BuildParameters
584+
/// Diagnostics Engine to emit diagnostics.
585+
private let diagnostics: DiagnosticsEngine
585586

586587
/// Creates an instance of TestRunner.
587588
///
@@ -593,49 +594,37 @@ final class TestRunner {
593594
xctestArg: String? = nil,
594595
processSet: ProcessSet,
595596
toolchain: UserToolchain,
596-
diagnostics: DiagnosticsEngine,
597-
options: SwiftToolOptions,
598-
buildParameters: BuildParameters
597+
testEnv: [String: String],
598+
outputStream: OutputByteStream,
599+
diagnostics: DiagnosticsEngine
599600
) {
600601
self.bundlePaths = bundlePaths
601602
self.xctestArg = xctestArg
602603
self.processSet = processSet
603604
self.toolchain = toolchain
605+
self.testEnv = testEnv
606+
self.outputStream = outputStream
604607
self.diagnostics = diagnostics
605-
self.options = options
606-
self.buildParameters = buildParameters
607608
}
608609

609-
/// Executes the tests without printing anything on standard streams.
610-
///
611-
/// - Returns: A tuple with first bool member indicating if test execution returned code 0 and second argument
612-
/// containing the output of the execution.
613-
public func test() -> (Bool, String) {
610+
/// Executes and returns execution status. Prints test output on standard streams if requested
611+
/// - Returns: Boolean indicating if test execution returned code 0, and the output stream result
612+
public func test(writeToOutputStream: Bool) -> (Bool, String) {
614613
var success = true
615614
var output = ""
616-
for path in bundlePaths {
617-
let (testSuccess, testOutput) = test(testAt: path)
615+
for path in self.bundlePaths {
616+
let (testSuccess, testOutput) = self.test(at: path, writeToOutputStream: writeToOutputStream)
618617
success = success && testSuccess
619618
output += testOutput
620619
}
621620
return (success, output)
622621
}
623622

624-
/// Executes and returns execution status. Prints test output on standard streams.
625-
public func test() -> Bool {
626-
var success = true
627-
for path in bundlePaths {
628-
let testSuccess: Bool = test(testAt: path)
629-
success = success && testSuccess
630-
}
631-
return success
632-
}
633-
634623
/// Constructs arguments to execute XCTest.
635624
private func args(forTestAt testPath: AbsolutePath) throws -> [String] {
636625
var args: [String] = []
637626
#if os(macOS)
638-
guard let xctest = toolchain.xctest else {
627+
guard let xctest = self.toolchain.xctest else {
639628
throw TestError.testsExecutableNotFound
640629
}
641630
args = [xctest.pathString]
@@ -652,61 +641,48 @@ final class TestRunner {
652641
return args
653642
}
654643

655-
/// Executes the tests without printing anything on standard streams.
656-
///
657-
/// - Returns: A tuple with first bool member indicating if test execution returned code 0
658-
/// and second argument containing the output of the execution.
659-
private func test(testAt testPath: AbsolutePath) -> (Bool, String) {
660-
var output = ""
661-
var success = false
662-
do {
663-
// FIXME: The environment will be constructed for every test when using the
664-
// parallel test runner. We should do some kind of caching.
665-
let env = try constructTestEnvironment(toolchain: toolchain, options: self.options, buildParameters: self.buildParameters)
666-
let process = Process(arguments: try args(forTestAt: testPath), environment: env, outputRedirection: .collect, verbose: false)
667-
try process.launch()
668-
let result = try process.waitUntilExit()
669-
output = try (result.utf8Output() + result.utf8stderrOutput()).spm_chuzzle() ?? ""
670-
switch result.exitStatus {
671-
case .terminated(code: 0):
672-
success = true
673-
#if !os(Windows)
674-
case .signalled(let signal):
675-
output += "\n" + exitSignalText(code: signal)
676-
#endif
677-
default: break
678-
}
679-
} catch {
680-
diagnostics.emit(error)
644+
private func test(at path: AbsolutePath, writeToOutputStream: Bool) -> (Bool, String) {
645+
var stdout: [UInt8] = []
646+
var stderr: [UInt8] = []
647+
648+
func makeOutput() -> String {
649+
return String(bytes: stdout + stderr, encoding: .utf8)?.spm_chuzzle() ?? ""
681650
}
682-
return (success, output)
683-
}
684651

685-
/// Executes and returns execution status. Prints test output on standard streams.
686-
private func test(testAt testPath: AbsolutePath) -> Bool {
687652
do {
688-
let env = try constructTestEnvironment(toolchain: toolchain, options: self.options, buildParameters: self.buildParameters)
689-
let process = Process(arguments: try args(forTestAt: testPath), environment: env, outputRedirection: .none)
690-
try processSet.add(process)
653+
let outputRedirection = Process.OutputRedirection.stream(
654+
stdout: {
655+
stdout += $0
656+
if writeToOutputStream {
657+
self.outputStream.write($0)
658+
self.outputStream.flush()
659+
}
660+
},
661+
stderr: {
662+
stderr += $0
663+
if writeToOutputStream {
664+
TSCBasic.stderrStream.write($0)
665+
TSCBasic.stderrStream.flush()
666+
}
667+
}
668+
)
669+
let process = Process(arguments: try args(forTestAt: path), environment: self.testEnv, outputRedirection: outputRedirection, verbose: false)
670+
try self.processSet.add(process)
691671
try process.launch()
692672
let result = try process.waitUntilExit()
693673
switch result.exitStatus {
694674
case .terminated(code: 0):
695-
return true
696-
#if !os(Windows)
675+
return (true, makeOutput())
676+
#if !os(Windows)
697677
case .signalled(let signal):
698-
print(exitSignalText(code: signal))
699-
#endif
678+
outputRedirection.outputClosures?.stdoutClosure(Array("\nExited with signal code \(signal)".utf8))
679+
#endif
700680
default: break
701681
}
702682
} catch {
703-
diagnostics.emit(error)
683+
self.diagnostics.emit(error)
704684
}
705-
return false
706-
}
707-
708-
private func exitSignalText(code: Int32) -> String {
709-
return "Exited with signal code \(code)"
685+
return (false, makeOutput())
710686
}
711687
}
712688

@@ -740,19 +716,22 @@ final class ParallelTestRunner {
740716
/// True if all tests executed successfully.
741717
private(set) var ranSuccessfully = true
742718

743-
let processSet: ProcessSet
719+
private let processSet: ProcessSet
744720

745-
let toolchain: UserToolchain
746-
let xUnitOutput: AbsolutePath?
721+
private let toolchain: UserToolchain
722+
private let xUnitOutput: AbsolutePath?
747723

748-
let options: SwiftToolOptions
749-
let buildParameters: BuildParameters
724+
private let options: SwiftToolOptions
725+
private let buildParameters: BuildParameters
750726

751727
/// Number of tests to execute in parallel.
752-
let numJobs: Int
728+
private let numJobs: Int
729+
730+
/// Output stream for test results
731+
private let outputStream: OutputByteStream
753732

754733
/// Diagnostics Engine to emit diagnostics.
755-
let diagnostics: DiagnosticsEngine
734+
private let diagnostics: DiagnosticsEngine
756735

757736
init(
758737
bundlePaths: [AbsolutePath],
@@ -770,6 +749,7 @@ final class ParallelTestRunner {
770749
self.toolchain = toolchain
771750
self.xUnitOutput = xUnitOutput
772751
self.numJobs = numJobs
752+
self.outputStream = outputStream
773753
self.diagnostics = diagnostics
774754

775755
if ProcessEnv.vars["SWIFTPM_TEST_RUNNER_PROGRESS_BAR"] == "lit" {
@@ -813,6 +793,9 @@ final class ParallelTestRunner {
813793
/// Executes the tests spawning parallel workers. Blocks calling thread until all workers are finished.
814794
func run(_ tests: [UnitTest], outputStream: OutputByteStream) throws {
815795
assert(!tests.isEmpty, "There should be at least one test to execute.")
796+
797+
let testEnv = try constructTestEnvironment(toolchain: self.toolchain, options: self.options, buildParameters: self.buildParameters)
798+
816799
// Enqueue all the tests.
817800
try enqueueTests(tests)
818801

@@ -826,11 +809,11 @@ final class ParallelTestRunner {
826809
xctestArg: test.specifier,
827810
processSet: self.processSet,
828811
toolchain: self.toolchain,
829-
diagnostics: self.diagnostics,
830-
options: self.options,
831-
buildParameters: self.buildParameters
812+
testEnv: testEnv,
813+
outputStream: self.outputStream,
814+
diagnostics: self.diagnostics
832815
)
833-
let (success, output) = testRunner.test()
816+
let (success, output) = testRunner.test(writeToOutputStream: false)
834817
if !success {
835818
self.ranSuccessfully = false
836819
}
@@ -842,17 +825,14 @@ final class ParallelTestRunner {
842825
})
843826

844827
// List of processed tests.
845-
var processedTests: [TestResult] = []
846-
let processedTestsLock = TSCBasic.Lock()
828+
let processedTests = ThreadSafeArrayStore<TestResult>()
847829

848830
// Report (consume) the tests which have finished running.
849831
while let result = finishedTests.dequeue() {
850832
updateProgress(for: result.unitTest)
851833

852834
// Store the result.
853-
processedTestsLock.withLock {
854-
processedTests.append(result)
855-
}
835+
processedTests.append(result)
856836

857837
// We can't enqueue a sentinel into finished tests queue because we won't know
858838
// which test is last one so exit this when all the tests have finished running.
@@ -865,18 +845,18 @@ final class ParallelTestRunner {
865845
workers.forEach { $0.join() }
866846

867847
// Report the completion.
868-
progressAnimation.complete(success: processedTests.contains(where: { !$0.success }))
848+
progressAnimation.complete(success: processedTests.get().contains(where: { !$0.success }))
869849

870850
// Print test results.
871-
for test in processedTests {
851+
for test in processedTests.get() {
872852
if !test.success || shouldOutputSuccess {
873853
print(test, outputStream: outputStream)
874854
}
875855
}
876856

877857
// Generate xUnit file if requested.
878858
if let xUnitOutput = xUnitOutput {
879-
try XUnitGenerator(processedTests).generate(at: xUnitOutput)
859+
try XUnitGenerator(processedTests.get()).generate(at: xUnitOutput)
880860
}
881861
}
882862

@@ -1041,14 +1021,14 @@ fileprivate func constructTestEnvironment(
10411021
let codecovProfile = buildParameters.buildPath.appending(components: "codecov", "default%m.profraw")
10421022
env["LLVM_PROFILE_FILE"] = codecovProfile.pathString
10431023
}
1044-
#if !os(macOS)
1045-
#if os(Windows)
1024+
#if !os(macOS)
1025+
#if os(Windows)
10461026
if let location = toolchain.configuration.xctestPath {
1047-
env["Path"] = "\(location.pathString);\(env["Path"] ?? "")"
1027+
env["Path"] = "\(location.pathString);\(env["Path"] ?? "")"
10481028
}
1049-
#endif
1029+
#endif
10501030
return env
1051-
#else
1031+
#else
10521032
// Fast path when no sanitizers are enabled.
10531033
if options.sanitizers.isEmpty {
10541034
return env
@@ -1066,7 +1046,7 @@ fileprivate func constructTestEnvironment(
10661046

10671047
env["DYLD_INSERT_LIBRARIES"] = runtimes.joined(separator: ":")
10681048
return env
1069-
#endif
1049+
#endif
10701050
}
10711051

10721052
/// xUnit XML file generator for a swift-test run.

0 commit comments

Comments
 (0)