diff --git a/apps/wing/src/cli.ts b/apps/wing/src/cli.ts index 3d8c68a8094..8858d658ca7 100644 --- a/apps/wing/src/cli.ts +++ b/apps/wing/src/cli.ts @@ -152,6 +152,7 @@ async function main() { .default("sim") ) .option("-p, --plugins [plugin...]", "Compiler plugins") + .option("--no-clean", "Keep build output") .hook("preAction", progressHook) .hook("preAction", collectAnalyticsHook) .action(runSubCommand("test")); diff --git a/apps/wing/src/commands/test.ts b/apps/wing/src/commands/test.ts index fe6776e3922..efc38b01dab 100644 --- a/apps/wing/src/commands/test.ts +++ b/apps/wing/src/commands/test.ts @@ -1,4 +1,4 @@ -import { basename, sep } from "path"; +import { basename, resolve, sep } from "path"; import { compile, CompileOptions } from "./compile"; import chalk from "chalk"; import { std, testing } from "@winglang/sdk"; @@ -23,7 +23,9 @@ const generateTestName = (path: string) => path.split(sep).slice(-2).join("/"); /** * Options for the `test` command. */ -export interface TestOptions extends CompileOptions {} +export interface TestOptions extends CompileOptions { + clean: boolean; +} export async function test(entrypoints: string[], options: TestOptions): Promise { const startTime = Date.now(); @@ -129,7 +131,8 @@ function printResults( async function testOne(entrypoint: string, options: TestOptions) { // since the test cleans up after each run, it's essential to create a temporary directory- // at least one that is different then the usual compilation dir, otherwise we might end up cleaning up the user's actual resources. - const tempFile: string = Target.SIM ? entrypoint : await generateTmpDir(entrypoint); + const tempFile: string = + options.target === Target.SIM ? entrypoint : await generateTmpDir(entrypoint); const synthDir = await withSpinner( `Compiling ${generateTestName(entrypoint)} to ${options.target}...`, () => @@ -141,11 +144,11 @@ async function testOne(entrypoint: string, options: TestOptions) { switch (options.target) { case Target.SIM: - return await testSimulator(synthDir); + return await testSimulator(synthDir, options); case Target.TF_AWS: - return await testTfAws(synthDir); + return await testTfAws(synthDir, options); case Target.AWSCDK: - return await testAwsCdk(synthDir); + return await testAwsCdk(synthDir, options); default: throw new Error(`unsupported target ${options.target}`); } @@ -235,8 +238,15 @@ function testResultsContainsFailure(results: std.TestResult[]): boolean { return results.some((r) => !r.pass); } -async function testSimulator(synthDir: string) { +function noCleanUp(synthDir: string) { + console.log( + chalk.yellowBright.bold(`Cleanup is disabled!\nOutput files available at ${resolve(synthDir)}`) + ); +} + +async function testSimulator(synthDir: string, options: TestOptions) { const s = new testing.Simulator({ simfile: synthDir }); + const { clean } = options; await s.start(); const testRunner = s.getResource("root/cloud.TestRunner") as std.ITestRunnerClient; @@ -254,12 +264,17 @@ async function testSimulator(synthDir: string) { const testReport = renderTestReport(synthDir, results); console.log(testReport); - rmSync(synthDir, { recursive: true, force: true }); + if (clean) { + rmSync(synthDir, { recursive: true, force: true }); + } else { + noCleanUp(synthDir); + } return results; } -async function testAwsCdk(synthDir: string): Promise { +async function testAwsCdk(synthDir: string, options: TestOptions): Promise { + const { clean } = options; try { isAwsCdkInstalled(synthDir); @@ -301,7 +316,11 @@ async function testAwsCdk(synthDir: string): Promise { console.warn((err as Error).message); return [{ pass: false, path: "", error: (err as Error).message, traces: [] }]; } finally { - await cleanupCdk(synthDir); + if (clean) { + await cleanupCdk(synthDir); + } else { + noCleanUp(synthDir); + } } } @@ -339,7 +358,8 @@ async function awsCdkOutput(synthDir: string, name: string, stackName: string) { return parsed[stackName][name]; } -async function testTfAws(synthDir: string): Promise { +async function testTfAws(synthDir: string, options: TestOptions): Promise { + const { clean } = options; try { if (!isTerraformInstalled(synthDir)) { throw new Error( @@ -382,7 +402,11 @@ async function testTfAws(synthDir: string): Promise { console.warn((err as Error).message); return [{ pass: false, path: "", error: (err as Error).message, traces: [] }]; } finally { - await cleanupTf(synthDir); + if (clean) { + await cleanupTf(synthDir); + } else { + noCleanUp(synthDir); + } } } diff --git a/docs/docs/02-concepts/04-tests.md b/docs/docs/02-concepts/04-tests.md index 59e0239e8f3..7bd3b4121c6 100644 --- a/docs/docs/02-concepts/04-tests.md +++ b/docs/docs/02-concepts/04-tests.md @@ -8,12 +8,15 @@ keywords: [Wing test, multi-cloud] Winglang incorporates a lightweight testing framework, which is built around the `wing test` command and the `test` keyword. It lets you to run the same tests, in isolation, on both the Wing simulator and in the cloud. ### How to create a test + You can create a test by adding the following code structure to any Winglang file (.w): + ```ts wing test "" { - // test code + // test code } ``` + If a test throws an exception (typically using the `assert` function), it's considered to have failed. Here's an example: @@ -28,13 +31,14 @@ test "abs" { ``` ### Running tests in the Simulator + You can execute all tests in a `.w` file in the simulator using the following command: ```sh -% wing test example.w --target sim +% wing test example.w --target sim pass ─ example.wsim » root/env0/test:abs - + Tests 1 passed (1) Test Files 1 passed (1) Duration 0m0.54s @@ -43,6 +47,7 @@ Duration 0m0.54s **Notice:** The `--target sim` argument can be omitted as it's the default target for the wing test command. ### Tests run in isolation + Every Winglang test is executed in complete isolation. Take a look at the following code: ```ts playground @@ -92,9 +97,10 @@ test "bucket onCreate" { ``` -Running the test on the tf-aws target +Running the test on the tf-aws target + ```sh -% wing test example.w -t tf-aws +% wing test example.w -t tf-aws ✔ Compiling example.w to tf-aws... ✔ terraform init ✔ terraform apply @@ -102,8 +108,27 @@ Running the test on the tf-aws target ✔ Running tests... pass ─ example.tfaws » root/Default/env0/test:bucket onCreate ✔ terraform destroy - - + + +Tests 1 passed (1) +Test Files 1 passed (1) +Duration 1m31.44s +``` + +By default, the tested resources will be destroyed at the end of the test. You can use the `--no-clean` to keep them up. The path to the randomized directory containing the output files will be displayed at the end of the test. + +```sh +% wing test example.w -t tf-aws --no-clean +✔ Compiling example.w to tf-aws... +✔ terraform init +✔ terraform apply +✔ Setting up test runner... +✔ Running tests... +pass ─ example.tfaws » root/Default/env0/test:bucket onCreate +✔ terraform destroy +Clean up is disabled! +Output files available at /var/folders/1m/..../example.tfaws + Tests 1 passed (1) Test Files 1 passed (1) Duration 1m31.44s @@ -113,7 +138,8 @@ Duration 1m31.44s Wing Console provides a straightforward method to run either a single test or all your tests. -Consider the following code: +Consider the following code: + ```ts playground // example.w bring cloud; @@ -138,5 +164,3 @@ test "this test should fail" { Refer to the TESTS section in the image below. You have the option to run all tests or a single test. ![image](https://github.com/winglang/wing/assets/1727147/7d5ebc00-9316-41d1-9a3c-0e28e195d077) - -