Skip to content

Commit

Permalink
use tee to stream and capture
Browse files Browse the repository at this point in the history
  • Loading branch information
omochi committed May 20, 2024
1 parent e030a49 commit d83abb5
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 116 deletions.
6 changes: 6 additions & 0 deletions Sources/CartonHelpers/CartonHelpersError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
struct CartonHelpersError: Error & CustomStringConvertible {
init(_ description: String) {
self.description = description
}
var description: String
}
28 changes: 28 additions & 0 deletions Sources/CartonHelpers/FileUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

public enum FileUtils {
static var errnoString: String {
String(cString: strerror(errno))
}

public static var temporaryDirectory: URL {
URL(fileURLWithPath: NSTemporaryDirectory())
}

public static func makeTemporaryFile(prefix: String, in directory: URL? = nil) throws -> URL {
let directory = directory ?? temporaryDirectory
var template = directory.appendingPathComponent("\(prefix)XXXXXX").path
let result = try template.withUTF8 { template in
let copy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: template.count + 1)
defer { copy.deallocate() }
template.copyBytes(to: copy)
copy[template.count] = 0
guard mkstemp(copy.baseAddress!) != -1 else {
let error = errnoString
throw CartonHelpersError("Failed to make a temporary file at \(template): \(error)")
}
return String(cString: copy.baseAddress!)
}
return URL(fileURLWithPath: result)
}
}
27 changes: 1 addition & 26 deletions Sources/carton/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func derivePackageCommandArguments(
packageArguments += ["--disable-sandbox"]
case "test":
// 1. Ask the plugin process to generate the build command based on the given options
let commandFile = try makeTemporaryFile(prefix: "test-build")
let commandFile = try FileUtils.makeTemporaryFile(prefix: "test-build")
try Foundation.Process.checkRun(
swiftExec,
arguments: packageArguments + pluginArguments + [
Expand Down Expand Up @@ -138,31 +138,6 @@ func derivePackageCommandArguments(
return packageArguments + pluginArguments + ["carton-\(subcommand)"] + cartonPluginArguments
}

var errnoString: String {
String(cString: strerror(errno))
}

var temporaryDirectory: URL {
URL(fileURLWithPath: NSTemporaryDirectory())
}

func makeTemporaryFile(prefix: String, in directory: URL? = nil) throws -> URL {
let directory = directory ?? temporaryDirectory
var template = directory.appendingPathComponent("\(prefix)XXXXXX").path
let result = try template.withUTF8 { template in
let copy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: template.count + 1)
defer { copy.deallocate() }
template.copyBytes(to: copy)
copy[template.count] = 0
guard mkstemp(copy.baseAddress!) != -1 else {
let error = errnoString
throw CartonCommandError("Failed to make a temporary file at \(template): \(error)")
}
return String(cString: copy.baseAddress!)
}
return URL(fileURLWithPath: result)
}

func pluginSubcommand(subcommand: String, argv0: String, arguments: [String]) async throws {
let scratchPath = URL(fileURLWithPath: ".build/carton")
if FileManager.default.fileExists(atPath: scratchPath.path) {
Expand Down
43 changes: 22 additions & 21 deletions Tests/CartonCommandTests/BundleCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import XCTest
@testable import CartonFrontend

final class BundleCommandTests: XCTestCase {
func testWithNoArguments() throws {
try withFixture("EchoExecutable") { packageDirectory in
func testWithNoArguments() async throws {
try await withFixture("EchoExecutable") { packageDirectory in
let bundleDirectory = packageDirectory.appending(component: "Bundle")

try swiftRun(["carton", "bundle"], packageDirectory: packageDirectory.url)
let result = try await swiftRun(["carton", "bundle"], packageDirectory: packageDirectory.url)
try result.checkSuccess()

// Confirm that the files are actually in the folder
XCTAssertTrue(bundleDirectory.exists, "The Bundle directory should exist")
Expand All @@ -41,31 +42,31 @@ final class BundleCommandTests: XCTestCase {
}
}

func testWithDebugInfo() throws {
try withFixture("EchoExecutable") { packageDirectory in
let result = try swiftRun(
func testWithDebugInfo() async throws {
try await withFixture("EchoExecutable") { packageDirectory in
let result = try await swiftRun(
["carton", "bundle", "--debug-info"], packageDirectory: packageDirectory.url
)
result.assertZeroExit()
try result.checkSuccess()

let bundleDirectory = packageDirectory.appending(component: "Bundle")
guard let wasmBinary = (bundleDirectory.ls().filter { $0.contains("wasm") }).first else {
XCTFail("No wasm binary found")
return
}
let headers = try Process.checkNonZeroExit(arguments: [
let headers = try await Process.checkNonZeroExit(arguments: [
"wasm-objdump", "--headers", bundleDirectory.appending(component: wasmBinary).pathString,
])
XCTAssert(headers.contains("\"name\""), "name section not found: \(headers)")
}
}

func testWithoutContentHash() throws {
try withFixture("EchoExecutable") { packageDirectory in
let result = try swiftRun(
func testWithoutContentHash() async throws {
try await withFixture("EchoExecutable") { packageDirectory in
let result = try await swiftRun(
["carton", "bundle", "--no-content-hash", "--wasm-optimizations", "none"], packageDirectory: packageDirectory.url
)
result.assertZeroExit()
try result.checkSuccess()

let bundleDirectory = packageDirectory.appending(component: "Bundle")
guard let wasmBinary = (bundleDirectory.ls().filter { $0.contains("wasm") }).first else {
Expand All @@ -76,16 +77,16 @@ final class BundleCommandTests: XCTestCase {
}
}

func testWasmOptimizationOptions() throws {
try withFixture("EchoExecutable") { packageDirectory in
func getFileSizeOfWasmBinary(wasmOptimizations: WasmOptimizations) throws -> UInt64 {
func testWasmOptimizationOptions() async throws {
try await withFixture("EchoExecutable") { packageDirectory in
func getFileSizeOfWasmBinary(wasmOptimizations: WasmOptimizations) async throws -> UInt64 {
let bundleDirectory = packageDirectory.appending(component: "Bundle")

let result = try swiftRun(
let result = try await swiftRun(
["carton", "bundle", "--wasm-optimizations", wasmOptimizations.rawValue],
packageDirectory: packageDirectory.url
)
result.assertZeroExit()
try result.checkSuccess()

guard let wasmFile = (bundleDirectory.ls().filter { $0.contains("wasm") }).first else {
XCTFail("No wasm binary found")
Expand All @@ -95,10 +96,10 @@ final class BundleCommandTests: XCTestCase {
return try localFileSystem.getFileInfo(bundleDirectory.appending(component: wasmFile)).size
}

try XCTAssertGreaterThan(
getFileSizeOfWasmBinary(wasmOptimizations: .none),
getFileSizeOfWasmBinary(wasmOptimizations: .size)
)
let noneSize = try await getFileSizeOfWasmBinary(wasmOptimizations: .none)
let optimizedSize = try await getFileSizeOfWasmBinary(wasmOptimizations: .size)

XCTAssertGreaterThan(noneSize, optimizedSize)
}
}
}
158 changes: 128 additions & 30 deletions Tests/CartonCommandTests/CommandTestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
// limitations under the License.

import ArgumentParser
import XCTest
import CartonHelpers
import Foundation
import XCTest

struct CommandTestError: Swift.Error & CustomStringConvertible {
init(_ description: String) {
Expand Down Expand Up @@ -51,28 +52,137 @@ func findSwiftExecutable() throws -> AbsolutePath {
try findExecutable(name: "swift")
}

struct SwiftRunResult {
var exitCode: Int32
var stdout: String
var stderr: String
func makeTeeProcess(file: AbsolutePath) throws -> Foundation.Process {
let process = Process()
process.executableURL = try findExecutable(name: "tee").asURL
process.arguments = [file.pathString]
return process
}

private let processConcurrentQueue = DispatchQueue(
label: "carton.processConcurrentQueue",
attributes: .concurrent
)

struct FoundationProcessResult: CustomStringConvertible {
struct Error: Swift.Error & CustomStringConvertible {
var result: FoundationProcessResult

var description: String {
result.description
}
}

var executable: String
var arguments: [String]
var statusCode: Int32
var output: Data?
var errorOutput: Data?

func utf8Output() -> String? {
guard let output else { return nil }
return String(decoding: output, as: UTF8.self)
}

func utf8ErrorOutput() -> String? {
guard let errorOutput else { return nil }
return String(decoding: errorOutput, as: UTF8.self)
}

var description: String {
let commandLine = ([executable] + arguments).joined(separator: " ")

let summary = if statusCode == EXIT_SUCCESS {
"Process succeeded."
} else {
"Process failed with status code \(statusCode)."
}

var lines: [String] = [
summary,
"Command line: \(commandLine)"
]

if let string = utf8Output() {
lines += ["Output:", string]
}
if let string = utf8ErrorOutput() {
lines += ["Error output:", string]
}
return lines.joined(separator: "\n")
}

func checkSuccess() throws {
guard statusCode == EXIT_SUCCESS else {
throw Error(result: self)
}
}
}


extension Foundation.Process {
func waitUntilExit() async {
await withCheckedContinuation { (continuation) in
processConcurrentQueue.async {
self.waitUntilExit()
continuation.resume()
}
}
}

func result(
output: Data? = nil,
errorOutput: Data? = nil
) throws -> FoundationProcessResult {
guard let executableURL else {
throw CommandTestError("executableURL is nil")
}
return FoundationProcessResult(
executable: executableURL.path,
arguments: arguments ?? [],
statusCode: terminationStatus,
output: output,
errorOutput: errorOutput
)
}
}

struct SwiftRunProcess {
var process: Foundation.Process
var tee: Foundation.Process
var outputFile: AbsolutePath

func output() throws -> Data {
try Data(contentsOf: outputFile.asURL)
}

func waitUntilExit() async {
await process.waitUntilExit()
await tee.waitUntilExit()
}

func assertZeroExit(_ file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(exitCode, 0, "stdout: " + stdout + "\nstderr: " + stderr, file: file, line: line)
func result() throws -> FoundationProcessResult {
return try process.result(output: try output())
}
}

func swiftRunProcess(
_ arguments: [CustomStringConvertible],
_ arguments: [String],
packageDirectory: URL
) throws -> (Foundation.Process, stdout: Pipe, stderr: Pipe) {
) throws -> SwiftRunProcess {
let outputFile = try AbsolutePath(
validating: try FileUtils.makeTemporaryFile(prefix: "swift-run").path
)
let tee = try makeTeeProcess(file: outputFile)

let teePipe = Pipe()
tee.standardInput = teePipe

let process = Process()
process.executableURL = try findSwiftExecutable().asURL
process.arguments = ["run"] + arguments.map(\.description)
process.currentDirectoryURL = packageDirectory
let stdoutPipe = Pipe()
process.standardOutput = stdoutPipe
let stderrPipe = Pipe()
process.standardError = stderrPipe
process.standardOutput = teePipe

func setSignalForwarding(_ signalNo: Int32) {
signal(signalNo, SIG_IGN)
Expand All @@ -88,23 +198,11 @@ func swiftRunProcess(

try process.run()

return (process, stdoutPipe, stderrPipe)
return SwiftRunProcess(process: process, tee: tee, outputFile: outputFile)
}

@discardableResult
func swiftRun(_ arguments: [CustomStringConvertible], packageDirectory: URL) throws
-> SwiftRunResult
{
let (process, stdoutPipe, stderrPipe) = try swiftRunProcess(
arguments, packageDirectory: packageDirectory)
process.waitUntilExit()

let stdout = String(
data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
.trimmingCharacters(in: .whitespacesAndNewlines)

let stderr = String(
data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
.trimmingCharacters(in: .whitespacesAndNewlines)
return SwiftRunResult(exitCode: process.terminationStatus, stdout: stdout, stderr: stderr)
func swiftRun(_ arguments: [String], packageDirectory: URL) async throws -> FoundationProcessResult {
let process = try swiftRunProcess(arguments, packageDirectory: packageDirectory)
await process.waitUntilExit()
return try process.result()
}
Loading

0 comments on commit d83abb5

Please sign in to comment.