Skip to content

Commit 4f7be4f

Browse files
Fix test output parser to be incremental and show failed tests (#505)
1 parent b91944c commit 4f7be4f

File tree

7 files changed

+189
-179
lines changed

7 files changed

+189
-179
lines changed

Sources/CartonHelpers/Process+run.swift

Lines changed: 21 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,12 @@ import Dispatch
1717
import Foundation
1818

1919
struct ProcessError: Error {
20-
let stderr: String?
21-
let stdout: String?
20+
let exitCode: Int32
2221
}
2322

2423
extension ProcessError: CustomStringConvertible {
2524
var description: String {
26-
var result = "Process failed with non-zero exit status"
27-
if let stdout = stdout {
28-
result += " and following output:\n\(stdout)"
29-
}
30-
31-
if let stderr = stderr {
32-
result += " and following error output:\n\(stderr)"
33-
}
34-
return result
25+
return "Process failed with exit code \(exitCode)"
3526
}
3627
}
3728

@@ -41,11 +32,10 @@ extension Foundation.Process {
4132
_ arguments: [String],
4233
environment: [String: String] = [:],
4334
loadingMessage: String = "Running...",
44-
parser: ProcessOutputParser? = nil,
4535
_ terminal: InteractiveWriter
4636
) async throws {
4737
terminal.clearLine()
48-
terminal.write("\(loadingMessage)\n", inColor: .yellow)
38+
terminal.write("Running \(arguments.joined(separator: " "))\n")
4939

5040
if !environment.isEmpty {
5141
terminal.write(environment.map { "\($0)=\($1)" }.joined(separator: " ") + " ")
@@ -54,91 +44,26 @@ extension Foundation.Process {
5444
let processName = URL(fileURLWithPath: arguments[0]).lastPathComponent
5545

5646
do {
57-
try await withCheckedThrowingContinuation {
58-
(continuation: CheckedContinuation<(), Swift.Error>) in
59-
DispatchQueue.global().async {
60-
var stdoutBuffer = ""
61-
62-
let stdout: Process.OutputClosure = {
63-
guard let string = String(data: Data($0), encoding: .utf8) else { return }
64-
if parser != nil {
65-
// Aggregate this for formatting later
66-
stdoutBuffer += string
67-
} else {
68-
terminal.write(string)
69-
}
70-
}
71-
72-
var stderrBuffer = [UInt8]()
73-
74-
let stderr: Process.OutputClosure = {
75-
stderrBuffer.append(contentsOf: $0)
76-
}
77-
78-
let process = Process(
79-
arguments: arguments,
80-
environmentBlock: ProcessEnvironmentBlock(
81-
ProcessInfo.processInfo.environment.merging(environment) { _, new in new }
82-
),
83-
outputRedirection: .stream(stdout: stdout, stderr: stderr),
84-
startNewProcessGroup: true,
85-
loggingHandler: {
86-
terminal.write($0 + "\n")
87-
}
88-
)
89-
90-
let result = Result<ProcessResult, Swift.Error> {
91-
try process.launch()
92-
return try process.waitUntilExit()
93-
}
94-
95-
switch result.map(\.exitStatus) {
96-
case .success(.terminated(code: EXIT_SUCCESS)):
97-
if let parser = parser {
98-
if parser.parsingConditions.contains(.success) {
99-
parser.parse(stdoutBuffer, terminal)
100-
}
101-
} else {
102-
terminal.write(stdoutBuffer)
103-
}
104-
terminal.write(
105-
"`\(processName)` process finished successfully\n",
106-
inColor: .green,
107-
bold: false
108-
)
109-
continuation.resume()
110-
111-
case let .failure(error):
112-
continuation.resume(throwing: error)
113-
default:
114-
continuation.resume(
115-
throwing: ProcessError(
116-
stderr: String(data: Data(stderrBuffer), encoding: .utf8) ?? "",
117-
stdout: stdoutBuffer
118-
)
119-
)
120-
}
47+
try await Process.checkNonZeroExit(
48+
arguments: arguments,
49+
environmentBlock: ProcessEnvironmentBlock(
50+
ProcessInfo.processInfo.environment.merging(environment) { _, new in new }
51+
),
52+
loggingHandler: {
53+
terminal.write($0 + "\n")
12154
}
122-
}
55+
)
56+
terminal.write(
57+
"`\(processName)` process finished successfully\n",
58+
inColor: .green,
59+
bold: false
60+
)
12361
} catch {
124-
let errorString = String(describing: error)
125-
if errorString.isEmpty {
126-
terminal.clearLine()
127-
terminal.write(
128-
"\(processName) process failed.\n\n",
129-
inColor: .red
130-
)
131-
if let error = error as? ProcessError, let stdout = error.stdout {
132-
if let parser = parser {
133-
if parser.parsingConditions.contains(.failure) {
134-
parser.parse(stdout, terminal)
135-
}
136-
} else {
137-
terminal.write(stdout)
138-
}
139-
}
140-
}
141-
62+
terminal.clearLine()
63+
terminal.write(
64+
"\(processName) process failed.\n\n",
65+
inColor: .red
66+
)
14267
throw error
14368
}
14469
}

Sources/CartonKit/Parsers/DiagnosticsParser.swift

Lines changed: 0 additions & 15 deletions
This file was deleted.

Sources/carton-frontend-slim/CartonFrontendTestCommand.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ struct CartonFrontendTestCommand: AsyncParsableCommand {
7575
@Flag(help: "When running browser tests, run the browser in headless mode")
7676
var headless: Bool = false
7777

78+
@Flag(help: "Enable verbose output")
79+
var verbose: Bool = false
80+
7881
@Option(help: "Turn on runtime checks for various behavior.")
7982
private var sanitize: SanitizeVariant?
8083

@@ -195,6 +198,8 @@ struct CartonFrontendTestCommand: AsyncParsableCommand {
195198
env[key] = parentEnv[key]
196199
}
197200
}
198-
return TestRunnerOptions(env: env, listTestCases: list, testCases: testCases)
201+
return TestRunnerOptions(
202+
env: env, listTestCases: list, testCases: testCases,
203+
testsParser: verbose ? RawTestsParser() : FancyTestsParser())
199204
}
200205
}

Sources/carton-frontend-slim/TestRunners/CommandTestRunner.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ struct CommandTestRunner: TestRunner {
4949
}
5050

5151
arguments += [testFilePath.pathString] + xctestArgs
52-
try await Process.run(arguments, parser: TestsParser(), terminal)
52+
try await runTestProcess(arguments, parser: options.testsParser, terminal)
5353
}
5454

5555
func defaultWASIRuntime() throws -> String {

Sources/carton-frontend-slim/TestRunners/JavaScriptTestRunner.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ struct JavaScriptTestRunner: TestRunner {
5858
var arguments =
5959
["node"] + nodeArguments + [pluginWorkDirectory.appending(component: testHarness).pathString]
6060
options.applyXCTestArguments(to: &arguments)
61-
try await Process.run(arguments, environment: options.env, parser: TestsParser(), terminal)
61+
try await runTestProcess(
62+
arguments, environment: options.env, parser: options.testsParser, terminal)
6263
}
6364
}

Sources/carton-frontend-slim/TestRunners/TestRunner.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import CartonCore
16+
import CartonHelpers
17+
import Foundation
18+
1519
struct TestRunnerOptions {
1620
/// The environment variables to pass to the test process.
1721
let env: [String: String]
1822
/// When specified, list all available test cases.
1923
let listTestCases: Bool
2024
/// Filter the test cases to run.
2125
let testCases: [String]
26+
/// The parser to use for the test output.
27+
let testsParser: any TestsParser
2228

2329
func applyXCTestArguments(to arguments: inout [String]) {
2430
if listTestCases {
@@ -32,3 +38,58 @@ struct TestRunnerOptions {
3238
protocol TestRunner {
3339
func run(options: TestRunnerOptions) async throws
3440
}
41+
42+
struct LineStream {
43+
var buffer: String = ""
44+
let onLine: (String) -> Void
45+
46+
mutating func feed(_ bytes: [UInt8]) {
47+
buffer += String(decoding: bytes, as: UTF8.self)
48+
while let newlineIndex = buffer.firstIndex(of: "\n") {
49+
let line = buffer[..<newlineIndex]
50+
buffer.removeSubrange(buffer.startIndex...newlineIndex)
51+
onLine(String(line))
52+
}
53+
}
54+
}
55+
56+
extension TestRunner {
57+
func runTestProcess(
58+
_ arguments: [String],
59+
environment: [String: String] = [:],
60+
parser: any TestsParser,
61+
_ terminal: InteractiveWriter
62+
) async throws {
63+
do {
64+
terminal.clearLine()
65+
let commandLine = arguments.map { "\"\($0)\"" }.joined(separator: " ")
66+
terminal.write("Running \(commandLine)\n")
67+
68+
let (lines, continuation) = AsyncStream.makeStream(
69+
of: String.self, bufferingPolicy: .unbounded
70+
)
71+
var lineStream = LineStream { line in
72+
continuation.yield(line)
73+
}
74+
let process = Process(
75+
arguments: arguments,
76+
environmentBlock: ProcessEnvironmentBlock(
77+
ProcessInfo.processInfo.environment.merging(environment) { _, new in new }
78+
),
79+
outputRedirection: .stream(
80+
stdout: { bytes in
81+
lineStream.feed(bytes)
82+
}, stderr: { _ in },
83+
redirectStderr: true
84+
),
85+
startNewProcessGroup: true
86+
)
87+
async let _ = parser.parse(lines, terminal)
88+
try process.launch()
89+
let result = try await process.waitUntilExit()
90+
guard result.exitStatus == .terminated(code: 0) else {
91+
throw ProcessResult.Error.nonZeroExit(result)
92+
}
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)