Skip to content

Commit

Permalink
feat(sdk): simulator state (#5164)
Browse files Browse the repository at this point in the history
This PR adds the capability for the Wing simulator to store resource state across compilations. This means if you performed a lot of work to fill a `cloud.Bucket` with objects or values in `cloud.Counter`, it will be remembered when you recompile and reload the application as long as the resource is still in your app. This will be extended to other resources like Queue and Workload in the future.

Here's a screen recording - notice how even as the app is updated with new resources or different inflight code, the state of resources doesn't go away.

https://github.com/winglang/wing/assets/5008987/a877e0f1-8085-4264-bf32-1ac7c971af4c

Notes:
- The cloud.TestRunner resource is now only added to a Wing app's construct tree when it is compiled in test mode (with `wing test`).
- The Wing Console now runs two copies of the app, one compiled in the default mode, and one compiled with tests. The resource graph only shows the default app, but the tests panel shows a list of all tests from the app compiled with the simulator, and clicking any of the tests will run them with fresh state every time.

Future work:

- #5197
- #5198

## 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)
- [ ] Docs updated (only required for features)
- [ ] 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
Chriscbr authored Dec 14, 2023
1 parent 27624eb commit 698e645
Show file tree
Hide file tree
Showing 386 changed files with 1,251 additions and 8,075 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ describe(`${__dirname}/main.w`, () => {

const currentValue = page.getByTestId("cloud.counter:current-value");

await expect(currentValue).toHaveValue("0");
await expect(currentValue).toHaveValue("1");

await page.getByTestId("cloud.counter:decrease").click();

await expect(currentValue).toHaveValue("-1");
await expect(currentValue).toHaveValue("0");
});

test("resets counter", async ({ page }) => {
Expand Down
5 changes: 5 additions & 0 deletions apps/wing-console/console/server/src/expressServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { LogInterface } from "./utils/LogInterface.js";

export interface CreateExpressServerOptions {
simulatorInstance(): Promise<simulator.Simulator>;
testSimulatorInstance(): Promise<simulator.Simulator>;
consoleLogger: ConsoleLogger;
errorMessage(): string | undefined;
emitter: Emittery<{
Expand All @@ -47,6 +48,7 @@ export interface CreateExpressServerOptions {

export const createExpressServer = async ({
simulatorInstance,
testSimulatorInstance,
consoleLogger,
errorMessage,
emitter,
Expand Down Expand Up @@ -74,6 +76,9 @@ export const createExpressServer = async ({
async simulator() {
return await simulatorInstance();
},
async testSimulator() {
return await testSimulatorInstance();
},
async appDetails() {
return {
wingVersion: await getWingVersion(),
Expand Down
20 changes: 19 additions & 1 deletion apps/wing-console/console/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import type { inferRouterInputs } from "@trpc/server";
import { prettyPrintError } from "@winglang/sdk/lib/util/enhanced-error.js";
import Emittery from "emittery";
Expand Down Expand Up @@ -107,7 +111,7 @@ export const createConsoleServer = async ({
log,
});

const compiler = createCompiler({ wingfile, platform });
const compiler = createCompiler({ wingfile, platform, testing: false });
let isStarting = false;
let isStopping = false;

Expand All @@ -122,6 +126,16 @@ export const createConsoleServer = async ({
}
});

const testCompiler = createCompiler({
wingfile,
platform,
testing: true,
});
const testSimulator = createSimulator();
testCompiler.on("compiled", ({ simfile }) => {
testSimulator.start(simfile);
});

let lastErrorMessage = "";
let selectedNode = "";
let tests: TestItem[] = [];
Expand Down Expand Up @@ -221,6 +235,10 @@ export const createConsoleServer = async ({

const { server, port } = await createExpressServer({
consoleLogger,
testSimulatorInstance() {
const statedir = mkdtempSync(join(tmpdir(), "wing-console-test-"));
return testSimulator.instance(statedir);
},
simulatorInstance() {
return simulator.instance();
},
Expand Down
20 changes: 12 additions & 8 deletions apps/wing-console/console/server/src/router/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ const listTests = (simulator: Simulator): Promise<string[]> => {
return testRunner.listTests();
};

const reloadSimulator = async (simulator: Simulator, logger: ConsoleLogger) => {
logger.verbose("Reloading simulator...", "console", {
messageType: "info",
});
await simulator.reload(true);
};

const runTest = async (
simulator: Simulator,
resourcePath: string,
logger: ConsoleLogger,
): Promise<InternalTestResult> => {
logger.log("Reloading simulator...", "console", {
messageType: "info",
});
await simulator.reload();

const client = simulator.getResource(
"root/cloud.TestRunner",
) as ITestRunnerClient;
Expand Down Expand Up @@ -80,7 +82,7 @@ export interface InternalTestResult extends TestResult {
export const createTestRouter = () => {
return createRouter({
"test.list": createProcedure.query(async ({ input, ctx }) => {
const simulator = await ctx.simulator();
const simulator = await ctx.testSimulator();
const list = await listTests(simulator);

const testsState = ctx.testsStateManager();
Expand All @@ -103,8 +105,9 @@ export const createTestRouter = () => {
}),
)
.mutation(async ({ input, ctx }) => {
await reloadSimulator(await ctx.testSimulator(), ctx.logger);
const response = await runTest(
await ctx.simulator(),
await ctx.testSimulator(),
input.resourcePath,
ctx.logger,
);
Expand All @@ -120,7 +123,8 @@ export const createTestRouter = () => {
return response;
}),
"test.runAll": createProcedure.mutation(async ({ ctx }) => {
const simulator = await ctx.simulator();
const simulator = await ctx.testSimulator();
await reloadSimulator(simulator, ctx.logger);
const testsState = ctx.testsStateManager();

const testList = await listTests(simulator);
Expand Down
3 changes: 3 additions & 0 deletions apps/wing-console/console/server/src/utils/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ export interface Compiler {
export interface CreateCompilerProps {
wingfile: string;
platform?: string[];
testing?: boolean;
}

export const createCompiler = ({
wingfile,
platform = [wing.BuiltinPlatform.SIM],
testing = false,
}: CreateCompilerProps): Compiler => {
const events = new Emittery<CompilerEvents>();
let isCompiling = false;
Expand All @@ -44,6 +46,7 @@ export const createCompiler = ({
await events.emit("compiling");
const simfile = await wing.compile(wingfile, {
platform,
testing,
});
await events.emit("compiled", { simfile });
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions apps/wing-console/console/server/src/utils/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface FileLink {

export interface RouterContext {
simulator(): Promise<simulator.Simulator>;
testSimulator(): Promise<simulator.Simulator>;
appDetails(): Promise<{
wingVersion: string | undefined;
}>;
Expand Down
2 changes: 1 addition & 1 deletion apps/wing-console/console/server/src/utils/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface SimulatorEvents {
}

export interface Simulator {
instance(): Promise<simulator.Simulator>;
instance(statedir?: string): Promise<simulator.Simulator>;
start(simfile: string): Promise<void>;
stop(): Promise<void>;
on<T extends keyof SimulatorEvents>(
Expand Down
20 changes: 0 additions & 20 deletions apps/wing-console/console/ui/src/ui/map-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,6 @@ export const MapControls = ({}: MapControlsProps) => {
<ToolbarButton title="Zoom to fit" onClick={zoomToFit}>
<ArrowsPointingOutIcon className="w-4 h-4" />
</ToolbarButton>

{testsExists && (
<div className="ml-2">
<ToolbarButton
title={showTests ? "Hide tests" : "Show tests"}
onClick={() => setShowTests(!showTests)}
>
<div className="flex items-center gap-1">
{showTests ? (
<EyeSlashIcon className="w-4 h-4" />
) : (
<EyeIcon className="w-4 h-4" />
)}
<span className="text-xs px-0.5">
{showTests ? "Hide tests" : "Show tests"}
</span>
</div>
</ToolbarButton>
</div>
)}
</Toolbar>
</div>
</div>
Expand Down
11 changes: 4 additions & 7 deletions apps/wing/src/commands/test/test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,16 @@ describe("wing test (no options)", () => {

describe("output-file option", () => {
let writeResultsSpy: SpyInstance;
let writeFileSpy: SpyInstance;

beforeEach(() => {
chalk.level = 0;
writeResultsSpy = vi.spyOn(resultsFn, "writeResultsToFile");
writeFileSpy = vi.spyOn(fsPromises, "writeFile");
});

afterEach(() => {
chalk.level = defaultChalkLevel;
process.chdir(cwd);
writeResultsSpy.mockRestore();
writeFileSpy.mockRestore();
});

test("wing test with output file calls writeResultsToFile", async () => {
Expand All @@ -158,10 +155,10 @@ describe("output-file option", () => {
expect(testName).toBe("test.test.w");
expect(writeResultsSpy.mock.calls[0][2]).toBe(outputFile);

expect(writeFileSpy).toBeCalledTimes(2);
const [filePath, output] = writeFileSpy.mock.calls[1];
expect(filePath).toBe("out.json");
expect(JSON.parse(output as string)).toMatchObject(OUTPUT_FILE);
const outputFileExists = fs.existsSync(outputFile);
expect(outputFileExists).toBe(true);
const outputContents = fs.readFileSync(outputFile, "utf-8");
expect(JSON.parse(outputContents)).toMatchObject(OUTPUT_FILE);
});

test("wing test without output file calls writeResultsToFile", async () => {
Expand Down
8 changes: 1 addition & 7 deletions libs/awscdk/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ export class App extends core.App {
private synthedOutput: string | undefined;
private synthHooks?: core.SynthHooks;

/**
* The test runner for this app.
*/
protected readonly testRunner: TestRunner;

constructor(props: CdkAppProps) {
let stackName = props.stackName ?? process.env.CDK_STACK_NAME;
if (stackName === undefined) {
Expand Down Expand Up @@ -113,9 +108,8 @@ export class App extends core.App {
this.synthed = false;
this.isTestEnvironment = props.isTestEnvironment ?? false;
registerTokenResolver(new CdkTokens());
this.testRunner = new TestRunner(this, "cloud.TestRunner");

this.synthRoots(props, this.testRunner);
TestRunner._createTree(this, props.rootConstruct);
}

/**
Expand Down
40 changes: 0 additions & 40 deletions libs/awscdk/test/__snapshots__/bucket.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

exports[`bucket is public 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down Expand Up @@ -104,11 +99,6 @@ exports[`bucket is public 1`] = `

exports[`bucket with onCreate method 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down Expand Up @@ -561,11 +551,6 @@ def submit_response(event: dict, context, response_status: str, error_message: s

exports[`bucket with onDelete method 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down Expand Up @@ -1018,11 +1003,6 @@ def submit_response(event: dict, context, response_status: str, error_message: s

exports[`bucket with onEvent method 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down Expand Up @@ -1497,11 +1477,6 @@ def submit_response(event: dict, context, response_status: str, error_message: s

exports[`bucket with onUpdate method 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down Expand Up @@ -1954,11 +1929,6 @@ def submit_response(event: dict, context, response_status: str, error_message: s

exports[`bucket with two preflight files 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down Expand Up @@ -2266,11 +2236,6 @@ exports[`bucket with two preflight files 1`] = `

exports[`bucket with two preflight objects 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down Expand Up @@ -2578,11 +2543,6 @@ exports[`bucket with two preflight objects 1`] = `

exports[`create a bucket 1`] = `
{
"Outputs": {
"WingTestRunnerFunctionArns": {
"Value": "[]",
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down
Loading

0 comments on commit 698e645

Please sign in to comment.