Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions Sources/CartonFrontend/Commands/CartonFrontendTestCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ArgumentParser
import CartonHelpers
import CartonKit
import CartonCore
import Foundation

enum SanitizeVariant: String, CaseIterable, ExpressibleByArgument {
case stackOverflow
Expand All @@ -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]()

Expand Down Expand Up @@ -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),
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -86,6 +86,7 @@ struct BrowserTestRunner: TestRunner {
bindingAddress: bindingAddress,
port: port,
host: host,
env: options.env,
customIndexPath: nil,
resourcesPaths: resourcesPaths,
entrypoint: Constants.entrypoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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("--")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
}
}
7 changes: 6 additions & 1 deletion Sources/CartonFrontend/Commands/TestRunners/TestRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

struct TestRunnerOptions {
/// The environment variables to pass to the test process.
let env: [String: String]
}

protocol TestRunner {
func run() async throws
func run(options: TestRunnerOptions) async throws
}
7 changes: 6 additions & 1 deletion Sources/CartonKit/Server/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -194,6 +196,7 @@ public actor Server {
bindingAddress: String,
port: Int,
host: String,
env: [String: String]? = nil,
customIndexPath: AbsolutePath?,
resourcesPaths: [String],
entrypoint: Entrypoint,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions Sources/CartonKit/Server/ServerHTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down
8 changes: 4 additions & 4 deletions Sources/CartonKit/Server/StaticArchive.swift

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions Tests/CartonCommandTests/CommandTestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions Tests/CartonCommandTests/TestCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions Tests/Fixtures/EnvVarTest/Package.swift
Original file line number Diff line number Diff line change
@@ -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")]
)
8 changes: 8 additions & 0 deletions Tests/Fixtures/EnvVarTest/Tests/Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import XCTest

class Tests: XCTestCase {
func testEnvVar() {
XCTAssertEqual(ProcessInfo.processInfo.environment["FOO"], "BAR")
XCTAssertEqual(ProcessInfo.processInfo.environment["BAZ"], "QUX")
}
}
6 changes: 5 additions & 1 deletion entrypoint/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
});

Expand Down
6 changes: 5 additions & 1 deletion entrypoint/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -134,4 +138,4 @@ async function main(): Promise<void> {
}
}

main();
main();
10 changes: 10 additions & 0 deletions entrypoint/testNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down