Skip to content

Commit d17e7bd

Browse files
test(evm-api-worker): increase unit test coverage (#1319)
* Tests * Remove defaults schema * Remove joi
1 parent 3b45e75 commit d17e7bd

10 files changed

Lines changed: 483 additions & 10 deletions

File tree

packages/evm-api-worker/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
"dependencies": {
2424
"@mainsail/constants": "workspace:*",
2525
"@mainsail/container": "workspace:*",
26-
"@mainsail/kernel": "workspace:*",
27-
"joi": "18.2.1"
26+
"@mainsail/kernel": "workspace:*"
2827
},
2928
"devDependencies": {
3029
"@mainsail/contracts": "workspace:*",
3130
"@mainsail/test-runner": "workspace:*",
31+
"esmock": "2.7.5",
3232
"uvu": "0.5.6"
3333
},
3434
"engines": {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Identifiers } from "@mainsail/constants";
2+
import { Application } from "@mainsail/kernel";
3+
4+
import { describe } from "@mainsail/test-runner";
5+
import { CommitHandler } from "./commit";
6+
7+
describe<{
8+
app: Application;
9+
handler: CommitHandler;
10+
stateStore: any;
11+
logger: any;
12+
}>("CommitHandler", ({ assert, beforeEach, it, spy }) => {
13+
beforeEach((context) => {
14+
context.stateStore = { setBlockNumber: () => {} };
15+
context.logger = { error: () => {} };
16+
17+
context.app = new Application();
18+
context.app.bind(Identifiers.State.Store).toConstantValue(context.stateStore);
19+
context.app.bind(Identifiers.Services.Log.Service).toConstantValue(context.logger);
20+
21+
context.handler = context.app.resolve(CommitHandler);
22+
});
23+
24+
it("sets the block number on the state store", async ({ handler, stateStore }) => {
25+
const setBlockNumber = spy(stateStore, "setBlockNumber");
26+
27+
await handler.handle(123);
28+
29+
setBlockNumber.calledOnce();
30+
setBlockNumber.calledWith(123);
31+
});
32+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Identifiers } from "@mainsail/constants";
2+
import { Application } from "@mainsail/kernel";
3+
4+
import { describe } from "@mainsail/test-runner";
5+
import { SetPeerCountHandler } from "./set-peer-count";
6+
7+
describe<{
8+
app: Application;
9+
handler: SetPeerCountHandler;
10+
state: any;
11+
}>("SetPeerCountHandler", ({ assert, beforeEach, it }) => {
12+
beforeEach((context) => {
13+
context.state = { peerCount: 0 };
14+
15+
context.app = new Application();
16+
context.app.bind(Identifiers.Evm.State).toConstantValue(context.state);
17+
18+
context.handler = context.app.resolve(SetPeerCountHandler);
19+
});
20+
21+
it("stores the peer count on the evm state", async ({ handler, state }) => {
22+
await handler.handle(7);
23+
24+
assert.equal(state.peerCount, 7);
25+
});
26+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Identifiers } from "@mainsail/constants";
2+
import { Application } from "@mainsail/kernel";
3+
4+
import { describe } from "@mainsail/test-runner";
5+
import { StartHandler } from "./start";
6+
7+
describe<{
8+
app: Application;
9+
handler: StartHandler;
10+
store: any;
11+
httpServer: any;
12+
httpsServer: any;
13+
enabled: { http: boolean; https: boolean };
14+
}>("StartHandler", ({ beforeEach, it, spy }) => {
15+
beforeEach((context) => {
16+
context.store = { setBlockNumber: () => {} };
17+
context.httpServer = { boot: async () => {} };
18+
context.httpsServer = { boot: async () => {} };
19+
context.enabled = { http: false, https: false };
20+
21+
const configuration = {
22+
getRequired: (key: string) =>
23+
key === "server.http.enabled" ? context.enabled.http : context.enabled.https,
24+
};
25+
26+
// Application binds itself as Application.Instance, so the handler resolves the servers
27+
// off the same container the test binds them into.
28+
context.app = new Application();
29+
context.app.bind(Identifiers.State.Store).toConstantValue(context.store);
30+
context.app.bind(Identifiers.Evm.API.HTTP).toConstantValue(context.httpServer);
31+
context.app.bind(Identifiers.Evm.API.HTTPS).toConstantValue(context.httpsServer);
32+
context.app
33+
.bind(Identifiers.ServiceProvider.Configuration)
34+
.toConstantValue(configuration)
35+
.whenTagged("plugin", "api-evm");
36+
37+
context.handler = context.app.resolve(StartHandler);
38+
});
39+
40+
it("sets the block number", async ({ handler, store }) => {
41+
const setBlockNumber = spy(store, "setBlockNumber");
42+
43+
await handler.handle(42);
44+
45+
setBlockNumber.calledOnce();
46+
setBlockNumber.calledWith(42);
47+
});
48+
49+
it("does not boot any server when neither http nor https is enabled", async ({
50+
handler,
51+
httpServer,
52+
httpsServer,
53+
}) => {
54+
const http = spy(httpServer, "boot");
55+
const https = spy(httpsServer, "boot");
56+
57+
await handler.handle(42);
58+
59+
http.neverCalled();
60+
https.neverCalled();
61+
});
62+
63+
it("boots only the http server when http is enabled", async ({ handler, enabled, httpServer, httpsServer }) => {
64+
enabled.http = true;
65+
const http = spy(httpServer, "boot");
66+
const https = spy(httpsServer, "boot");
67+
68+
await handler.handle(42);
69+
70+
http.calledOnce();
71+
https.neverCalled();
72+
});
73+
74+
it("boots only the https server when https is enabled", async ({ handler, enabled, httpServer, httpsServer }) => {
75+
enabled.https = true;
76+
const http = spy(httpServer, "boot");
77+
const https = spy(httpsServer, "boot");
78+
79+
await handler.handle(42);
80+
81+
http.neverCalled();
82+
https.calledOnce();
83+
});
84+
85+
it("boots both servers when http and https are enabled", async ({ handler, enabled, httpServer, httpsServer }) => {
86+
enabled.http = true;
87+
enabled.https = true;
88+
const http = spy(httpServer, "boot");
89+
const https = spy(httpsServer, "boot");
90+
91+
await handler.handle(42);
92+
93+
http.calledOnce();
94+
https.calledOnce();
95+
});
96+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Identifiers } from "@mainsail/constants";
2+
import { Application, Ipc } from "@mainsail/kernel";
3+
import { EventEmitter } from "events";
4+
import esmock from "esmock";
5+
import { PassThrough } from "stream";
6+
7+
import { describe } from "@mainsail/test-runner";
8+
9+
// Records every `new Worker(...)` so the factory test can assert how the thread is spawned.
10+
const constructions: any[][] = [];
11+
12+
// Stand-in for worker_threads.Worker: an EventEmitter exposing the stdout/stderr streams and
13+
// threadId that Ipc.Subprocess reads, so the real Subprocess wraps it without a real thread.
14+
class FakeWorker extends EventEmitter {
15+
public threadId = 1;
16+
public readonly stdout = new PassThrough();
17+
public readonly stderr = new PassThrough();
18+
19+
public constructor(...arguments_: any[]) {
20+
super();
21+
constructions.push(arguments_);
22+
}
23+
24+
public postMessage(): void {}
25+
public async terminate(): Promise<number> {
26+
return 0;
27+
}
28+
}
29+
30+
// Load the provider with worker_threads.Worker swapped for the fake; the real Ipc.Subprocess
31+
// and ./worker.js stay in place.
32+
const { ServiceProvider } = await esmock("./service-provider", {
33+
worker_threads: { Worker: FakeWorker },
34+
});
35+
36+
describe<{
37+
app: Application;
38+
serviceProvider: any;
39+
worker: any;
40+
}>("ServiceProvider", ({ assert, beforeEach, it, spy, stub }) => {
41+
beforeEach((context) => {
42+
constructions.length = 0;
43+
context.worker = { boot: async () => {}, dispose: async () => {} };
44+
45+
context.app = new Application();
46+
context.app.bind(Identifiers.Config.Flags).toConstantValue({ network: "testnet" });
47+
// Ipc.Subprocess resolves the logger from the container when the factory runs.
48+
context.app.bind(Identifiers.Services.Log.Service).toConstantValue({ debug: () => {}, error: () => {} });
49+
50+
context.serviceProvider = context.app.resolve(ServiceProvider);
51+
52+
// register() resolves the WorkerInstance, whose @postConstruct would invoke the factory
53+
// and spawn. Intercept that resolution so only the explicit factory call below runs it.
54+
stub(context.app, "resolve").returnValue(context.worker);
55+
});
56+
57+
it("register binds the worker subprocess factory and the worker", async (context) => {
58+
assert.false(context.app.isBound(Identifiers.Evm.WorkerSubprocess.Factory));
59+
assert.false(context.app.isBound(Identifiers.Evm.Worker));
60+
61+
await context.serviceProvider.register();
62+
63+
assert.true(context.app.isBound(Identifiers.Evm.WorkerSubprocess.Factory));
64+
assert.true(context.app.isBound(Identifiers.Evm.Worker));
65+
assert.function(context.app.get(Identifiers.Evm.WorkerSubprocess.Factory));
66+
assert.equal(context.app.get(Identifiers.Evm.Worker), context.worker);
67+
});
68+
69+
it("the subprocess factory spawns the worker script with piped stdio and wraps it in an Ipc.Subprocess", async (context) => {
70+
await context.serviceProvider.register();
71+
72+
const factory = context.app.get(Identifiers.Evm.WorkerSubprocess.Factory) as () => Ipc.Subprocess;
73+
const subprocess = factory();
74+
75+
assert.length(constructions, 1);
76+
const [scriptPath, options] = constructions[0];
77+
assert.true(scriptPath.endsWith("worker-script.js"));
78+
assert.equal(options, { stderr: true, stdout: true });
79+
assert.instance(subprocess, Ipc.Subprocess);
80+
});
81+
82+
it("boot delegates to the worker with the flags and the thread name", async (context) => {
83+
await context.serviceProvider.register();
84+
const boot = spy(context.worker, "boot");
85+
86+
await context.serviceProvider.boot();
87+
88+
boot.calledOnce();
89+
boot.calledWith({ network: "testnet", thread: "evm-api" });
90+
});
91+
92+
it("dispose delegates to the worker", async (context) => {
93+
await context.serviceProvider.register();
94+
const dispose = spy(context.worker, "dispose");
95+
96+
await context.serviceProvider.dispose();
97+
98+
dispose.calledOnce();
99+
});
100+
101+
it("is required", async (context) => {
102+
assert.true(await context.serviceProvider.required());
103+
});
104+
});

packages/evm-api-worker/source/service-provider.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { Contracts } from "@mainsail/contracts";
33
import { Identifiers } from "@mainsail/constants";
44
import { inject, injectable } from "@mainsail/container";
55
import { Ipc, Providers } from "@mainsail/kernel";
6-
import Joi from "joi";
76
import { fileURLToPath } from "url";
87
import { Worker } from "worker_threads";
98

@@ -40,8 +39,4 @@ export class ServiceProvider extends Providers.ServiceProvider {
4039
public async required(): Promise<boolean> {
4140
return true;
4241
}
43-
44-
public configSchema(): Joi.AnySchema {
45-
return Joi.object({}).required().unknown(true);
46-
}
4742
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Application } from "@mainsail/kernel";
2+
3+
import { describe } from "@mainsail/test-runner";
4+
import { CommitHandler, SetPeerCountHandler, StartHandler } from "./handlers/index.js";
5+
import { WorkerScriptHandler } from "./worker-handler";
6+
7+
describe<{
8+
subject: WorkerScriptHandler;
9+
handler: any;
10+
resolve: any;
11+
}>("WorkerScriptHandler", ({ beforeEach, it, spy, stub }) => {
12+
beforeEach((context) => {
13+
// WorkerScriptHandler owns a private `new Application()`; stub the prototype so the
14+
// handler resolutions and lifecycle calls stay in-process.
15+
context.handler = { handle: async () => {} };
16+
context.resolve = stub(Application.prototype, "resolve").returnValue(context.handler);
17+
18+
context.subject = new WorkerScriptHandler();
19+
});
20+
21+
it("boot bootstraps the app with the flags and boots it", async ({ subject }) => {
22+
const bootstrap = stub(Application.prototype, "bootstrap").resolvedValue(undefined);
23+
const boot = stub(Application.prototype, "boot").resolvedValue(undefined);
24+
const flags = { network: "testnet" } as any;
25+
26+
await subject.boot(flags);
27+
28+
bootstrap.calledWith({ flags });
29+
boot.calledOnce();
30+
});
31+
32+
it("dispose terminates the app", async ({ subject }) => {
33+
const terminate = stub(Application.prototype, "terminate").resolvedValue(undefined);
34+
35+
await subject.dispose();
36+
37+
terminate.calledOnce();
38+
});
39+
40+
it("start resolves the StartHandler and forwards the height", async ({ subject, handler, resolve }) => {
41+
const handle = spy(handler, "handle");
42+
43+
await subject.start(42);
44+
45+
resolve.calledWith(StartHandler);
46+
handle.calledWith(42);
47+
});
48+
49+
it("setPeerCount resolves the SetPeerCountHandler and forwards the count", async ({
50+
subject,
51+
handler,
52+
resolve,
53+
}) => {
54+
const handle = spy(handler, "handle");
55+
56+
await subject.setPeerCount(5);
57+
58+
resolve.calledWith(SetPeerCountHandler);
59+
handle.calledWith(5);
60+
});
61+
62+
it("commit resolves the CommitHandler and forwards the height", async ({ subject, handler, resolve }) => {
63+
const handle = spy(handler, "handle");
64+
65+
await subject.commit(99);
66+
67+
resolve.calledWith(CommitHandler);
68+
handle.calledWith(99);
69+
});
70+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { describe } from "@mainsail/test-runner";
2+
3+
// worker-script.ts is the worker thread entrypoint: importing it wires an Ipc.Handler to a
4+
// WorkerScriptHandler. On the main thread parentPort is null, so the handler registers no
5+
// listener — the import should simply complete without throwing.
6+
describe("WorkerScript", ({ assert, it }) => {
7+
it("loads without throwing", async () => {
8+
await assert.resolves(() => import("./worker-script.js"));
9+
});
10+
});

0 commit comments

Comments
 (0)