Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/evm-api-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
"dependencies": {
"@mainsail/constants": "workspace:*",
"@mainsail/container": "workspace:*",
"@mainsail/kernel": "workspace:*",
"joi": "18.2.1"
"@mainsail/kernel": "workspace:*"
},
"devDependencies": {
"@mainsail/contracts": "workspace:*",
"@mainsail/test-runner": "workspace:*",
"esmock": "2.7.5",
"uvu": "0.5.6"
},
"engines": {
Expand Down
32 changes: 32 additions & 0 deletions packages/evm-api-worker/source/handlers/commit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { CommitHandler } from "./commit";

describe<{
app: Application;
handler: CommitHandler;
stateStore: any;
logger: any;
}>("CommitHandler", ({ assert, beforeEach, it, spy }) => {
beforeEach((context) => {
context.stateStore = { setBlockNumber: () => {} };
context.logger = { error: () => {} };

context.app = new Application();
context.app.bind(Identifiers.State.Store).toConstantValue(context.stateStore);
context.app.bind(Identifiers.Services.Log.Service).toConstantValue(context.logger);

context.handler = context.app.resolve(CommitHandler);
});

it("sets the block number on the state store", async ({ handler, stateStore }) => {
const setBlockNumber = spy(stateStore, "setBlockNumber");

await handler.handle(123);

setBlockNumber.calledOnce();
setBlockNumber.calledWith(123);
});
});
26 changes: 26 additions & 0 deletions packages/evm-api-worker/source/handlers/set-peer-count.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { SetPeerCountHandler } from "./set-peer-count";

describe<{
app: Application;
handler: SetPeerCountHandler;
state: any;
}>("SetPeerCountHandler", ({ assert, beforeEach, it }) => {
beforeEach((context) => {
context.state = { peerCount: 0 };

context.app = new Application();
context.app.bind(Identifiers.Evm.State).toConstantValue(context.state);

context.handler = context.app.resolve(SetPeerCountHandler);
});

it("stores the peer count on the evm state", async ({ handler, state }) => {
await handler.handle(7);

assert.equal(state.peerCount, 7);
});
});
96 changes: 96 additions & 0 deletions packages/evm-api-worker/source/handlers/start.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { StartHandler } from "./start";

describe<{
app: Application;
handler: StartHandler;
store: any;
httpServer: any;
httpsServer: any;
enabled: { http: boolean; https: boolean };
}>("StartHandler", ({ beforeEach, it, spy }) => {
beforeEach((context) => {
context.store = { setBlockNumber: () => {} };
context.httpServer = { boot: async () => {} };
context.httpsServer = { boot: async () => {} };
context.enabled = { http: false, https: false };

const configuration = {
getRequired: (key: string) =>
key === "server.http.enabled" ? context.enabled.http : context.enabled.https,
};

// Application binds itself as Application.Instance, so the handler resolves the servers
// off the same container the test binds them into.
context.app = new Application();
context.app.bind(Identifiers.State.Store).toConstantValue(context.store);
context.app.bind(Identifiers.Evm.API.HTTP).toConstantValue(context.httpServer);
context.app.bind(Identifiers.Evm.API.HTTPS).toConstantValue(context.httpsServer);
context.app
.bind(Identifiers.ServiceProvider.Configuration)
.toConstantValue(configuration)
.whenTagged("plugin", "api-evm");

context.handler = context.app.resolve(StartHandler);
});

it("sets the block number", async ({ handler, store }) => {
const setBlockNumber = spy(store, "setBlockNumber");

await handler.handle(42);

setBlockNumber.calledOnce();
setBlockNumber.calledWith(42);
});

it("does not boot any server when neither http nor https is enabled", async ({
handler,
httpServer,
httpsServer,
}) => {
const http = spy(httpServer, "boot");
const https = spy(httpsServer, "boot");

await handler.handle(42);

http.neverCalled();
https.neverCalled();
});

it("boots only the http server when http is enabled", async ({ handler, enabled, httpServer, httpsServer }) => {
enabled.http = true;
const http = spy(httpServer, "boot");
const https = spy(httpsServer, "boot");

await handler.handle(42);

http.calledOnce();
https.neverCalled();
});

it("boots only the https server when https is enabled", async ({ handler, enabled, httpServer, httpsServer }) => {
enabled.https = true;
const http = spy(httpServer, "boot");
const https = spy(httpsServer, "boot");

await handler.handle(42);

http.neverCalled();
https.calledOnce();
});

it("boots both servers when http and https are enabled", async ({ handler, enabled, httpServer, httpsServer }) => {
enabled.http = true;
enabled.https = true;
const http = spy(httpServer, "boot");
const https = spy(httpsServer, "boot");

await handler.handle(42);

http.calledOnce();
https.calledOnce();
});
});
105 changes: 105 additions & 0 deletions packages/evm-api-worker/source/service-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Identifiers } from "@mainsail/constants";
import { Application, Ipc } from "@mainsail/kernel";
import { EventEmitter } from "events";
import esmock from "esmock";
import Joi from "joi";
import { PassThrough } from "stream";

import { describe } from "@mainsail/test-runner";

// Records every `new Worker(...)` so the factory test can assert how the thread is spawned.
const constructions: any[][] = [];

// Stand-in for worker_threads.Worker: an EventEmitter exposing the stdout/stderr streams and
// threadId that Ipc.Subprocess reads, so the real Subprocess wraps it without a real thread.
class FakeWorker extends EventEmitter {
public threadId = 1;
public readonly stdout = new PassThrough();
public readonly stderr = new PassThrough();

public constructor(...arguments_: any[]) {
super();
constructions.push(arguments_);
}

public postMessage(): void {}
public async terminate(): Promise<number> {
return 0;
}
}

// Load the provider with worker_threads.Worker swapped for the fake; the real Ipc.Subprocess
// and ./worker.js stay in place.
const { ServiceProvider } = await esmock("./service-provider", {
worker_threads: { Worker: FakeWorker },
});

describe<{
app: Application;
serviceProvider: any;
worker: any;
}>("ServiceProvider", ({ assert, beforeEach, it, spy, stub }) => {
beforeEach((context) => {
constructions.length = 0;
context.worker = { boot: async () => {}, dispose: async () => {} };

context.app = new Application();
context.app.bind(Identifiers.Config.Flags).toConstantValue({ network: "testnet" });
// Ipc.Subprocess resolves the logger from the container when the factory runs.
context.app.bind(Identifiers.Services.Log.Service).toConstantValue({ debug: () => {}, error: () => {} });

context.serviceProvider = context.app.resolve(ServiceProvider);

// register() resolves the WorkerInstance, whose @postConstruct would invoke the factory
// and spawn. Intercept that resolution so only the explicit factory call below runs it.
stub(context.app, "resolve").returnValue(context.worker);
});

it("register binds the worker subprocess factory and the worker", async (context) => {
assert.false(context.app.isBound(Identifiers.Evm.WorkerSubprocess.Factory));
assert.false(context.app.isBound(Identifiers.Evm.Worker));

await context.serviceProvider.register();

assert.true(context.app.isBound(Identifiers.Evm.WorkerSubprocess.Factory));
assert.true(context.app.isBound(Identifiers.Evm.Worker));
assert.function(context.app.get(Identifiers.Evm.WorkerSubprocess.Factory));
assert.equal(context.app.get(Identifiers.Evm.Worker), context.worker);
});

it("the subprocess factory spawns the worker script with piped stdio and wraps it in an Ipc.Subprocess", async (context) => {
await context.serviceProvider.register();

const factory = context.app.get(Identifiers.Evm.WorkerSubprocess.Factory) as () => Ipc.Subprocess;
const subprocess = factory();

assert.length(constructions, 1);
const [scriptPath, options] = constructions[0];
assert.true(scriptPath.endsWith("worker-script.js"));
assert.equal(options, { stderr: true, stdout: true });
assert.instance(subprocess, Ipc.Subprocess);
});

it("boot delegates to the worker with the flags and the thread name", async (context) => {
await context.serviceProvider.register();
const boot = spy(context.worker, "boot");

await context.serviceProvider.boot();

boot.calledOnce();
boot.calledWith({ network: "testnet", thread: "evm-api" });
});

it("dispose delegates to the worker", async (context) => {
await context.serviceProvider.register();
const dispose = spy(context.worker, "dispose");

await context.serviceProvider.dispose();

dispose.calledOnce();
});

it("is required", async (context) => {
assert.true(await context.serviceProvider.required());
});
});
5 changes: 0 additions & 5 deletions packages/evm-api-worker/source/service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { Contracts } from "@mainsail/contracts";
import { Identifiers } from "@mainsail/constants";
import { inject, injectable } from "@mainsail/container";
import { Ipc, Providers } from "@mainsail/kernel";
import Joi from "joi";
import { fileURLToPath } from "url";
import { Worker } from "worker_threads";

Expand Down Expand Up @@ -40,8 +39,4 @@ export class ServiceProvider extends Providers.ServiceProvider {
public async required(): Promise<boolean> {
return true;
}

public configSchema(): Joi.AnySchema {
return Joi.object({}).required().unknown(true);
}
}
70 changes: 70 additions & 0 deletions packages/evm-api-worker/source/worker-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { CommitHandler, SetPeerCountHandler, StartHandler } from "./handlers/index.js";
import { WorkerScriptHandler } from "./worker-handler";

describe<{
subject: WorkerScriptHandler;
handler: any;
resolve: any;
}>("WorkerScriptHandler", ({ beforeEach, it, spy, stub }) => {
beforeEach((context) => {
// WorkerScriptHandler owns a private `new Application()`; stub the prototype so the
// handler resolutions and lifecycle calls stay in-process.
context.handler = { handle: async () => {} };
context.resolve = stub(Application.prototype, "resolve").returnValue(context.handler);

context.subject = new WorkerScriptHandler();
});

it("boot bootstraps the app with the flags and boots it", async ({ subject }) => {
const bootstrap = stub(Application.prototype, "bootstrap").resolvedValue(undefined);
const boot = stub(Application.prototype, "boot").resolvedValue(undefined);
const flags = { network: "testnet" } as any;

await subject.boot(flags);

bootstrap.calledWith({ flags });
boot.calledOnce();
});

it("dispose terminates the app", async ({ subject }) => {
const terminate = stub(Application.prototype, "terminate").resolvedValue(undefined);

await subject.dispose();

terminate.calledOnce();
});

it("start resolves the StartHandler and forwards the height", async ({ subject, handler, resolve }) => {
const handle = spy(handler, "handle");

await subject.start(42);

resolve.calledWith(StartHandler);
handle.calledWith(42);
});

it("setPeerCount resolves the SetPeerCountHandler and forwards the count", async ({
subject,
handler,
resolve,
}) => {
const handle = spy(handler, "handle");

await subject.setPeerCount(5);

resolve.calledWith(SetPeerCountHandler);
handle.calledWith(5);
});

it("commit resolves the CommitHandler and forwards the height", async ({ subject, handler, resolve }) => {
const handle = spy(handler, "handle");

await subject.commit(99);

resolve.calledWith(CommitHandler);
handle.calledWith(99);
});
});
10 changes: 10 additions & 0 deletions packages/evm-api-worker/source/worker-script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe } from "@mainsail/test-runner";

// worker-script.ts is the worker thread entrypoint: importing it wires an Ipc.Handler to a
// WorkerScriptHandler. On the main thread parentPort is null, so the handler registers no
// listener — the import should simply complete without throwing.
describe("WorkerScript", ({ assert, it }) => {
it("loads without throwing", async () => {
await assert.resolves(() => import("./worker-script.js"));
});
});
Loading
Loading