Skip to content

Commit

Permalink
test: move mocha to vitest for beacon-node change (#6028)
Browse files Browse the repository at this point in the history
* Add jest dependencies

* Convert beacon node unit tests to jest

* Convert all beacon unit tests to vitest

* Update dependencies

* Move all e2e tests to vitest

* Fix http but which was causing abort to not work

* Update the e2e script for the beacon-node

* Fix the e2e tests

* Update yarn dependencies

* Remove .only filter

* Fix lint and type errors

* Made close callbacks async

* Fix the test path

* Fix order of resource cleanup

* Fix the peer manager for agent version

* Fix failing unit test

* Update e2e workflow

* Add code coverage support for vitest

* Match the code coverage configuration to previous nyc config

* Fix the formatting for easy code review

* Add custom error messages to extremely confusing assertions

* Add custom matcher support in the vitest

* Update code with feedback
  • Loading branch information
nazarhussain authored Oct 13, 2023
1 parent ab2dfdd commit c06f4e5
Show file tree
Hide file tree
Showing 169 changed files with 2,966 additions and 2,353 deletions.
6 changes: 1 addition & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,7 @@ jobs:
run: scripts/run_e2e_env.sh start

- name: E2E tests
# E2E tests are sometimes stalling until timeout is reached but we know that
# after 15 minutes those should have passed already if there are no failed test cases.
# In this case, just set the job status to passed as there was likely no actual issue.
# See https://github.com/ChainSafe/lodestar/issues/5913
run: timeout 15m yarn test:e2e || { test $? -eq 124 || exit 1; }
run: yarn test:e2e
env:
GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL!=0 && secrets.GOERLI_RPC_URL || env.GOERLI_RPC_DEFAULT_URL }}

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@
"typescript": "^5.2.2",
"typescript-docs-verifier": "^2.5.0",
"webpack": "^5.88.2",
"wait-port": "^1.1.0"
"wait-port": "^1.1.0",
"vitest": "^0.34.6",
"vitest-when": "^0.2.0",
"@vitest/coverage-v8": "^0.34.6"
},
"resolutions": {
"dns-over-http-resolver": "^2.1.1"
Expand Down
4 changes: 2 additions & 2 deletions packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@
"lint:fix": "yarn run lint --fix",
"pretest": "yarn run check-types",
"test": "yarn test:unit && yarn test:e2e",
"test:unit:minimal": "nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit/**/*.test.ts'",
"test:unit:minimal": "vitest --run --dir test/unit/ --coverage",
"test:unit:mainnet": "LODESTAR_PRESET=mainnet nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit-mainnet/**/*.test.ts'",
"test:unit": "yarn test:unit:minimal && yarn test:unit:mainnet",
"test:e2e": "LODESTAR_PRESET=minimal mocha 'test/e2e/**/*.test.ts'",
"test:e2e": "LODESTAR_PRESET=minimal vitest --run --single-thread --dir test/e2e",
"test:sim": "mocha 'test/sim/**/*.test.ts'",
"test:sim:merge-interop": "mocha 'test/sim/merge-interop.test.ts'",
"test:sim:mergemock": "mocha 'test/sim/mergemock.test.ts'",
Expand Down
8 changes: 7 additions & 1 deletion packages/beacon-node/src/chain/bls/multithread/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import path from "node:path";
import {spawn, Worker} from "@chainsafe/threads";
// `threads` library creates self global variable which breaks `timeout-abort-controller` https://github.com/jacobheun/timeout-abort-controller/issues/9
// Don't add an eslint disable here as a reminder that this has to be fixed eventually
Expand Down Expand Up @@ -28,6 +29,9 @@ import {
jobItemWorkReq,
} from "./jobItem.js";

// Worker constructor consider the path relative to the current working directory
const workerDir = process.env.NODE_ENV === "test" ? "../../../../lib/chain/bls/multithread" : "./";

export type BlsMultiThreadWorkerPoolModules = {
logger: Logger;
metrics: Metrics | null;
Expand Down Expand Up @@ -263,7 +267,9 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier {

for (let i = 0; i < poolSize; i++) {
const workerData: WorkerData = {implementation, workerId: i};
const worker = new Worker("./worker.js", {workerData} as ConstructorParameters<typeof Worker>[1]);
const worker = new Worker(path.join(workerDir, "worker.js"), {
workerData,
} as ConstructorParameters<typeof Worker>[1]);

const workerDescriptor: WorkerDescriptor = {
worker,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
retries: opts?.retryAttempts ?? this.opts?.retryAttempts ?? 1,
retryDelay: opts?.retryDelay ?? this.opts?.retryDelay ?? 0,
shouldRetry: opts?.shouldRetry,
signal: this.opts?.signal,
}
);
return parseRpcResponse(res, payload);
Expand Down Expand Up @@ -279,7 +280,6 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
return bodyJson;
} catch (e) {
this.metrics?.requestErrors.inc({routeId});

if (controller.signal.aborted) {
// controller will abort on both parent signal abort + timeout of this specific request
if (this.opts?.signal?.aborted) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from "node:path";
import worker_threads from "node:worker_threads";
import {PeerScoreStatsDump} from "@chainsafe/libp2p-gossipsub/dist/src/score/peer-score.js";
import {PublishOpts} from "@chainsafe/libp2p-gossipsub/types";
Expand Down Expand Up @@ -28,6 +29,9 @@ import {
} from "./events.js";
import {INetworkCore, MultiaddrStr, NetworkWorkerApi, NetworkWorkerData, PeerIdStr} from "./types.js";

// Worker constructor consider the path relative to the current working directory
const workerDir = process.env.NODE_ENV === "test" ? "../../../lib/network/core/" : "./";

export type WorkerNetworkCoreOpts = NetworkOptions & {
metricsEnabled: boolean;
peerStoreDir?: string;
Expand Down Expand Up @@ -116,7 +120,7 @@ export class WorkerNetworkCore implements INetworkCore {
loggerOpts: modules.logger.toOpts(),
};

const worker = new Worker("./networkCoreWorker.js", {
const worker = new Worker(path.join(workerDir, "networkCoreWorker.js"), {
workerData,
/**
* maxYoungGenerationSizeMb defaults to 152mb through the cli option defaults.
Expand Down
23 changes: 14 additions & 9 deletions packages/beacon-node/src/network/peers/peerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {BitArray} from "@chainsafe/ssz";
import {SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params";
import {BeaconConfig} from "@lodestar/config";
import {allForks, altair, phase0} from "@lodestar/types";
import {withTimeout} from "@lodestar/utils";
import {retry, withTimeout} from "@lodestar/utils";
import {LoggerNode} from "@lodestar/logger/node";
import {GoodByeReasonCode, GOODBYE_KNOWN_CODES, Libp2pEvent} from "../../constants/index.js";
import {IClock} from "../../util/clock.js";
Expand Down Expand Up @@ -610,14 +610,19 @@ export class PeerManager {
// AgentVersion was set in libp2p IdentifyService, 'peer:connect' event handler
// since it's not possible to handle it async, we have to wait for a while to set AgentVersion
// See https://github.com/libp2p/js-libp2p/pull/1168
setTimeout(async () => {
const agentVersionBytes = (await this.libp2p.peerStore.get(peerData.peerId)).metadata.get("AgentVersion");
if (agentVersionBytes) {
const agentVersion = new TextDecoder().decode(agentVersionBytes) || "N/A";
peerData.agentVersion = agentVersion;
peerData.agentClient = clientFromAgentVersion(agentVersion);
}
}, 1000);
retry(
async () => {
const agentVersionBytes = (await this.libp2p.peerStore.get(peerData.peerId)).metadata.get("AgentVersion");
if (agentVersionBytes) {
const agentVersion = new TextDecoder().decode(agentVersionBytes) || "N/A";
peerData.agentVersion = agentVersion;
peerData.agentClient = clientFromAgentVersion(agentVersion);
}
},
{retries: 3, retryDelay: 1000}
).catch((err) => {
this.logger.error("Error setting agentVersion for the peer", {peerId: peerData.peerId.toString()}, err);
});
};

/**
Expand Down
44 changes: 44 additions & 0 deletions packages/beacon-node/test/__mocks__/apiMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {config} from "@lodestar/config/default";
import {ChainForkConfig} from "@lodestar/config";
import {getBeaconBlockApi} from "../../src/api/impl/beacon/blocks/index.js";
import {getMockedBeaconChain, MockedBeaconChain} from "./mockedBeaconChain.js";
import {MockedBeaconSync, getMockedBeaconSync} from "./beaconSyncMock.js";
import {MockedBeaconDb, getMockedBeaconDb} from "./mockedBeaconDb.js";
import {MockedNetwork, getMockedNetwork} from "./mockedNetwork.js";

export type ApiImplTestModules = {
forkChoiceStub: MockedBeaconChain["forkChoice"];
chainStub: MockedBeaconChain;
syncStub: MockedBeaconSync;
dbStub: MockedBeaconDb;
networkStub: MockedNetwork;
blockApi: ReturnType<typeof getBeaconBlockApi>;
config: ChainForkConfig;
};

export function setupApiImplTestServer(): ApiImplTestModules {
const chainStub = getMockedBeaconChain();
const forkChoiceStub = chainStub.forkChoice;
const syncStub = getMockedBeaconSync();
const dbStub = getMockedBeaconDb();
const networkStub = getMockedNetwork();

const blockApi = getBeaconBlockApi({
chain: chainStub,
config,
db: dbStub,
network: networkStub,
metrics: null,
});

return {
forkChoiceStub,
chainStub,
syncStub,
dbStub,
networkStub,
blockApi,
config,
};
}
27 changes: 27 additions & 0 deletions packages/beacon-node/test/__mocks__/beaconSyncMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {MockedObject, vi} from "vitest";
import {BeaconSync} from "../../src/sync/index.js";

export type MockedBeaconSync = MockedObject<BeaconSync>;

vi.mock("../../src/sync/index.js", async (requireActual) => {
const mod = await requireActual<typeof import("../../src/sync/index.js")>();

const BeaconSync = vi.fn().mockImplementation(() => {
const sync = {};
Object.defineProperty(sync, "state", {value: undefined, configurable: true});

return sync;
});

return {
...mod,
BeaconSync,
};
});

export function getMockedBeaconSync(): MockedBeaconSync {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return vi.mocked(new BeaconSync({})) as MockedBeaconSync;
}
14 changes: 14 additions & 0 deletions packages/beacon-node/test/__mocks__/loggerMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {vi, MockedObject} from "vitest";
import {Logger} from "@lodestar/logger";

export type MockedLogger = MockedObject<Logger>;

export function getMockedLogger(): MockedLogger {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
verbose: vi.fn(),
};
}
114 changes: 114 additions & 0 deletions packages/beacon-node/test/__mocks__/mockedBeaconChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {vi, MockedObject, Mock} from "vitest";
import {ForkChoice} from "@lodestar/fork-choice";
import {config as defaultConfig} from "@lodestar/config/default";
import {ChainForkConfig} from "@lodestar/config";
import {BeaconChain} from "../../src/chain/index.js";
import {ExecutionEngineHttp} from "../../src/execution/engine/http.js";
import {Eth1ForBlockProduction} from "../../src/eth1/index.js";
import {OpPool} from "../../src/chain/opPools/opPool.js";
import {AggregatedAttestationPool} from "../../src/chain/opPools/aggregatedAttestationPool.js";
import {BeaconProposerCache} from "../../src/chain/beaconProposerCache.js";
import {QueuedStateRegenerator} from "../../src/chain/regen/index.js";
import {LightClientServer} from "../../src/chain/lightClient/index.js";
import {Clock} from "../../src/util/clock.js";
import {getMockedLogger} from "./loggerMock.js";

export type MockedBeaconChain = MockedObject<BeaconChain> & {
getHeadState: Mock<[]>;
forkChoice: MockedObject<ForkChoice>;
executionEngine: MockedObject<ExecutionEngineHttp>;
eth1: MockedObject<Eth1ForBlockProduction>;
opPool: MockedObject<OpPool>;
aggregatedAttestationPool: MockedObject<AggregatedAttestationPool>;
beaconProposerCache: MockedObject<BeaconProposerCache>;
regen: MockedObject<QueuedStateRegenerator>;
bls: {
verifySignatureSets: Mock<[boolean]>;
verifySignatureSetsSameMessage: Mock<[boolean]>;
close: Mock;
canAcceptWork: Mock<[boolean]>;
};
lightClientServer: MockedObject<LightClientServer>;
};
vi.mock("@lodestar/fork-choice");
vi.mock("../../src/execution/engine/http.js");
vi.mock("../../src/eth1/index.js");
vi.mock("../../src/chain/opPools/opPool.js");
vi.mock("../../src/chain/opPools/aggregatedAttestationPool.js");
vi.mock("../../src/chain/beaconProposerCache.js");
vi.mock("../../src/chain/regen/index.js");
vi.mock("../../src/chain/lightClient/index.js");
vi.mock("../../src/chain/index.js", async (requireActual) => {
const mod = await requireActual<typeof import("../../src/chain/index.js")>();

const BeaconChain = vi.fn().mockImplementation(({clock, genesisTime, config}: MockedBeaconChainOptions) => {
return {
config,
opts: {},
genesisTime,
clock:
clock === "real"
? new Clock({config, genesisTime: 0, signal: new AbortController().signal})
: {
currentSlot: undefined,
currentSlotWithGossipDisparity: undefined,
isCurrentSlotGivenGossipDisparity: vi.fn(),
},
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
forkChoice: new ForkChoice(),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
executionEngine: new ExecutionEngineHttp(),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
eth1: new Eth1ForBlockProduction(),
opPool: new OpPool(),
aggregatedAttestationPool: new AggregatedAttestationPool(),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
beaconProposerCache: new BeaconProposerCache(),
produceBlock: vi.fn(),
getCanonicalBlockAtSlot: vi.fn(),
recomputeForkChoiceHead: vi.fn(),
getHeadStateAtCurrentEpoch: vi.fn(),
getHeadState: vi.fn(),
updateBuilderStatus: vi.fn(),
processBlock: vi.fn(),
close: vi.fn(),
logger: getMockedLogger(),
regen: new QueuedStateRegenerator({} as any),
lightClientServer: new LightClientServer({} as any, {} as any),
bls: {
verifySignatureSets: vi.fn().mockResolvedValue(true),
verifySignatureSetsSameMessage: vi.fn().mockResolvedValue([true]),
close: vi.fn().mockResolvedValue(true),
canAcceptWork: vi.fn().mockReturnValue(true),
},
emitter: new mod.ChainEventEmitter(),
};
});

return {
...mod,
BeaconChain,
};
});

type MockedBeaconChainOptions = {
clock: "real" | "fake";
genesisTime: number;
config: ChainForkConfig;
};

export function getMockedBeaconChain(opts?: Partial<MockedBeaconChainOptions>): MockedBeaconChain {
const {clock, genesisTime, config} = opts ?? {};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return new BeaconChain({
clock: clock ?? "fake",
genesisTime: genesisTime ?? 0,
config: config ?? defaultConfig,
}) as MockedBeaconChain;
}
Loading

0 comments on commit c06f4e5

Please sign in to comment.