Skip to content

Commit

Permalink
chore: test harness interface (#7248)
Browse files Browse the repository at this point in the history
Extract common logic across Terraform and AWS CDK targets when running
tests. This is an initial step towards allowing custom platforms to
provide test running capabilities.

## Checklist

- [x] Title matches [Winglang's style
guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [x] Description explains motivation and solution
- [x] Tests added (always)
- [x] Docs updated (only required for features)
- [x] Added `pr/e2e-full` label if this feature requires end-to-end
testing

*By submitting this pull request, I confirm that my contribution is made
under the terms of the [Wing Cloud Contribution
License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
eladb authored Jan 20, 2025
1 parent 023ab50 commit 0dca2e2
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 195 deletions.
19 changes: 19 additions & 0 deletions packages/winglang/src/commands/test/harness/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ITestRunnerClient } from "@winglang/sdk/lib/std";

/**
* API for running wing tests.
*/
export interface ITestHarness {
/**
* Deploys the test program synthesized in the given directory and return an `ITestRunnerClient`
* that can be used to run the tests.
* @param synthDir - The directory containing the synthesized test program.
*/
deploy(synthDir: string): Promise<ITestRunnerClient>;

/**
* Cleans up the test harness after the tests have been run.
* @param synthDir - The directory containing the synthesized test program.
*/
cleanup(synthDir: string): Promise<void>;
}
48 changes: 48 additions & 0 deletions packages/winglang/src/commands/test/harness/awscdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { readFile, rm } from "fs/promises";
import { ITestRunnerClient } from "@winglang/sdk/lib/std";
import { Util } from "@winglang/sdk/lib/util";
import { ITestHarness } from "./api";
import { withSpinner } from "../../../util";
import { execCapture } from "../util";

const ENV_WING_TEST_RUNNER_FUNCTION_IDENTIFIERS_AWSCDK = "WingTestRunnerFunctionArns";

export class AwsCdkTestHarness implements ITestHarness {
public async deploy(synthDir: string): Promise<ITestRunnerClient> {
try {
await execCapture("cdk version --ci true", { cwd: synthDir });
} catch (err) {
throw new Error(
"AWS-CDK is not installed. Please install AWS-CDK to run tests in the cloud (npm i -g aws-cdk)."
);
}

await withSpinner("cdk deploy", () =>
execCapture("cdk deploy --require-approval never --ci true -O ./output.json --app . ", {
cwd: synthDir,
})
);

const stackName = process.env.CDK_STACK_NAME! + Util.sha256(synthDir).slice(-8);
const testArns = await this.getFunctionArnsOutput(synthDir, stackName);

const { TestRunnerClient } = await import("@winglang/sdk/lib/shared-aws/test-runner.inflight");
const runner = new TestRunnerClient({ $tests: testArns });
return runner;
}

public async cleanup(synthDir: string): Promise<void> {
await withSpinner("aws-cdk destroy", async () => {
await rm(synthDir.concat("/output.json"));
await execCapture("cdk destroy -f --ci true --app ./", { cwd: synthDir });
});

await rm(synthDir, { recursive: true, force: true });
}

private async getFunctionArnsOutput(synthDir: string, stackName: string) {
const file = await readFile(synthDir.concat("/output.json"));
const parsed = JSON.parse(Buffer.from(file).toString());
return parsed[stackName][ENV_WING_TEST_RUNNER_FUNCTION_IDENTIFIERS_AWSCDK];
}
}
72 changes: 72 additions & 0 deletions packages/winglang/src/commands/test/harness/terraform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { rm } from "fs/promises";
import { BuiltinPlatform, determineTargetFromPlatforms } from "@winglang/compiler";
import { ITestRunnerClient } from "@winglang/sdk/lib/std";
import { ITestHarness } from "./api";
import { withSpinner } from "../../../util";
import { TestOptions } from "../test";
import { execCapture } from "../util";

const ENV_WING_TEST_RUNNER_FUNCTION_IDENTIFIERS = "WING_TEST_RUNNER_FUNCTION_IDENTIFIERS";
const PARALLELISM = { [BuiltinPlatform.TF_AZURE]: 5 };
const targetFolder: Record<string, string> = {
[BuiltinPlatform.TF_AWS]: "shared-aws",
[BuiltinPlatform.TF_AZURE]: "shared-azure",
[BuiltinPlatform.TF_GCP]: "shared-gcp",
};

export class TerraformTestHarness implements ITestHarness {
private readonly options: TestOptions;
private readonly parallelism: string;

constructor(options: TestOptions) {
this.options = options;
const p = PARALLELISM[options.platform[0]];
this.parallelism = p ? `-parallelism=${p}` : "";
}

public async deploy(synthDir: string): Promise<ITestRunnerClient> {
// Check if Terraform is installed
const tfVersion = await execCapture("terraform version", { cwd: synthDir });
const installed = tfVersion.startsWith("Terraform v");
if (!installed) {
throw new Error(
"Terraform is not installed. Please install Terraform to run tests in the cloud."
);
}

// Initialize Terraform
await withSpinner("terraform init", () => execCapture("terraform init", { cwd: synthDir }));

// Apply Terraform
await withSpinner("terraform apply", () =>
execCapture(`terraform apply -auto-approve ${this.parallelism}`, { cwd: synthDir })
);

// Get the test runner function ARNs
const output = await execCapture("terraform output -json", { cwd: synthDir });
const parsed = JSON.parse(output);
const testArns = parsed[ENV_WING_TEST_RUNNER_FUNCTION_IDENTIFIERS]?.value;
if (!testArns) {
throw new Error(`terraform output ${ENV_WING_TEST_RUNNER_FUNCTION_IDENTIFIERS} not found`);
}

// Create the test runner client
const target = determineTargetFromPlatforms(this.options.platform);
const testRunnerPath = `@winglang/sdk/lib/${targetFolder[target]}/test-runner.inflight`;
const { TestRunnerClient } = await import(testRunnerPath);
const runner = new TestRunnerClient({ $tests: testArns });
return runner;
}

public async cleanup(synthDir: string): Promise<void> {
try {
await withSpinner("terraform destroy", () =>
execCapture(`terraform destroy -auto-approve ${this.parallelism}`, { cwd: synthDir })
);

await rm(synthDir, { recursive: true, force: true });
} catch (e) {
console.error(e);
}
}
}
Loading

0 comments on commit 0dca2e2

Please sign in to comment.