Inherit all env-var from host and add `--env` option to `carton-test` (#495)
This change introduces a new feature in the carton-test command, allowing users to pass environment variables to the test process via a new `--env` option. Additionally, it ensures that all environment variables from the host system are inherited by the guest test process.
This commit is contained in:
parent
348d9f471e
commit
1b2abfd880
|
@ -16,6 +16,7 @@ import ArgumentParser
|
|||
import CartonHelpers
|
||||
import CartonKit
|
||||
import CartonCore
|
||||
import Foundation
|
||||
|
||||
enum SanitizeVariant: String, CaseIterable, ExpressibleByArgument {
|
||||
case stackOverflow
|
||||
|
@ -38,6 +39,24 @@ struct CartonFrontendTestCommand: AsyncParsableCommand {
|
|||
@Flag(name: .shortAndLong, help: "When specified, list all available test cases.")
|
||||
var list = false
|
||||
|
||||
@Option(name: .long, help: ArgumentHelp(
|
||||
"""
|
||||
Pass an environment variable to the test process.
|
||||
--env NAME=VALUE will set the environment variable NAME to VALUE.
|
||||
--env NAME will inherit the environment variable NAME from the parent process.
|
||||
""",
|
||||
valueName: "NAME=VALUE or NAME"
|
||||
), transform: Self.parseEnvOption(_:))
|
||||
var env: [(key: String, value: String?)] = []
|
||||
|
||||
static func parseEnvOption(_ value: String) -> (key: String, value: String?) {
|
||||
let parts = value.split(separator: "=", maxSplits: 1)
|
||||
if parts.count == 1 {
|
||||
return (String(parts[0]), nil)
|
||||
}
|
||||
return (String(parts[0]), String(parts[1]))
|
||||
}
|
||||
|
||||
@Argument(help: "The list of test cases to run in the test suite.")
|
||||
var testCases = [String]()
|
||||
|
||||
|
@ -119,16 +138,26 @@ struct CartonFrontendTestCommand: AsyncParsableCommand {
|
|||
throw ExitCode.failure
|
||||
}
|
||||
|
||||
let runner = try deriveRunner(bundlePath: bundlePath, terminal: terminal, cwd: cwd)
|
||||
let options = deriveRunnerOptions()
|
||||
try await runner.run(options: options)
|
||||
}
|
||||
|
||||
func deriveRunner(
|
||||
bundlePath: AbsolutePath,
|
||||
terminal: InteractiveWriter,
|
||||
cwd: AbsolutePath
|
||||
) throws -> TestRunner {
|
||||
switch environment {
|
||||
case .command:
|
||||
try await CommandTestRunner(
|
||||
return CommandTestRunner(
|
||||
testFilePath: bundlePath,
|
||||
listTestCases: list,
|
||||
testCases: testCases,
|
||||
terminal: terminal
|
||||
).run()
|
||||
)
|
||||
case .browser:
|
||||
try await BrowserTestRunner(
|
||||
return BrowserTestRunner(
|
||||
testFilePath: bundlePath,
|
||||
bindingAddress: bind,
|
||||
host: Server.Configuration.host(bindOption: bind, hostOption: host),
|
||||
|
@ -137,15 +166,28 @@ struct CartonFrontendTestCommand: AsyncParsableCommand {
|
|||
resourcesPaths: resources,
|
||||
pid: pid,
|
||||
terminal: terminal
|
||||
).run()
|
||||
)
|
||||
case .node:
|
||||
try await NodeTestRunner(
|
||||
return try NodeTestRunner(
|
||||
pluginWorkDirectory: AbsolutePath(validating: pluginWorkDirectory, relativeTo: cwd),
|
||||
testFilePath: bundlePath,
|
||||
listTestCases: list,
|
||||
testCases: testCases,
|
||||
terminal: terminal
|
||||
).run()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func deriveRunnerOptions() -> TestRunnerOptions {
|
||||
let parentEnv = ProcessInfo.processInfo.environment
|
||||
var env: [String: String] = parentEnv
|
||||
for (key, value) in self.env {
|
||||
if let value = value {
|
||||
env[key] = value
|
||||
} else {
|
||||
env[key] = parentEnv[key]
|
||||
}
|
||||
}
|
||||
return TestRunnerOptions(env: env)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ struct BrowserTestRunner: TestRunner {
|
|||
self.terminal = terminal
|
||||
}
|
||||
|
||||
func run() async throws {
|
||||
func run(options: TestRunnerOptions) async throws {
|
||||
let server = try await Server(
|
||||
.init(
|
||||
builder: nil,
|
||||
|
@ -86,6 +86,7 @@ struct BrowserTestRunner: TestRunner {
|
|||
bindingAddress: bindingAddress,
|
||||
port: port,
|
||||
host: host,
|
||||
env: options.env,
|
||||
customIndexPath: nil,
|
||||
resourcesPaths: resourcesPaths,
|
||||
entrypoint: Constants.entrypoint,
|
||||
|
|
|
@ -30,7 +30,7 @@ struct CommandTestRunner: TestRunner {
|
|||
let testCases: [String]
|
||||
let terminal: InteractiveWriter
|
||||
|
||||
func run() async throws {
|
||||
func run(options: TestRunnerOptions) async throws {
|
||||
let program = try ProcessInfo.processInfo.environment["CARTON_TEST_RUNNER"] ?? defaultWASIRuntime()
|
||||
terminal.write("\nRunning the test bundle with \"\(program)\":\n", inColor: .yellow)
|
||||
|
||||
|
@ -43,6 +43,9 @@ struct CommandTestRunner: TestRunner {
|
|||
if programName == "wasmtime" {
|
||||
arguments += ["--dir", "."]
|
||||
}
|
||||
for (key, value) in options.env {
|
||||
arguments += ["--env", "\(key)=\(value)"]
|
||||
}
|
||||
|
||||
if !testCases.isEmpty {
|
||||
xctestArgs.append("--")
|
||||
|
|
|
@ -28,7 +28,7 @@ struct NodeTestRunner: TestRunner {
|
|||
let testCases: [String]
|
||||
let terminal: InteractiveWriter
|
||||
|
||||
func run() async throws {
|
||||
func run(options: TestRunnerOptions) async throws {
|
||||
terminal.write("\nRunning the test bundle with Node.js:\n", inColor: .yellow)
|
||||
|
||||
let entrypointPath = try Constants.entrypoint.write(
|
||||
|
@ -63,6 +63,6 @@ struct NodeTestRunner: TestRunner {
|
|||
} else if !testCases.isEmpty {
|
||||
nodeArguments.append(contentsOf: testCases)
|
||||
}
|
||||
try await Process.run(nodeArguments, terminal)
|
||||
try await Process.run(nodeArguments, environment: options.env, terminal)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
protocol TestRunner {
|
||||
func run() async throws
|
||||
struct TestRunnerOptions {
|
||||
/// The environment variables to pass to the test process.
|
||||
let env: [String: String]
|
||||
}
|
||||
|
||||
protocol TestRunner {
|
||||
func run(options: TestRunnerOptions) async throws
|
||||
}
|
||||
|
|
|
@ -181,6 +181,8 @@ public actor Server {
|
|||
let bindingAddress: String
|
||||
let port: Int
|
||||
let host: String
|
||||
/// Environment variables to be passed to the test process.
|
||||
let env: [String: String]?
|
||||
let customIndexPath: AbsolutePath?
|
||||
let resourcesPaths: [String]
|
||||
let entrypoint: Entrypoint
|
||||
|
@ -194,6 +196,7 @@ public actor Server {
|
|||
bindingAddress: String,
|
||||
port: Int,
|
||||
host: String,
|
||||
env: [String: String]? = nil,
|
||||
customIndexPath: AbsolutePath?,
|
||||
resourcesPaths: [String],
|
||||
entrypoint: Entrypoint,
|
||||
|
@ -206,6 +209,7 @@ public actor Server {
|
|||
self.bindingAddress = bindingAddress
|
||||
self.port = port
|
||||
self.host = host
|
||||
self.env = env
|
||||
self.customIndexPath = customIndexPath
|
||||
self.resourcesPaths = resourcesPaths
|
||||
self.entrypoint = entrypoint
|
||||
|
@ -343,7 +347,8 @@ public actor Server {
|
|||
customIndexPath: configuration.customIndexPath,
|
||||
resourcesPaths: configuration.resourcesPaths,
|
||||
entrypoint: configuration.entrypoint,
|
||||
serverName: serverName.description
|
||||
serverName: serverName.description,
|
||||
env: configuration.env
|
||||
)
|
||||
let channel = try await ServerBootstrap(group: group)
|
||||
// Specify backlog and enable SO_REUSEADDR for the server itself
|
||||
|
|
|
@ -29,6 +29,7 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
let resourcesPaths: [String]
|
||||
let entrypoint: Entrypoint
|
||||
let serverName: String
|
||||
let env: [String: String]?
|
||||
}
|
||||
|
||||
struct ServerError: Error, CustomStringConvertible {
|
||||
|
@ -87,6 +88,8 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
bytes: localFileSystem.readFileContents(configuration.mainWasmPath).contents
|
||||
)
|
||||
)
|
||||
case "/process-info.json":
|
||||
response = try respondProcessInfo(context: context)
|
||||
case "/" + configuration.entrypoint.fileName:
|
||||
response = StaticResponse(
|
||||
contentType: "application/javascript",
|
||||
|
@ -218,6 +221,18 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
)
|
||||
}
|
||||
|
||||
private func respondProcessInfo(context: ChannelHandlerContext) throws -> StaticResponse {
|
||||
struct ProcessInfoBody: Encodable {
|
||||
let env: [String: String]?
|
||||
}
|
||||
let config = ProcessInfoBody(env: configuration.env)
|
||||
let json = try JSONEncoder().encode(config)
|
||||
return StaticResponse(
|
||||
contentType: "application/json", contentSize: json.count,
|
||||
body: context.channel.allocator.buffer(bytes: json)
|
||||
)
|
||||
}
|
||||
|
||||
private func respondEmpty(context: ChannelHandlerContext, status: HTTPResponseStatus) {
|
||||
var headers = HTTPHeaders()
|
||||
headers.add(name: "Connection", value: "close")
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -79,14 +79,21 @@ struct SwiftRunProcess {
|
|||
|
||||
func swiftRunProcess(
|
||||
_ arguments: [String],
|
||||
packageDirectory: URL
|
||||
packageDirectory: URL,
|
||||
environment: [String: String]? = nil
|
||||
) throws -> SwiftRunProcess {
|
||||
let swiftBin = try findSwiftExecutable().pathString
|
||||
|
||||
var outputBuffer = Array<UInt8>()
|
||||
|
||||
var environmentBlock = ProcessEnv.block
|
||||
for (key, value) in environment ?? [:] {
|
||||
environmentBlock[ProcessEnvironmentKey(key)] = value
|
||||
}
|
||||
|
||||
let process = CartonHelpers.Process(
|
||||
arguments: [swiftBin, "run"] + arguments,
|
||||
environmentBlock: environmentBlock,
|
||||
workingDirectory: try AbsolutePath(validating: packageDirectory.path),
|
||||
outputRedirection: .stream(
|
||||
stdout: { (chunk) in
|
||||
|
@ -112,10 +119,10 @@ func swiftRunProcess(
|
|||
}
|
||||
|
||||
@discardableResult
|
||||
func swiftRun(_ arguments: [String], packageDirectory: URL) async throws
|
||||
func swiftRun(_ arguments: [String], packageDirectory: URL, environment: [String: String]? = nil) async throws
|
||||
-> CartonHelpers.ProcessResult
|
||||
{
|
||||
let process = try swiftRunProcess(arguments, packageDirectory: packageDirectory)
|
||||
let process = try swiftRunProcess(arguments, packageDirectory: packageDirectory, environment: environment)
|
||||
var result = try await process.process.waitUntilExit()
|
||||
result.setOutput(.success(process.output()))
|
||||
return result
|
||||
|
|
|
@ -25,6 +25,7 @@ private enum Constants {
|
|||
static let nodeJSKitPackageName = "NodeJSKitTest"
|
||||
static let crashTestPackageName = "CrashTest"
|
||||
static let failTestPackageName = "FailTest"
|
||||
static let envVarTestPackageName = "EnvVarTest"
|
||||
}
|
||||
|
||||
final class TestCommandTests: XCTestCase {
|
||||
|
@ -92,6 +93,23 @@ final class TestCommandTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
func testEnvVar() async throws {
|
||||
var environmentsToTest: [String] = ["node", "command"]
|
||||
if Process.findExecutable("safaridriver") != nil {
|
||||
environmentsToTest.append("browser")
|
||||
}
|
||||
for environment in environmentsToTest {
|
||||
try await withFixture(Constants.envVarTestPackageName) { packageDirectory in
|
||||
let result = try await swiftRun(
|
||||
["carton", "test", "--environment", environment, "--env", "FOO=BAR"],
|
||||
packageDirectory: packageDirectory.asURL,
|
||||
environment: ["BAZ": "QUX"]
|
||||
)
|
||||
try result.checkNonZeroExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testHeadlessBrowserWithCrash() async throws {
|
||||
try await checkCartonTestFail(fixture: Constants.crashTestPackageName)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// swift-tools-version:5.5
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Test",
|
||||
dependencies: [.package(path: "../../..")],
|
||||
targets: [.testTarget(name: "EnvVarTest", path: "Tests")]
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
import XCTest
|
||||
|
||||
class Tests: XCTestCase {
|
||||
func testEnvVar() {
|
||||
XCTAssertEqual(ProcessInfo.processInfo.environment["FOO"], "BAR")
|
||||
XCTAssertEqual(ProcessInfo.processInfo.environment["BAZ"], "QUX")
|
||||
}
|
||||
}
|
|
@ -46,6 +46,7 @@ export class LineDecoder {
|
|||
|
||||
export type Options = {
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
onStdout?: (chunk: Uint8Array) => void;
|
||||
onStdoutLine?: (line: string) => void;
|
||||
onStderr?: (chunk: Uint8Array) => void;
|
||||
|
@ -90,7 +91,10 @@ export const WasmRunner = (rawOptions: Options, SwiftRuntime: SwiftRuntimeConstr
|
|||
new PreopenDirectory("/", new Map()),
|
||||
];
|
||||
|
||||
const wasi = new WASI(args, [], fds, {
|
||||
// Convert env Record to array of "key=value" strings
|
||||
const envs = options.env ? Object.entries(options.env).map(([key, value]) => `${key}=${value}`) : [];
|
||||
|
||||
const wasi = new WASI(args, envs, fds, {
|
||||
debug: false
|
||||
});
|
||||
|
||||
|
|
|
@ -42,9 +42,13 @@ const startWasiTask = async () => {
|
|||
);
|
||||
}
|
||||
|
||||
// Load configuration from the server
|
||||
const config = await fetch("/process-info.json").then((response) => response.json());
|
||||
|
||||
let testRunOutput = "";
|
||||
const wasmRunner = WasmRunner(
|
||||
{
|
||||
env: config.env,
|
||||
onStdoutLine: (line) => {
|
||||
console.log(line);
|
||||
testRunOutput += line + "\n";
|
||||
|
@ -134,4 +138,4 @@ async function main(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
main();
|
||||
main();
|
||||
|
|
|
@ -46,8 +46,18 @@ const startWasiTask = async () => {
|
|||
// No JavaScriptKit module found, run the Wasm module without JSKit
|
||||
}
|
||||
|
||||
// carton-frontend passes all environment variables to the test Node process.
|
||||
const env: Record<string, string> = {};
|
||||
for (const key in process.env) {
|
||||
const value = process.env[key];
|
||||
if (value) {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const wasmRunner = WasmRunner({
|
||||
args: testArgs,
|
||||
env,
|
||||
onStdoutLine: (line) => {
|
||||
console.log(line);
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue