Skip to content

Commit

Permalink
feat(sdk)!: expose simulated resources via HTTP server (attempt 2) (#…
Browse files Browse the repository at this point in the history
…4697)

Reroll of the changes in #4105. Differences from last time:
- using the built-in `http` module instead of `express`
- added a small load test (examples/tests/sdk_tests/bucket/load_test.test.w)
- adjusted some TypeScript import statements to reduce bundle sizes
- added extra debug logging in the simulator for showing produced bundle sizes
- handle an additional edge case - simulator HTTP server gracefully returns 500 (instead of crashing) if it receives a request to a resource that has been cleaned up after the simulation shut-down sequence

----

This PR evolves the Wing simulator so that every simulation spins up an HTTP server. This HTTP server accepts requests to call inflight methods on resources, making it possible for simulated resources to interact across multiple processes on the same machine (such as from containers running on your system).

BREAKING CHANGE: The rawClient() method on ex.Redis has been removed. This API was infrequently used and was provided without any type information. If you have a use case for this API, let us know!

## Checklist

- [ ] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [ ] Description explains motivation and solution
- [ ] 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 Nov 1, 2023
1 parent af2b20e commit 87cdbb3
Show file tree
Hide file tree
Showing 52 changed files with 708 additions and 650 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:

test:
name: Test
timeout-minutes: 60
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion apps/wing/src/commands/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { join, resolve } from "path";
import { Target } from "@winglang/compiler";
import { describe, test, expect } from "vitest";
import { compile } from "./compile";
import { generateTmpDir } from "src/util";
import { generateTmpDir } from "../util";

const exampleDir = resolve("../../examples/tests/valid");
const exampleSmallDir = resolve("../../examples/tests/valid/subdir2");
Expand Down
2 changes: 1 addition & 1 deletion apps/wing/src/commands/pack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from "fs/promises";
import { join } from "path";
import { describe, it, expect, afterEach, vi } from "vitest";
import { pack } from "./pack";
import { exec, generateTmpDir } from "src/util";
import { exec, generateTmpDir } from "../util";

const fixturesDir = join(__dirname, "..", "..", "fixtures");
const goodFixtureDir = join(__dirname, "..", "..", "..", "..", "examples", "wing-fixture");
Expand Down
8 changes: 4 additions & 4 deletions apps/wing/src/commands/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test("wing it runs the only entrypoint file named main.w", async () => {
wingfile: resolve("main.w"),
requestedPort: 3000,
hostUtils: expect.anything(),
requireAcceptTerms: false,
requireAcceptTerms: expect.anything(),
});
expect(open).toBeCalledWith("http://localhost:3000/");
} finally {
Expand All @@ -53,7 +53,7 @@ test("wing it runs the only entrypoint file ending with .main.w", async () => {
wingfile: resolve("foo.main.w"),
requestedPort: 3000,
hostUtils: expect.anything(),
requireAcceptTerms: false,
requireAcceptTerms: expect.anything(),
});
expect(open).toBeCalledWith("http://localhost:3000/");
} finally {
Expand Down Expand Up @@ -126,7 +126,7 @@ test("wing it with a nested file runs", async () => {
wingfile: resolve(filePath),
requestedPort: 3000,
hostUtils: expect.anything(),
requireAcceptTerms: false,
requireAcceptTerms: expect.anything(),
});
expect(open).toBeCalledWith("http://localhost:3000/");
} finally {
Expand Down Expand Up @@ -161,7 +161,7 @@ test("wing it with a custom port runs", async () => {
wingfile: resolve("foo.main.w"),
requestedPort: 5000,
hostUtils: expect.anything(),
requireAcceptTerms: false,
requireAcceptTerms: expect.anything(),
});
expect(open).toBeCalledWith("http://localhost:5000/");
} finally {
Expand Down
124 changes: 67 additions & 57 deletions apps/wing/src/commands/test/test.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import { writeFileSync } from "fs";
import fs from "fs";
import { mkdtemp } from "fs/promises";
import { tmpdir } from "os";
Expand All @@ -11,6 +10,7 @@ import { filterTests, pickOneTestPerEnvironment, renderTestReport, test as wingT
import * as resultsFn from "./results";

const defaultChalkLevel = chalk.level;
const cwd = process.cwd();

describe("printing test reports", () => {
beforeEach(() => {
Expand All @@ -19,6 +19,7 @@ describe("printing test reports", () => {

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

test("resource traces are not shown if debug mode is disabled", () => {
Expand All @@ -34,7 +35,11 @@ describe("printing test reports", () => {

const testReport = renderTestReport("hello.w", EXAMPLE_TEST_RESULTS);

process.env.DEBUG = oldDebug;
if (oldDebug) {
process.env.DEBUG = oldDebug;
} else {
delete process.env.DEBUG;
}

expect(testReport).toMatchSnapshot();
expect(testReport).toContain("Push (message=cool)");
Expand All @@ -48,11 +53,11 @@ describe("test options", () => {

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

test("wing test (default entrypoint)", async () => {
const outDir = await mkdtemp(join(tmpdir(), "-wing-compile-test"));
const prevdir = process.cwd();
const logSpy = vi.spyOn(console, "log");

try {
Expand All @@ -67,64 +72,69 @@ describe("test options", () => {
expect(logSpy).toHaveBeenCalledWith("pass ─ bar.test.wsim (no tests)");
expect(logSpy).toHaveBeenCalledWith("pass ─ baz.test.wsim (no tests)");
} finally {
process.chdir(prevdir);
logSpy.mockRestore();
}
});

test("wing test with output file calls writeResultsToFile", async () => {
const outDir = await mkdtemp(join(tmpdir(), "-wing-compile-test"));
const prevdir = process.cwd();
const writeResults = vi.spyOn(resultsFn, "writeResultsToFile");
const writeFile = vi.spyOn(fs, "writeFile").mockImplementation(() => null);

try {
process.chdir(outDir);
fs.writeFileSync("test.test.w", EXAMPLE_TEST);

const outputFile = "out.json";

await wingTest(["test.test.w"], {
clean: true,
target: Target.SIM,
outputFile,
});

expect(writeResults).toBeCalledTimes(1);
const { testName, results } = writeResults.mock.calls[0][0][0];
expect(results).toMatchObject(BUCKET_TEST_RESULT);
expect(testName).toBe("test.test.w");
expect(writeResults.mock.calls[0][2]).toBe(outputFile);

expect(writeFile).toBeCalledTimes(1);
const [filePath, output] = writeFile.mock.calls[0];
expect(filePath).toBe(resolve("out.json"));
expect(JSON.parse(output as string)).toMatchObject(OUTPUT_FILE);
} finally {
writeResults.mockClear();
process.chdir(prevdir);
}
});

test("wing test without output file calls writeResultsToFile", async () => {
const writeResults = vi.spyOn(resultsFn, "writeResultsToFile");
const outDir = await mkdtemp(join(tmpdir(), "-wing-compile-test"));
const prevdir = process.cwd();

try {
process.chdir(outDir);
fs.writeFileSync("test.test.w", EXAMPLE_TEST);

await wingTest(["test.test.w"], {
clean: true,
target: Target.SIM,
});
expect(writeResults).toBeCalledTimes(0);
writeResults.mockClear();
} finally {
process.chdir(prevdir);
}
});
test(
"wing test with output file calls writeResultsToFile",
async () => {
const outDir = await mkdtemp(join(tmpdir(), "-wing-compile-test"));
const writeResults = vi.spyOn(resultsFn, "writeResultsToFile");
const writeFile = vi.spyOn(fs, "writeFile").mockImplementation(() => null);

try {
process.chdir(outDir);
fs.writeFileSync("test.test.w", EXAMPLE_TEST);

const outputFile = "out.json";

await wingTest(["test.test.w"], {
clean: true,
target: Target.SIM,
outputFile,
});

expect(writeResults).toBeCalledTimes(1);
const { testName, results } = writeResults.mock.calls[0][0][0];
expect(results).toMatchObject(BUCKET_TEST_RESULT);
expect(testName).toBe("test.test.w");
expect(writeResults.mock.calls[0][2]).toBe(outputFile);

expect(writeFile).toBeCalledTimes(1);
const [filePath, output] = writeFile.mock.calls[0];
expect(filePath).toBe(resolve("out.json"));
expect(JSON.parse(output as string)).toMatchObject(OUTPUT_FILE);
} finally {
writeResults.mockClear();
}
},
{ timeout: 10000 }
);

test(
"wing test without output file calls writeResultsToFile",
async () => {
const writeResults = vi.spyOn(resultsFn, "writeResultsToFile");
const outDir = await mkdtemp(join(tmpdir(), "-wing-compile-test"));
const prevdir = process.cwd();

try {
process.chdir(outDir);
fs.writeFileSync("test.test.w", EXAMPLE_TEST);

await wingTest(["test.test.w"], {
clean: true,
target: Target.SIM,
});
expect(writeResults).toBeCalledTimes(0);
writeResults.mockClear();
} finally {
process.chdir(prevdir);
}
},
{ timeout: 10000 }
);

test("validate output file", () => {
expect(resultsFn.validateOutputFilePath("/path/out.json")).toBeUndefined();
Expand Down
18 changes: 0 additions & 18 deletions docs/docs/04-standard-library/02-ex/redis.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ new ex.Redis();
| <code><a href="#@winglang/sdk.ex.IRedisClient.get">get</a></code> | Get value at given key. |
| <code><a href="#@winglang/sdk.ex.IRedisClient.hget">hget</a></code> | Returns the value associated with field in the hash stored at key. |
| <code><a href="#@winglang/sdk.ex.IRedisClient.hset">hset</a></code> | Sets the specified field to respective value in the hash stored at key. |
| <code><a href="#@winglang/sdk.ex.IRedisClient.rawClient">rawClient</a></code> | Get raw redis client (currently IoRedis). |
| <code><a href="#@winglang/sdk.ex.IRedisClient.sadd">sadd</a></code> | Add the specified members to the set stored at key. |
| <code><a href="#@winglang/sdk.ex.IRedisClient.set">set</a></code> | Set key value pair. |
| <code><a href="#@winglang/sdk.ex.IRedisClient.smembers">smembers</a></code> | Returns all the members of the set value stored at key. |
Expand Down Expand Up @@ -132,14 +131,6 @@ value to set at field in key.

---

##### `rawClient` <a name="rawClient" id="@winglang/sdk.ex.IRedisClient.rawClient"></a>

```wing
inflight rawClient(): any
```

Get raw redis client (currently IoRedis).

##### `sadd` <a name="sadd" id="@winglang/sdk.ex.IRedisClient.sadd"></a>

```wing
Expand Down Expand Up @@ -263,7 +254,6 @@ new ex.RedisClientBase();
| <code><a href="#@winglang/sdk.ex.RedisClientBase.get">get</a></code> | Get value at given key. |
| <code><a href="#@winglang/sdk.ex.RedisClientBase.hget">hget</a></code> | Returns the value associated with field in the hash stored at key. |
| <code><a href="#@winglang/sdk.ex.RedisClientBase.hset">hset</a></code> | Sets the specified field to respective value in the hash stored at key. |
| <code><a href="#@winglang/sdk.ex.RedisClientBase.rawClient">rawClient</a></code> | Get raw redis client (currently IoRedis). |
| <code><a href="#@winglang/sdk.ex.RedisClientBase.sadd">sadd</a></code> | Add the specified members to the set stored at key. |
| <code><a href="#@winglang/sdk.ex.RedisClientBase.set">set</a></code> | Set key value pair. |
| <code><a href="#@winglang/sdk.ex.RedisClientBase.smembers">smembers</a></code> | Returns all the members of the set value stored at key. |
Expand Down Expand Up @@ -345,14 +335,6 @@ Sets the specified field to respective value in the hash stored at key.

---

##### `rawClient` <a name="rawClient" id="@winglang/sdk.ex.RedisClientBase.rawClient"></a>

```wing
rawClient(): any
```

Get raw redis client (currently IoRedis).

##### `sadd` <a name="sadd" id="@winglang/sdk.ex.RedisClientBase.sadd"></a>

```wing
Expand Down
4 changes: 2 additions & 2 deletions examples/tests/sdk_tests/bucket/delete.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test "delete" {
try {
block();
} catch actual {
assert(actual == expected);
assert(actual.contains(expected));
error = true;
}
assert(error);
Expand All @@ -34,4 +34,4 @@ test "delete" {
b.delete("file2.txt");

assert(!b.exists("file2.txt"));
}
}
9 changes: 9 additions & 0 deletions examples/tests/sdk_tests/bucket/load_test.test.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
bring cloud;

let b = new cloud.Bucket();

test "uploading many objects" {
for i in 0..500 {
b.put("test${i}", "${i}");
}
}
2 changes: 1 addition & 1 deletion examples/tests/sdk_tests/bucket/metadata.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ test "metadata()" {
try {
b.metadata("no-such-file.txt");
} catch e {
assert(e == "Object does not exist (key=no-such-file.txt).");
assert(e.contains("Object does not exist (key=no-such-file.txt)."));
}
}
4 changes: 2 additions & 2 deletions examples/tests/sdk_tests/bucket/public_url.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test "publicUrl" {
try {
block();
} catch actual {
assert(actual == expected);
assert(actual.contains(expected));
error = true;
}
assert(error);
Expand All @@ -33,4 +33,4 @@ test "publicUrl" {
assertThrows(BUCKET_NOT_PUBLIC_ERROR, () => {
privateBucket.publicUrl("file2.txt");
});
}
}
6 changes: 3 additions & 3 deletions examples/tests/sdk_tests/queue/push.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ new std.Test(inflight () => {
q.push("");
assert(false);
} catch e {
assert(e == "Empty messages are not allowed");
assert(e.contains("Empty messages are not allowed"));
}

try {
q.push("Foo", "");
assert(false);
} catch e {
assert(e == "Empty messages are not allowed");
assert(e.contains("Empty messages are not allowed"));
}

q.push("Foo");
Expand All @@ -47,4 +47,4 @@ new std.Test(inflight () => {
assert(util.waitUntil((): bool => {
return q.approxSize() == 3;
}));
}, { timeout: 3m }) as "push";
}, timeout: 3m) as "push";
24 changes: 10 additions & 14 deletions examples/tests/sdk_tests/schedule/on_tick.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,23 @@ let from_rate = new cloud.Schedule( rate: 1m ) as "from_rate";
let c1 = new cloud.Counter() as "c1";
let c2 = new cloud.Counter() as "c2";


from_cron.onTick(inflight () => {
c1.inc();
c1.inc();
});

from_rate.onTick(inflight () => {
c2.inc();
c2.inc();
});


// std.Test is used setting the timeout property
new std.Test(inflight () => {
// counters start at zero
assert(c1.peek() == 0);
assert(c2.peek() == 0);

// wait at least one minute
util.sleep(1.1m);
let c1Value = c1.peek();
let c2Value = c2.peek();

// check that both counters have been incremented
assert(c1.peek() >= 1);
assert(c2.peek() >= 1);
// wait at least one minute
util.sleep(1.1m);

}, std.TestProps { timeout: 2m }) as "on tick is called both for rate and cron schedules";
// check that both counters have been incremented
assert(c1.peek() >= c1Value + 1);
assert(c2.peek() >= c2Value + 1);
}, timeout: 2m) as "on tick is called both for rate and cron schedules";
Loading

0 comments on commit 87cdbb3

Please sign in to comment.