blob: 8ebc3cef00ab892de83dd099e12bb2c026cbf2a0 [file] [log] [blame]
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2015 - 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
#if DEPLOYMENT_RUNTIME_OBJC || os(Linux)
import Foundation
import XCTest
#else
import SwiftFoundation
import SwiftXCTest
#endif
class TestProcess : XCTestCase {
static var allTests: [(String, (TestProcess) -> () throws -> Void)] {
#if os(Android)
return []
#else
return [
("test_exit0" , test_exit0),
("test_exit1" , test_exit1),
("test_exit100" , test_exit100),
("test_sleep2", test_sleep2),
("test_sleep2_exit1", test_sleep2_exit1),
("test_terminationReason_uncaughtSignal", test_terminationReason_uncaughtSignal),
("test_pipe_stdin", test_pipe_stdin),
("test_pipe_stdout", test_pipe_stdout),
("test_pipe_stderr", test_pipe_stderr),
("test_current_working_directory", test_current_working_directory),
("test_pipe_stdout_and_stderr_same_pipe", test_pipe_stdout_and_stderr_same_pipe),
("test_file_stdout", test_file_stdout),
("test_passthrough_environment", test_passthrough_environment),
("test_no_environment", test_no_environment),
("test_custom_environment", test_custom_environment),
]
#endif
}
#if !os(Android)
func test_exit0() {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", "exit 0"]
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 0)
XCTAssertEqual(process.terminationReason, .exit)
}
func test_exit1() {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", "exit 1"]
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 1)
XCTAssertEqual(process.terminationReason, .exit)
}
func test_exit100() {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", "exit 100"]
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 100)
XCTAssertEqual(process.terminationReason, .exit)
}
func test_sleep2() {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", "sleep 2"]
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 0)
XCTAssertEqual(process.terminationReason, .exit)
}
func test_sleep2_exit1() {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", "sleep 2; exit 1"]
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 1)
XCTAssertEqual(process.terminationReason, .exit)
}
func test_terminationReason_uncaughtSignal() {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", "kill -TERM $$"]
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 15)
XCTAssertEqual(process.terminationReason, .uncaughtSignal)
}
func test_pipe_stdin() {
let process = Process()
process.launchPath = "/bin/cat"
let outputPipe = Pipe()
process.standardOutput = outputPipe
let inputPipe = Pipe()
process.standardInput = inputPipe
process.launch()
inputPipe.fileHandleForWriting.write("Hello, 🐶.\n".data(using: .utf8)!)
// Close the input pipe to send EOF to cat.
inputPipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 0)
let data = outputPipe.fileHandleForReading.availableData
guard let string = String(data: data, encoding: .utf8) else {
XCTFail("Could not read stdout")
return
}
XCTAssertEqual(string, "Hello, 🐶.\n")
}
func test_pipe_stdout() {
let process = Process()
process.launchPath = "/usr/bin/which"
process.arguments = ["which"]
let pipe = Pipe()
process.standardOutput = pipe
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 0)
let data = pipe.fileHandleForReading.availableData
guard let string = String(data: data, encoding: .ascii) else {
XCTFail("Could not read stdout")
return
}
XCTAssertTrue(string.hasSuffix("/which\n"))
}
func test_pipe_stderr() {
let process = Process()
process.launchPath = "/bin/cat"
process.arguments = ["invalid_file_name"]
let errorPipe = Pipe()
process.standardError = errorPipe
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 1)
let data = errorPipe.fileHandleForReading.availableData
guard let _ = String(data: data, encoding: .ascii) else {
XCTFail("Could not read stdout")
return
}
// testing the return value of an external process does not port well, and may change.
// XCTAssertEqual(string, "/bin/cat: invalid_file_name: No such file or directory\n")
}
func test_pipe_stdout_and_stderr_same_pipe() {
let process = Process()
process.launchPath = "/bin/cat"
process.arguments = ["invalid_file_name"]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
// Clear the environment to stop the malloc debug flags used in Xcode debug being
// set in the subprocess.
process.environment = [:]
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 1)
let data = pipe.fileHandleForReading.availableData
guard let string = String(data: data, encoding: .ascii) else {
XCTFail("Could not read stdout")
return
}
// Remove the leading '/bin/' since on macOS '/bin/cat' just outputs 'cat:'
let searchStr = "/bin/"
let errMsg = string.replacingOccurrences(of: searchStr, with: "", options: [.literal, .anchored],
range: searchStr.startIndex..<searchStr.endIndex)
XCTAssertEqual(errMsg, "cat: invalid_file_name: No such file or directory\n")
}
func test_file_stdout() {
let process = Process()
process.launchPath = "/usr/bin/which"
process.arguments = ["which"]
mkstemp(template: "TestProcess.XXXXXX") { handle in
process.standardOutput = handle
process.launch()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 0)
handle.seek(toFileOffset: 0)
let data = handle.readDataToEndOfFile()
guard let string = String(data: data, encoding: .ascii) else {
XCTFail("Could not read stdout")
return
}
XCTAssertTrue(string.hasSuffix("/which\n"))
}
}
func test_passthrough_environment() {
do {
let (output, _) = try runTask(["/usr/bin/env"], environment: nil)
let env = try parseEnv(output)
XCTAssertGreaterThan(env.count, 0)
} catch let error {
XCTFail("Test failed: \(error)")
}
}
func test_no_environment() {
do {
let (output, _) = try runTask(["/usr/bin/env"], environment: [:])
let env = try parseEnv(output)
XCTAssertEqual(env.count, 0)
} catch let error {
XCTFail("Test failed: \(error)")
}
}
func test_custom_environment() {
do {
let input = ["HELLO": "WORLD", "HOME": "CUPERTINO"]
let (output, _) = try runTask(["/usr/bin/env"], environment: input)
let env = try parseEnv(output)
XCTAssertEqual(env, input)
} catch let error {
XCTFail("Test failed: \(error)")
}
}
func test_current_working_directory() {
do {
let previousWorkingDirectory = FileManager.default.currentDirectoryPath
// Darwin Foundation requires the full path to the executable (.launchPath)
let (output, _) = try runTask(["/bin/bash", "-c", "pwd"], currentDirectoryPath: "/bin")
XCTAssertEqual(output.trimmingCharacters(in: .newlines), "/bin")
XCTAssertEqual(previousWorkingDirectory, FileManager.default.currentDirectoryPath)
} catch let error {
XCTFail("Test failed: \(error)")
}
}
#endif
}
private func mkstemp(template: String, body: (FileHandle) throws -> Void) rethrows {
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("TestProcess.XXXXXX")
try url.withUnsafeFileSystemRepresentation {
switch mkstemp(UnsafeMutablePointer(mutating: $0!)) {
case -1: XCTFail("Could not create temporary file")
case let fd:
defer { url.withUnsafeFileSystemRepresentation { _ = unlink($0!) } }
try body(FileHandle(fileDescriptor: fd, closeOnDealloc: true))
}
}
}
private enum Error: Swift.Error {
case TerminationStatus(Int32)
case UnicodeDecodingError(Data)
case InvalidEnvironmentVariable(String)
}
#if !os(Android)
private func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) {
let process = Process()
var arguments = arguments
process.launchPath = arguments.removeFirst()
process.arguments = arguments
// Darwin Foundation doesnt allow .environment to be set to nil although the documentation
// says it is an optional. https://developer.apple.com/documentation/foundation/process/1409412-environment
if let e = environment {
process.environment = e
}
if let directoryPath = currentDirectoryPath {
process.currentDirectoryPath = directoryPath
}
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
process.launch()
process.waitUntilExit()
guard process.terminationStatus == 0 else {
throw Error.TerminationStatus(process.terminationStatus)
}
let stdoutData = stdoutPipe.fileHandleForReading.availableData
guard let stdout = String(data: stdoutData, encoding: .utf8) else {
throw Error.UnicodeDecodingError(stdoutData)
}
let stderrData = stderrPipe.fileHandleForReading.availableData
guard let stderr = String(data: stderrData, encoding: .utf8) else {
throw Error.UnicodeDecodingError(stderrData)
}
return (stdout, stderr)
}
private func parseEnv(_ env: String) throws -> [String: String] {
var result = [String: String]()
for line in env.components(separatedBy: "\n") where line != "" {
guard let range = line.range(of: "=") else {
throw Error.InvalidEnvironmentVariable(line)
}
result[String(line[..<range.lowerBound])] = String(line[range.upperBound...])
}
return result
}
#endif