inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
224 lines (199 loc) • 6.13 kB
text/typescript
/* eslint-disable @typescript-eslint/no-empty-function */
import type { MockPortBinding } from "@zwave-js/serial/mock";
import { MockController, MockNode, MockNodeOptions } from "@zwave-js/testing";
import { wait } from "alcalzone-shared/async";
import crypto from "crypto";
import fs from "fs-extra";
import os from "os";
import path from "path";
import {
createDefaultMockControllerBehaviors,
createDefaultMockNodeBehaviors,
} from "../../Utils";
import type { Driver } from "../driver/Driver";
import {
createAndStartDriverWithMockPort,
CreateAndStartDriverWithMockPortResult,
} from "../driver/DriverMock";
import type { ZWaveOptions } from "../driver/ZWaveOptions";
import type { ZWaveNode } from "../node/Node";
function prepareDriver(
cacheDir: string = path.join(__dirname, "cache"),
logToFile: boolean = false,
additionalOptions: Partial<ZWaveOptions> = {},
): Promise<CreateAndStartDriverWithMockPortResult> {
return createAndStartDriverWithMockPort({
...additionalOptions,
portAddress: "/tty/FAKE",
...(logToFile
? {
logConfig: {
filename: path.join(
cacheDir,
"logs",
"zwavejs_%DATE%.log",
),
logToFile: true,
enabled: true,
level: "debug",
},
}
: {}),
securityKeys: {
S0_Legacy: Buffer.from("0102030405060708090a0b0c0d0e0f10", "hex"),
S2_Unauthenticated: Buffer.from(
"11111111111111111111111111111111",
"hex",
),
S2_Authenticated: Buffer.from(
"22222222222222222222222222222222",
"hex",
),
S2_AccessControl: Buffer.from(
"33333333333333333333333333333333",
"hex",
),
},
storage: {
cacheDir: cacheDir,
lockDir: path.join(cacheDir, "locks"),
},
});
}
function prepareMocks(
mockPort: MockPortBinding,
nodeCapabilities?: MockNodeOptions["capabilities"],
): {
mockController: MockController;
mockNode: MockNode;
} {
const mockController = new MockController({
homeId: 0x7e570001,
ownNodeId: 1,
serial: mockPort,
});
const mockNode = new MockNode({
id: 2,
controller: mockController,
capabilities: nodeCapabilities,
});
mockController.addNode(mockNode);
// Apply default behaviors that are required for interacting with the driver correctly
mockController.defineBehavior(...createDefaultMockControllerBehaviors());
mockNode.defineBehavior(...createDefaultMockNodeBehaviors());
return { mockController, mockNode };
}
interface IntegrationTestOptions {
/** Enable debugging for this integration tests. When enabled, a driver logfile will be written and the test directory will not be deleted after each test. Default: false */
debug?: boolean;
/** If given, the files from this directory will be copied into the test cache directory prior to starting the driver. */
provisioningDirectory?: string;
/** Whether the recorded messages and frames should be cleared before executing the test body. Default: true. */
clearMessageStatsBeforeTest?: boolean;
nodeCapabilities?: MockNodeOptions["capabilities"];
customSetup?: (
driver: Driver,
mockController: MockController,
mockNode: MockNode,
) => Promise<void>;
testBody: (
driver: Driver,
node: ZWaveNode,
mockController: MockController,
mockNode: MockNode,
) => Promise<void>;
additionalDriverOptions?: Partial<ZWaveOptions>;
}
export interface IntegrationTestFn {
(name: string, options: IntegrationTestOptions): void;
}
export interface IntegrationTest extends IntegrationTestFn {
/** Only runs the tests inside this `integrationTest` suite for the current file */
only: IntegrationTestFn;
/** Skips running the tests inside this `integrationTest` suite for the current file */
skip: IntegrationTestFn;
}
function suite(options: IntegrationTestOptions) {
const {
nodeCapabilities,
customSetup,
testBody,
debug = false,
provisioningDirectory,
clearMessageStatsBeforeTest = true,
additionalDriverOptions,
} = options;
let driver: Driver;
let node: ZWaveNode;
let mockPort: MockPortBinding;
let continueStartup: () => void;
let mockController: MockController;
let mockNode: MockNode;
const cacheDir = path.join(
os.tmpdir(),
`zjs_test_cache_${crypto.randomBytes(4).toString("hex")}`,
);
beforeEach(async () => {
if (debug) {
console.log(`Running integration test in directory ${cacheDir}`);
}
// Make sure every test is starting fresh
await fs.emptyDir(cacheDir).catch(() => {});
// And potentially provision the cache
if (provisioningDirectory) {
await fs.copy(provisioningDirectory, cacheDir);
}
({ driver, continueStartup, mockPort } = await prepareDriver(
cacheDir,
debug,
additionalDriverOptions,
));
({ mockController, mockNode } = prepareMocks(
mockPort,
nodeCapabilities,
));
if (customSetup) {
await customSetup(driver, mockController, mockNode);
}
return new Promise<void>((resolve) => {
driver.once("driver ready", () => {
// Test code goes here
node = driver.controller.nodes.getOrThrow(mockNode.id);
node.once("ready", () => {
if (clearMessageStatsBeforeTest) {
mockNode.clearReceivedControllerFrames();
mockNode.clearSentControllerFrames();
mockController.clearReceivedHostMessages();
}
process.nextTick(resolve);
});
});
continueStartup();
});
}, 30000);
afterEach(async () => {
await driver.destroy();
if (!debug) await fs.emptyDir(cacheDir).catch(() => {});
});
it("Test body", async () => {
try {
await testBody(driver, node, mockController, mockNode);
} finally {
// Give everything a chance to settle before destroying the driver.
await wait(100);
}
}, 30000);
}
/** Performs an integration test with a real driver using a mock controller and one mock node */
export const integrationTest = ((
name: string,
options: IntegrationTestOptions,
): void => {
describe(name, () => suite(options));
}) as IntegrationTest;
integrationTest.only = (name: string, options: IntegrationTestOptions) => {
describe.only(name, () => suite(options));
};
integrationTest.skip = (name: string, options: IntegrationTestOptions) => {
describe.skip(name, () => suite(options));
};