@iobroker/testing 2.5.3 → 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,8 +4,9 @@
4
4
  PLACEHOLDER for next version:
5
5
  ## __WORK IN PROGRESS__
6
6
  -->
7
- ## 2.5.3 (2022-01-29)
8
- * Fix: Ensure the tests use the `file` DB type
7
+ ## 2.5.4 (2022-02-02)
8
+ * Modifying ioBroker databases now uses the same methods that JS-Controller uses internally. This ensures that the testing is compatible with the `jsonl` database format.
9
+ * Testing adapters with adapter dependencies that try to access the databases during installation now works.
9
10
 
10
11
  ## 2.5.2 (2021-09-18)
11
12
  * Fix: `adminUI.config` is now respected for the config UI check and JSON config is allowed too
@@ -8,6 +8,7 @@ const createMocks_1 = require("./unit/harness/createMocks");
8
8
  const mockDatabase_1 = require("./unit/mocks/mockDatabase");
9
9
  var harness_1 = require("./integration/lib/harness");
10
10
  Object.defineProperty(exports, "IntegrationTestHarness", { enumerable: true, get: function () { return harness_1.TestHarness; } });
11
+ var mockAdapter_1 = require("./unit/mocks/mockAdapter");
11
12
  var mockDatabase_2 = require("./unit/mocks/mockDatabase");
12
13
  Object.defineProperty(exports, "MockDatabase", { enumerable: true, get: function () { return mockDatabase_2.MockDatabase; } });
13
14
  /** Predefined test sets */
@@ -24,7 +24,6 @@ const async_1 = require("alcalzone-shared/async");
24
24
  const os = __importStar(require("os"));
25
25
  const path = __importStar(require("path"));
26
26
  const adapterTools_1 = require("../../lib/adapterTools");
27
- const executeCommand_1 = require("../../lib/executeCommand");
28
27
  const adapterSetup_1 = require("./lib/adapterSetup");
29
28
  const controllerSetup_1 = require("./lib/controllerSetup");
30
29
  const dbConnection_1 = require("./lib/dbConnection");
@@ -33,9 +32,10 @@ function testAdapter(adapterDir, options = {}) {
33
32
  const appName = (0, adapterTools_1.getAppName)(adapterDir);
34
33
  const adapterName = (0, adapterTools_1.getAdapterName)(adapterDir);
35
34
  const testDir = path.join(os.tmpdir(), `test-${appName}.${adapterName}`);
35
+ /** This db connection is only used for the lifetime of a test and then re-created */
36
+ let dbConnection;
36
37
  let harness;
37
- const dbConnection = new dbConnection_1.DBConnection(appName, testDir);
38
- const controllerSetup = new controllerSetup_1.ControllerSetup(adapterDir, testDir, dbConnection);
38
+ const controllerSetup = new controllerSetup_1.ControllerSetup(adapterDir, testDir);
39
39
  console.log();
40
40
  console.log(`Running tests in ${testDir}`);
41
41
  console.log();
@@ -49,46 +49,44 @@ function testAdapter(adapterDir, options = {}) {
49
49
  if (await controllerSetup.isJsControllerRunning()) {
50
50
  throw new Error("JS-Controller is already running! Stop it for the first test run and try again!");
51
51
  }
52
- const adapterSetup = new adapterSetup_1.AdapterSetup(adapterDir, testDir, dbConnection);
52
+ const adapterSetup = new adapterSetup_1.AdapterSetup(adapterDir, testDir);
53
+ // Installation happens in two steps:
54
+ // First we need to set up JS Controller, so the databases etc. can be created
53
55
  // First we need to copy all files and execute an npm install
54
56
  await controllerSetup.prepareTestDir();
55
- await adapterSetup.copyAdapterFilesToTestDir();
56
- // Remember if JS-Controller is installed already. If so, we need to call setup first later
57
- const wasJsControllerInstalled = await controllerSetup.isJsControllerInstalled();
58
- // Call npm install
59
- await (0, executeCommand_1.executeCommand)("npm", ["i", "--production"], {
60
- cwd: testDir,
61
- });
62
- // Prepare/clean the databases and config
63
- if (wasJsControllerInstalled)
64
- await controllerSetup.setupJsController();
65
- await controllerSetup.setupSystemConfig();
66
- await controllerSetup.disableAdminInstances();
67
- await adapterSetup.deleteOldInstances();
57
+ // Only then we can install the adapter, because some (including VIS) try to access
58
+ // the databases if JS Controller is installed
59
+ await adapterSetup.installAdapterInTestDir();
60
+ const dbConnection = new dbConnection_1.DBConnection(appName, testDir);
61
+ await dbConnection.start();
62
+ controllerSetup.setupSystemConfig(dbConnection);
63
+ await controllerSetup.disableAdminInstances(dbConnection);
64
+ await adapterSetup.deleteOldInstances(dbConnection);
68
65
  await adapterSetup.addAdapterInstance();
66
+ await dbConnection.stop();
69
67
  // Create a copy of the databases that we can restore later
70
68
  ({ objects: objectsBackup, states: statesBackup } =
71
- await dbConnection.readDB());
69
+ await dbConnection.backup());
72
70
  });
73
71
  beforeEach(async function () {
74
72
  this.timeout(30000);
73
+ dbConnection = new dbConnection_1.DBConnection(appName, testDir);
75
74
  // Clean up before every single test
76
75
  await Promise.all([
77
76
  controllerSetup.clearDBDir(),
78
77
  controllerSetup.clearLogDir(),
79
- dbConnection.writeDB(objectsBackup, statesBackup),
78
+ dbConnection.restore(objectsBackup, statesBackup),
80
79
  ]);
81
80
  // Create a new test harness
82
- harness = new harness_1.TestHarness(adapterDir, testDir);
81
+ await dbConnection.start();
82
+ harness = new harness_1.TestHarness(adapterDir, testDir, dbConnection);
83
83
  // Enable the adapter and set its loglevel to debug
84
- await harness.changeAdapterConfig(appName, testDir, adapterName, {
84
+ await harness.changeAdapterConfig(adapterName, {
85
85
  common: {
86
86
  enabled: true,
87
87
  loglevel: "debug",
88
88
  },
89
89
  });
90
- // Start the controller instance
91
- await harness.startController();
92
90
  // And enable the sendTo emulation
93
91
  await harness.enableSendTo();
94
92
  });
@@ -2,8 +2,7 @@ import type { DBConnection } from "./dbConnection";
2
2
  export declare class AdapterSetup {
3
3
  private adapterDir;
4
4
  private testDir;
5
- private dbConnection;
6
- constructor(adapterDir: string, testDir: string, dbConnection: DBConnection);
5
+ constructor(adapterDir: string, testDir: string);
7
6
  private testAdapterDir;
8
7
  private adapterName;
9
8
  private adapterFullName;
@@ -14,10 +13,10 @@ export declare class AdapterSetup {
14
13
  */
15
14
  isAdapterInstalled(): Promise<boolean>;
16
15
  /** Copies all adapter files (except a few) to the test directory */
17
- copyAdapterFilesToTestDir(): Promise<void>;
16
+ installAdapterInTestDir(): Promise<void>;
18
17
  /**
19
18
  * Adds an instance for an already installed adapter in the test directory
20
19
  */
21
20
  addAdapterInstance(): Promise<void>;
22
- deleteOldInstances(): Promise<void>;
21
+ deleteOldInstances(dbConnection: DBConnection): Promise<void>;
23
22
  }
@@ -23,6 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.AdapterSetup = void 0;
26
+ /* eslint-disable @typescript-eslint/no-empty-function */
26
27
  // Add debug logging for tests
27
28
  const objects_1 = require("alcalzone-shared/objects");
28
29
  const debug_1 = __importDefault(require("debug"));
@@ -33,10 +34,9 @@ const executeCommand_1 = require("../../../lib/executeCommand");
33
34
  const tools_1 = require("./tools");
34
35
  const debug = (0, debug_1.default)("testing:integration:AdapterSetup");
35
36
  class AdapterSetup {
36
- constructor(adapterDir, testDir, dbConnection) {
37
+ constructor(adapterDir, testDir) {
37
38
  this.adapterDir = adapterDir;
38
39
  this.testDir = testDir;
39
- this.dbConnection = dbConnection;
40
40
  debug("Creating AdapterSetup...");
41
41
  this.adapterName = (0, adapterTools_1.getAdapterName)(this.adapterDir);
42
42
  this.adapterFullName = (0, adapterTools_1.getAdapterFullName)(this.adapterDir);
@@ -57,7 +57,7 @@ class AdapterSetup {
57
57
  return (0, fs_extra_1.pathExists)(this.testAdapterDir);
58
58
  }
59
59
  /** Copies all adapter files (except a few) to the test directory */
60
- async copyAdapterFilesToTestDir() {
60
+ async installAdapterInTestDir() {
61
61
  debug("Copying adapter files to test directory...");
62
62
  // We install the adapter almost like it would be installed in the real world
63
63
  // Therefore pack it into a tarball and put it in the test dir for installation
@@ -87,6 +87,11 @@ class AdapterSetup {
87
87
  debug("Deleting old remains of this adapter");
88
88
  if (await (0, fs_extra_1.pathExists)(this.testAdapterDir))
89
89
  await (0, fs_extra_1.remove)(this.testAdapterDir);
90
+ debug("Installing adapter");
91
+ // Defer to npm to install the controller (if it wasn't already)
92
+ await (0, executeCommand_1.executeCommand)("npm", ["i", "--production"], {
93
+ cwd: this.testDir,
94
+ });
90
95
  debug(" => done!");
91
96
  }
92
97
  /**
@@ -109,9 +114,12 @@ class AdapterSetup {
109
114
  throw new Error(`Adding the adapter instance failed!`);
110
115
  debug(" => done!");
111
116
  }
112
- async deleteOldInstances() {
117
+ async deleteOldInstances(dbConnection) {
113
118
  debug("Removing old adapter instances...");
114
- const { objects, states } = await this.dbConnection.readDB();
119
+ const allKeys = new Set([
120
+ ...(await dbConnection.getObjectIDs()),
121
+ ...(await dbConnection.getStateIDs()),
122
+ ]);
115
123
  const instanceRegex = new RegExp(`^system\\.adapter\\.${this.adapterName}\\.\\d+`);
116
124
  const instanceObjsRegex = new RegExp(`^${this.adapterName}\\.\\d+\.`);
117
125
  const belongsToAdapter = (id) => {
@@ -120,19 +128,12 @@ class AdapterSetup {
120
128
  id === this.adapterName ||
121
129
  id === `${this.adapterName}.admin`);
122
130
  };
123
- if (objects) {
124
- for (const id of Object.keys(objects)) {
125
- if (belongsToAdapter(id))
126
- delete objects[id];
127
- }
128
- }
129
- if (states) {
130
- for (const id of Object.keys(states)) {
131
- if (belongsToAdapter(id))
132
- delete states[id];
133
- }
131
+ const idsToDelete = [...allKeys].filter((id) => belongsToAdapter(id));
132
+ for (const id of idsToDelete) {
133
+ await dbConnection.delObject(id).catch(() => { });
134
+ await dbConnection.delState(id).catch(() => { });
134
135
  }
135
- await this.dbConnection.writeDB(objects, states);
136
+ debug(" => done!");
136
137
  }
137
138
  }
138
139
  exports.AdapterSetup = AdapterSetup;
@@ -2,8 +2,7 @@ import type { DBConnection } from "./dbConnection";
2
2
  export declare class ControllerSetup {
3
3
  private adapterDir;
4
4
  private testDir;
5
- private dbConnection;
6
- constructor(adapterDir: string, testDir: string, dbConnection: DBConnection);
5
+ constructor(adapterDir: string, testDir: string);
7
6
  private appName;
8
7
  private adapterName;
9
8
  private testAdapterDir;
@@ -22,8 +21,6 @@ export declare class ControllerSetup {
22
21
  isJsControllerRunning(): Promise<boolean>;
23
22
  /**
24
23
  * Sets up an existing JS-Controller instance for testing by executing "iobroker setup first"
25
- * @param appName The branded name of "iobroker"
26
- * @param testDir The directory the integration tests are executed in
27
24
  */
28
25
  setupJsController(): Promise<void>;
29
26
  /**
@@ -31,7 +28,7 @@ export declare class ControllerSetup {
31
28
  * @param appName The branded name of "iobroker"
32
29
  * @param testDir The directory the integration tests are executed in
33
30
  */
34
- setupSystemConfig(): Promise<void>;
31
+ setupSystemConfig(dbConnection: DBConnection): void;
35
32
  /**
36
33
  * Clears the log dir for integration tests (and creates it if it doesn't exist)
37
34
  * @param appName The branded name of "iobroker"
@@ -48,5 +45,5 @@ export declare class ControllerSetup {
48
45
  * Disables all admin instances in the objects DB
49
46
  * @param objects The contents of objects.json
50
47
  */
51
- disableAdminInstances(): Promise<void>;
48
+ disableAdminInstances(dbConnection: DBConnection): Promise<void>;
52
49
  }
@@ -23,7 +23,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.ControllerSetup = void 0;
26
- /* eslint-disable @typescript-eslint/no-var-requires */
27
26
  // Add debug logging for tests
28
27
  const debug_1 = __importDefault(require("debug"));
29
28
  const fs_extra_1 = require("fs-extra");
@@ -34,10 +33,9 @@ const executeCommand_1 = require("../../../lib/executeCommand");
34
33
  const tools_1 = require("./tools");
35
34
  const debug = (0, debug_1.default)("testing:integration:ControllerSetup");
36
35
  class ControllerSetup {
37
- constructor(adapterDir, testDir, dbConnection) {
36
+ constructor(adapterDir, testDir) {
38
37
  this.adapterDir = adapterDir;
39
38
  this.testDir = testDir;
40
- this.dbConnection = dbConnection;
41
39
  debug("Creating ControllerSetup...");
42
40
  this.adapterName = (0, adapterTools_1.getAdapterName)(this.adapterDir);
43
41
  this.appName = (0, adapterTools_1.getAppName)(this.adapterDir);
@@ -83,6 +81,16 @@ class ControllerSetup {
83
81
  if (nodeMajorVersion >= 10) {
84
82
  await (0, fs_extra_1.writeFile)(path.join(this.testDir, ".npmrc"), "engine-strict=true", "utf8");
85
83
  }
84
+ // Remember if JS-Controller is installed already. If so, we need to call `setup first` afterwards
85
+ const wasJsControllerInstalled = await this.isJsControllerInstalled();
86
+ // Defer to npm to install the controller (if it wasn't already)
87
+ debug("(Re-)installing JS Controller...");
88
+ await (0, executeCommand_1.executeCommand)("npm", ["i", "--production"], {
89
+ cwd: this.testDir,
90
+ });
91
+ // Prepare/clean the databases and config
92
+ if (wasJsControllerInstalled)
93
+ await this.setupJsController();
86
94
  debug(" => done!");
87
95
  }
88
96
  /**
@@ -152,8 +160,6 @@ class ControllerSetup {
152
160
  // }
153
161
  /**
154
162
  * Sets up an existing JS-Controller instance for testing by executing "iobroker setup first"
155
- * @param appName The branded name of "iobroker"
156
- * @param testDir The directory the integration tests are executed in
157
163
  */
158
164
  async setupJsController() {
159
165
  debug("Initializing JS-Controller installation...");
@@ -175,15 +181,12 @@ class ControllerSetup {
175
181
  * @param appName The branded name of "iobroker"
176
182
  * @param testDir The directory the integration tests are executed in
177
183
  */
178
- async setupSystemConfig() {
179
- debug(`Moving databases to different ports and setting type "file"...`);
180
- const systemFilename = path.join(this.testDataDir, `${this.appName}.json`);
181
- const systemConfig = require(systemFilename);
184
+ setupSystemConfig(dbConnection) {
185
+ debug(`Moving databases to different ports...`);
186
+ const systemConfig = dbConnection.getSystemConfig();
182
187
  systemConfig.objects.port = 19001;
183
- systemConfig.objects.type = "file";
184
188
  systemConfig.states.port = 19000;
185
- systemConfig.states.type = "file";
186
- await (0, fs_extra_1.writeFile)(systemFilename, JSON.stringify(systemConfig, null, 2));
189
+ dbConnection.setSystemConfig(systemConfig);
187
190
  debug(" => done!");
188
191
  }
189
192
  /**
@@ -208,18 +211,17 @@ class ControllerSetup {
208
211
  * Disables all admin instances in the objects DB
209
212
  * @param objects The contents of objects.json
210
213
  */
211
- async disableAdminInstances() {
214
+ async disableAdminInstances(dbConnection) {
212
215
  debug("Disabling admin instances...");
213
- const objects = await this.dbConnection.readObjectsDB();
214
- if (objects) {
215
- for (const id of Object.keys(objects)) {
216
- if (/^system\.adapter\.admin\.\d.+$/.test(id)) {
217
- const obj = objects[id];
218
- if (obj && obj.common)
219
- obj.common.enabled = false;
220
- }
216
+ const instanceObjects = await dbConnection.getObjectViewAsync("system", "instance", {
217
+ startkey: "system.adapter.admin.",
218
+ endkey: "system.adapter.admin.\u9999",
219
+ });
220
+ for (const { id, value: obj } of instanceObjects.rows) {
221
+ if (obj && obj.common) {
222
+ obj.common.enabled = false;
223
+ await dbConnection.setObject(id, obj);
221
224
  }
222
- await this.dbConnection.writeObjectsDB(objects);
223
225
  }
224
226
  debug(" => done!");
225
227
  }
@@ -1,8 +1,14 @@
1
1
  /// <reference types="iobroker" />
2
+ /// <reference types="node" />
3
+ import EventEmitter from "events";
2
4
  export declare type ObjectsDB = Record<string, ioBroker.Object>;
3
5
  export declare type StatesDB = Record<string, ioBroker.State>;
4
- /** The DB connection capsules access to the objects.json and states.json on disk */
5
- export declare class DBConnection {
6
+ export interface DBConnection {
7
+ on(event: "objectChange", handler: ioBroker.ObjectChangeHandler): this;
8
+ on(event: "stateChange", handler: ioBroker.StateChangeHandler): this;
9
+ }
10
+ /** The DB connection capsules access to the states and objects DB */
11
+ export declare class DBConnection extends EventEmitter {
6
12
  private appName;
7
13
  private testDir;
8
14
  /**
@@ -11,25 +17,43 @@ export declare class DBConnection {
11
17
  */
12
18
  constructor(appName: string, testDir: string);
13
19
  private testDataDir;
14
- private objectsPath;
15
- private statesPath;
16
- readObjectsDB(): Promise<Record<string, ioBroker.Object> | undefined>;
17
- writeObjectsDB(objects: ObjectsDB | undefined): Promise<void>;
18
- writeStatesDB(states: StatesDB | undefined): Promise<void>;
19
- readStatesDB(): Promise<any>;
20
- /**
21
- * Creates a backup of the objects and states DB, so it can be restored after each test
22
- * @param appName The branded name of "iobroker"
23
- * @param testDir The directory the integration tests are executed in
24
- */
25
- readDB(): Promise<{
26
- objects: Record<string, ioBroker.Object> | undefined;
27
- states: any;
20
+ private testControllerDir;
21
+ private _objectsServer;
22
+ private _statesServer;
23
+ private _objectsClient;
24
+ /** The underlying objects client instance that can be used to access the objects DB */
25
+ get objectsClient(): any;
26
+ private _statesClient;
27
+ /** The underlying states client instance that can be used to access the states DB */
28
+ get statesClient(): any;
29
+ get objectsType(): "file" | "jsonl";
30
+ get objectsPath(): string;
31
+ get statesType(): "file" | "jsonl";
32
+ get statesPath(): string;
33
+ getSystemConfig(): any;
34
+ backup(): Promise<{
35
+ objects: Buffer;
36
+ states: Buffer;
28
37
  }>;
29
- /**
30
- * Restores a previous backup of the objects and states DB
31
- * @param appName The branded name of "iobroker"
32
- * @param testDir The directory the integration tests are executed in
33
- */
34
- writeDB(objects: any, states: any): Promise<void>;
38
+ restore(objects: Buffer, states: Buffer): Promise<void>;
39
+ setSystemConfig(systemConfig: any): void;
40
+ private _isRunning;
41
+ get isRunning(): boolean;
42
+ start(): Promise<void>;
43
+ stop(): Promise<void>;
44
+ /** Creates the objects DB and sets up listeners for it */
45
+ private createObjectsDB;
46
+ /** Creates the states DB and sets up listeners for it */
47
+ private createStatesDB;
48
+ readonly getObject: ioBroker.Adapter["getForeignObjectAsync"];
49
+ readonly setObject: ioBroker.Adapter["setForeignObjectAsync"];
50
+ readonly delObject: ioBroker.Adapter["delForeignObjectAsync"];
51
+ readonly getState: ioBroker.Adapter["getForeignStateAsync"];
52
+ readonly setState: ioBroker.Adapter["setForeignStateAsync"];
53
+ readonly delState: ioBroker.Adapter["delForeignStateAsync"];
54
+ subscribeMessage(id: string): void;
55
+ pushMessage(instanceId: string, msg: any, callback: (err: Error | null, id: any) => void): void;
56
+ readonly getObjectViewAsync: ioBroker.Adapter["getObjectViewAsync"];
57
+ getStateIDs(pattern?: string): Promise<string[]>;
58
+ getObjectIDs(pattern?: string): Promise<string[]>;
35
59
  }
@@ -18,72 +18,287 @@ var __importStar = (this && this.__importStar) || function (mod) {
18
18
  __setModuleDefault(result, mod);
19
19
  return result;
20
20
  };
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
21
24
  Object.defineProperty(exports, "__esModule", { value: true });
22
25
  exports.DBConnection = void 0;
26
+ const debug_1 = __importDefault(require("debug"));
27
+ const events_1 = __importDefault(require("events"));
23
28
  const fs_extra_1 = require("fs-extra");
24
29
  const path = __importStar(require("path"));
25
30
  const tools_1 = require("./tools");
26
- /** The DB connection capsules access to the objects.json and states.json on disk */
27
- class DBConnection {
31
+ const debug = (0, debug_1.default)("testing:integration:DBConnection");
32
+ /** The logger instance for the objects and states DB */
33
+ const logger = {
34
+ silly: console.log,
35
+ debug: console.log,
36
+ info: console.log,
37
+ warn: console.warn,
38
+ error: console.error,
39
+ };
40
+ /** The DB connection capsules access to the states and objects DB */
41
+ class DBConnection extends events_1.default {
28
42
  /**
29
43
  * @param appName The branded name of "iobroker"
30
44
  * @param testDir The directory the integration tests are executed in
31
45
  */
32
46
  constructor(appName, testDir) {
47
+ super();
33
48
  this.appName = appName;
34
49
  this.testDir = testDir;
50
+ this._isRunning = false;
51
+ this.getObject = async (id) => {
52
+ if (!this._objectsClient) {
53
+ throw new Error("Objects DB is not running");
54
+ }
55
+ return this._objectsClient.getObjectAsync(id);
56
+ };
57
+ this.setObject = async (...args) => {
58
+ if (!this._objectsClient) {
59
+ throw new Error("Objects DB is not running");
60
+ }
61
+ return this._objectsClient.setObjectAsync(...args);
62
+ };
63
+ this.delObject = async (...args) => {
64
+ if (!this._objectsClient) {
65
+ throw new Error("Objects DB is not running");
66
+ }
67
+ return this._objectsClient.delObjectAsync(...args);
68
+ };
69
+ this.getState = async (id) => {
70
+ if (!this._statesClient) {
71
+ throw new Error("States DB is not running");
72
+ }
73
+ return this._statesClient.getStateAsync(id);
74
+ };
75
+ this.setState = (async (...args) => {
76
+ if (!this._statesClient) {
77
+ throw new Error("States DB is not running");
78
+ }
79
+ return this._statesClient.setStateAsync(...args);
80
+ });
81
+ this.delState = async (...args) => {
82
+ if (!this._statesClient) {
83
+ throw new Error("States DB is not running");
84
+ }
85
+ return this._statesClient.delStateAsync(...args);
86
+ };
87
+ this.getObjectViewAsync = async (...args) => {
88
+ if (!this._objectsClient) {
89
+ throw new Error("Objects DB is not running");
90
+ }
91
+ return this._objectsClient.getObjectViewAsync(...args);
92
+ };
93
+ this.testControllerDir = (0, tools_1.getTestControllerDir)(this.appName, testDir);
35
94
  this.testDataDir = (0, tools_1.getTestDataDir)(appName, testDir);
36
- this.objectsPath = path.join(this.testDataDir, "objects.json");
37
- this.statesPath = path.join(this.testDataDir, "states.json");
38
- }
39
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
40
- async readObjectsDB() {
41
- // debug(`reading objects db...`);
42
- // debug(` dataDir: ${dataDir}`);
43
- // debug(` objectsPath: ${objectsPath}`);
44
- if (await (0, fs_extra_1.pathExists)(this.objectsPath)) {
45
- // debug(` exists: true`);
46
- return (0, fs_extra_1.readJSON)(this.objectsPath, { encoding: "utf8" });
47
- }
48
95
  }
49
- async writeObjectsDB(objects) {
50
- if (!objects)
96
+ /** The underlying objects client instance that can be used to access the objects DB */
97
+ get objectsClient() {
98
+ return this._objectsClient;
99
+ }
100
+ /** The underlying states client instance that can be used to access the states DB */
101
+ get statesClient() {
102
+ return this._statesClient;
103
+ }
104
+ get objectsType() {
105
+ return this.getSystemConfig().objects.type;
106
+ }
107
+ get objectsPath() {
108
+ return path.join(this.testDataDir, this.objectsType === "file" ? "objects.json" : "objects.jsonl");
109
+ }
110
+ get statesType() {
111
+ return this.getSystemConfig().states.type;
112
+ }
113
+ get statesPath() {
114
+ return path.join(this.testDataDir, this.statesType === "file" ? "states.json" : "states.jsonl");
115
+ }
116
+ getSystemConfig() {
117
+ const systemFilename = path.join(this.testDataDir, `${this.appName}.json`);
118
+ return (0, fs_extra_1.readJSONSync)(systemFilename);
119
+ }
120
+ async backup() {
121
+ debug("Creating DB backup...");
122
+ const wasRunning = this._isRunning;
123
+ await this.stop();
124
+ const objects = await (0, fs_extra_1.readFile)(this.objectsPath);
125
+ const states = await (0, fs_extra_1.readFile)(this.statesPath);
126
+ if (wasRunning)
127
+ await this.start();
128
+ return { objects, states };
129
+ }
130
+ async restore(objects, states) {
131
+ debug("Restoring DB backup...");
132
+ const wasRunning = this._isRunning;
133
+ await this.stop();
134
+ await (0, fs_extra_1.writeFile)(this.objectsPath, objects);
135
+ await (0, fs_extra_1.writeFile)(this.statesPath, states);
136
+ if (wasRunning)
137
+ await this.start();
138
+ }
139
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
140
+ setSystemConfig(systemConfig) {
141
+ const systemFilename = path.join(this.testDataDir, `${this.appName}.json`);
142
+ (0, fs_extra_1.writeJSONSync)(systemFilename, systemConfig, { spaces: 2 });
143
+ }
144
+ get isRunning() {
145
+ return this._isRunning;
146
+ }
147
+ async start() {
148
+ if (this._isRunning) {
149
+ debug("At least one DB instance is already running, not starting again...");
51
150
  return;
52
- return (0, fs_extra_1.writeJSON)(this.objectsPath, objects);
151
+ }
152
+ debug("starting DB instances...");
153
+ await this.createObjectsDB();
154
+ await this.createStatesDB();
155
+ this._isRunning = true;
156
+ debug("DB instances started");
53
157
  }
54
- async writeStatesDB(states) {
55
- if (!states)
158
+ async stop() {
159
+ var _a, _b, _c, _d;
160
+ if (!this._isRunning) {
161
+ debug("No DB instance is running, nothing to stop...");
56
162
  return;
57
- return (0, fs_extra_1.writeJSON)(this.statesPath, states);
58
- }
59
- async readStatesDB() {
60
- // debug(`reading states db...`);
61
- // debug(` dataDir: ${dataDir}`);
62
- // debug(` statesPath: ${statesPath}`);
63
- if (await (0, fs_extra_1.pathExists)(this.statesPath)) {
64
- // debug(` exists: true`);
65
- return (0, fs_extra_1.readJSON)(this.statesPath, { encoding: "utf8" });
66
163
  }
164
+ debug("Stopping DB instances...");
165
+ // Stop clients before servers
166
+ await ((_a = this._objectsClient) === null || _a === void 0 ? void 0 : _a.destroy());
167
+ await ((_b = this._objectsServer) === null || _b === void 0 ? void 0 : _b.destroy());
168
+ await ((_c = this._statesClient) === null || _c === void 0 ? void 0 : _c.destroy());
169
+ await ((_d = this._statesServer) === null || _d === void 0 ? void 0 : _d.destroy());
170
+ this._objectsClient = null;
171
+ this._objectsServer = null;
172
+ this._statesClient = null;
173
+ this._statesServer = null;
174
+ this._isRunning = false;
175
+ debug("DB instances stopped");
67
176
  }
68
- /**
69
- * Creates a backup of the objects and states DB, so it can be restored after each test
70
- * @param appName The branded name of "iobroker"
71
- * @param testDir The directory the integration tests are executed in
72
- */
73
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
74
- async readDB() {
75
- const objects = await this.readObjectsDB();
76
- const states = await this.readStatesDB();
77
- return { objects, states };
177
+ /** Creates the objects DB and sets up listeners for it */
178
+ async createObjectsDB() {
179
+ debug("creating objects DB");
180
+ const objectsType = this.objectsType;
181
+ debug(` => objects DB type: ${objectsType}`);
182
+ const settings = {
183
+ connection: {
184
+ type: objectsType,
185
+ host: "127.0.0.1",
186
+ port: 19001,
187
+ user: "",
188
+ pass: "",
189
+ noFileCache: false,
190
+ connectTimeout: 2000,
191
+ },
192
+ logger,
193
+ };
194
+ const objectsDbPath = require.resolve(`@iobroker/db-objects-${objectsType}`, {
195
+ paths: [
196
+ path.join(this.testDir, "node_modules"),
197
+ path.join(this.testControllerDir, "node_modules"),
198
+ ],
199
+ });
200
+ debug(` => objects DB lib found at ${objectsDbPath}`);
201
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
202
+ const { Server, Client } = require(objectsDbPath);
203
+ // First create the server
204
+ await new Promise((resolve) => {
205
+ this._objectsServer = new Server({
206
+ ...settings,
207
+ connected: () => {
208
+ resolve();
209
+ },
210
+ });
211
+ });
212
+ // Then the client
213
+ await new Promise((resolve) => {
214
+ this._objectsClient = new Client({
215
+ ...settings,
216
+ connected: () => {
217
+ this._objectsClient.subscribe("*");
218
+ resolve();
219
+ },
220
+ change: this.emit.bind(this, "objectChange"),
221
+ });
222
+ });
223
+ debug(" => done!");
78
224
  }
79
- /**
80
- * Restores a previous backup of the objects and states DB
81
- * @param appName The branded name of "iobroker"
82
- * @param testDir The directory the integration tests are executed in
83
- */
84
- async writeDB(objects, states) {
85
- await this.writeObjectsDB(objects);
86
- await this.writeStatesDB(states);
225
+ /** Creates the states DB and sets up listeners for it */
226
+ async createStatesDB() {
227
+ debug(`creating states DB`);
228
+ const statesType = this.statesType;
229
+ debug(` => states DB type: ${statesType}`);
230
+ const settings = {
231
+ connection: {
232
+ type: statesType,
233
+ host: "127.0.0.1",
234
+ port: 19000,
235
+ options: {
236
+ auth_pass: null,
237
+ retry_max_delay: 15000,
238
+ },
239
+ },
240
+ logger,
241
+ };
242
+ const statesDbPath = require.resolve(`@iobroker/db-states-${statesType}`, {
243
+ paths: [
244
+ path.join(this.testDir, "node_modules"),
245
+ path.join(this.testControllerDir, "node_modules"),
246
+ ],
247
+ });
248
+ debug(` => states DB lib found at ${statesDbPath}`);
249
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
250
+ const { Server, Client } = require(statesDbPath);
251
+ // First create the server
252
+ await new Promise((resolve) => {
253
+ this._statesServer = new Server({
254
+ ...settings,
255
+ connected: () => {
256
+ resolve();
257
+ },
258
+ });
259
+ });
260
+ // Then the client
261
+ await new Promise((resolve) => {
262
+ this._statesClient = new Client({
263
+ ...settings,
264
+ connected: () => {
265
+ this._statesClient.subscribe("*");
266
+ resolve();
267
+ },
268
+ change: this.emit.bind(this, "stateChange"),
269
+ });
270
+ });
271
+ debug(" => done!");
272
+ }
273
+ subscribeMessage(id) {
274
+ if (!this._statesClient) {
275
+ throw new Error("States DB is not running");
276
+ }
277
+ return this._statesClient.subscribeMessage(id);
278
+ }
279
+ pushMessage(instanceId,
280
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
281
+ msg, callback) {
282
+ if (!this._statesClient) {
283
+ throw new Error("States DB is not running");
284
+ }
285
+ this._statesClient.pushMessage(instanceId, msg, callback);
286
+ }
287
+ async getStateIDs(pattern = "*") {
288
+ var _a, _b, _c, _d;
289
+ if (!this._statesClient) {
290
+ throw new Error("States DB is not running");
291
+ }
292
+ return (((_b = (_a = this._statesClient).getKeysAsync) === null || _b === void 0 ? void 0 : _b.call(_a, pattern)) ||
293
+ ((_d = (_c = this._statesClient).getKeys) === null || _d === void 0 ? void 0 : _d.call(_c, pattern)));
294
+ }
295
+ async getObjectIDs(pattern = "*") {
296
+ var _a, _b, _c, _d;
297
+ if (!this._objectsClient) {
298
+ throw new Error("Objects DB is not running");
299
+ }
300
+ return (((_b = (_a = this._objectsClient).getKeysAsync) === null || _b === void 0 ? void 0 : _b.call(_a, pattern)) ||
301
+ ((_d = (_c = this._objectsClient).getKeys) === null || _d === void 0 ? void 0 : _d.call(_c, pattern)));
87
302
  }
88
303
  }
89
304
  exports.DBConnection = DBConnection;
@@ -2,6 +2,7 @@
2
2
  /// <reference types="node" />
3
3
  import { ChildProcess } from "child_process";
4
4
  import { EventEmitter } from "events";
5
+ import type { DBConnection } from "./dbConnection";
5
6
  export interface TestHarness {
6
7
  on(event: "objectChange", handler: ioBroker.ObjectChangeHandler): this;
7
8
  on(event: "stateChange", handler: ioBroker.StateChangeHandler): this;
@@ -14,21 +15,19 @@ export interface TestHarness {
14
15
  export declare class TestHarness extends EventEmitter {
15
16
  private adapterDir;
16
17
  private testDir;
18
+ private dbConnection;
17
19
  /**
18
20
  * @param adapterDir The root directory of the adapter
19
21
  * @param testDir The directory the integration tests are executed in
20
22
  */
21
- constructor(adapterDir: string, testDir: string);
23
+ constructor(adapterDir: string, testDir: string, dbConnection: DBConnection);
22
24
  private adapterName;
23
25
  private appName;
24
26
  private testControllerDir;
25
27
  private testAdapterDir;
26
- private dbConnection;
27
- private _objects;
28
- /** The actual objects DB */
28
+ /** Gives direct access to the Objects DB */
29
29
  get objects(): any;
30
- private _states;
31
- /** The actual states DB */
30
+ /** Gives direct access to the States DB */
32
31
  get states(): any;
33
32
  private _adapterProcess;
34
33
  /** The process the adapter is running in */
@@ -36,10 +35,6 @@ export declare class TestHarness extends EventEmitter {
36
35
  private _adapterExit;
37
36
  /** Contains the adapter exit code or signal if it was terminated unexpectedly */
38
37
  get adapterExit(): number | string | undefined;
39
- /** Creates the objects DB and sets up listeners for it */
40
- private createObjectsDB;
41
- /** Creates the states DB and sets up listeners for it */
42
- private createStatesDB;
43
38
  /** Checks if the controller instance is running */
44
39
  isControllerRunning(): boolean;
45
40
  /** Starts the controller instance by creating the databases */
@@ -65,7 +60,7 @@ export declare class TestHarness extends EventEmitter {
65
60
  /**
66
61
  * Updates the adapter config. The changes can be a subset of the target object
67
62
  */
68
- changeAdapterConfig(appName: string, testDir: string, adapterName: string, changes: any): Promise<void>;
63
+ changeAdapterConfig(adapterName: string, changes: Record<string, any>): Promise<void>;
69
64
  /** Enables the sendTo method */
70
65
  enableSendTo(): Promise<void>;
71
66
  private sendToID;
@@ -23,7 +23,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.TestHarness = void 0;
26
- /* eslint-disable @typescript-eslint/no-var-requires */
27
26
  const async_1 = require("alcalzone-shared/async");
28
27
  const objects_1 = require("alcalzone-shared/objects");
29
28
  const child_process_1 = require("child_process");
@@ -31,18 +30,9 @@ const debug_1 = __importDefault(require("debug"));
31
30
  const events_1 = require("events");
32
31
  const path = __importStar(require("path"));
33
32
  const adapterTools_1 = require("../../../lib/adapterTools");
34
- const dbConnection_1 = require("./dbConnection");
35
33
  const tools_1 = require("./tools");
36
34
  const debug = (0, debug_1.default)("testing:integration:TestHarness");
37
35
  const isWindows = /^win/.test(process.platform);
38
- /** The logger instance for the objects and states DB */
39
- const logger = {
40
- silly: console.log,
41
- debug: console.log,
42
- info: console.log,
43
- warn: console.warn,
44
- error: console.error,
45
- };
46
36
  const fromAdapterID = "system.adapter.test.0";
47
37
  /**
48
38
  * The test harness capsules the execution of the JS-Controller and the adapter instance and monitors their status.
@@ -53,10 +43,11 @@ class TestHarness extends events_1.EventEmitter {
53
43
  * @param adapterDir The root directory of the adapter
54
44
  * @param testDir The directory the integration tests are executed in
55
45
  */
56
- constructor(adapterDir, testDir) {
46
+ constructor(adapterDir, testDir, dbConnection) {
57
47
  super();
58
48
  this.adapterDir = adapterDir;
59
49
  this.testDir = testDir;
50
+ this.dbConnection = dbConnection;
60
51
  this.sendToID = 1;
61
52
  debug("Creating instance");
62
53
  this.adapterName = (0, adapterTools_1.getAdapterName)(this.adapterDir);
@@ -68,15 +59,26 @@ class TestHarness extends events_1.EventEmitter {
68
59
  debug(` adapter: ${this.testAdapterDir}`);
69
60
  debug(` appName: ${this.appName}`);
70
61
  debug(` adapterName: ${this.adapterName}`);
71
- this.dbConnection = new dbConnection_1.DBConnection(this.appName, this.testDir);
62
+ dbConnection.on("objectChange", (id, obj) => {
63
+ this.emit("objectChange", id, obj);
64
+ });
65
+ dbConnection.on("stateChange", (id, state) => {
66
+ this.emit("stateChange", id, state);
67
+ });
72
68
  }
73
- /** The actual objects DB */
69
+ /** Gives direct access to the Objects DB */
74
70
  get objects() {
75
- return this._objects;
71
+ if (!this.dbConnection.objectsClient) {
72
+ throw new Error("Objects DB is not running");
73
+ }
74
+ return this.dbConnection.objectsClient;
76
75
  }
77
- /** The actual states DB */
76
+ /** Gives direct access to the States DB */
78
77
  get states() {
79
- return this._states;
78
+ if (!this.dbConnection.statesClient) {
79
+ throw new Error("States DB is not running");
80
+ }
81
+ return this.dbConnection.statesClient;
80
82
  }
81
83
  /** The process the adapter is running in */
82
84
  get adapterProcess() {
@@ -86,68 +88,15 @@ class TestHarness extends events_1.EventEmitter {
86
88
  get adapterExit() {
87
89
  return this._adapterExit;
88
90
  }
89
- /** Creates the objects DB and sets up listeners for it */
90
- async createObjectsDB() {
91
- debug("creating objects DB");
92
- const Objects = require(path.join(this.testControllerDir, "lib/objects/objectsInMemServer"));
93
- return new Promise((resolve) => {
94
- this._objects = new Objects({
95
- connection: {
96
- type: "file",
97
- host: "127.0.0.1",
98
- port: 19001,
99
- user: "",
100
- pass: "",
101
- noFileCache: false,
102
- connectTimeout: 2000,
103
- },
104
- logger,
105
- connected: () => {
106
- debug(" => done!");
107
- this._objects.subscribe("*");
108
- resolve();
109
- },
110
- change: this.emit.bind(this, "objectChange"),
111
- });
112
- });
113
- }
114
- /** Creates the states DB and sets up listeners for it */
115
- async createStatesDB() {
116
- debug("creating states DB");
117
- const States = require(path.join(this.testControllerDir, "lib/states/statesInMemServer"));
118
- return new Promise((resolve) => {
119
- this._states = new States({
120
- connection: {
121
- type: "file",
122
- host: "127.0.0.1",
123
- port: 19000,
124
- options: {
125
- auth_pass: null,
126
- retry_max_delay: 15000,
127
- },
128
- },
129
- logger,
130
- connected: () => {
131
- debug(" => done!");
132
- this._states.subscribe("*");
133
- resolve();
134
- },
135
- change: this.emit.bind(this, "stateChange"),
136
- });
137
- });
138
- }
139
91
  /** Checks if the controller instance is running */
140
92
  isControllerRunning() {
141
- return !!this._objects || !!this._states;
93
+ // The "controller instance" is just the databases, so if they are running,
94
+ // the "controller" is.
95
+ return this.dbConnection.isRunning;
142
96
  }
143
97
  /** Starts the controller instance by creating the databases */
144
98
  async startController() {
145
- debug("starting controller instance...");
146
- if (this.isControllerRunning())
147
- throw new Error("The Controller is already running!");
148
- await this.createObjectsDB();
149
- await this.createStatesDB();
150
- debug("controller instance created");
99
+ await this.dbConnection.start();
151
100
  }
152
101
  /** Stops the controller instance (and the adapter if it is running) */
153
102
  async stopController() {
@@ -158,7 +107,7 @@ class TestHarness extends events_1.EventEmitter {
158
107
  // Give the adapter time to stop (as long as configured in the io-package.json)
159
108
  let stopTimeout;
160
109
  try {
161
- stopTimeout = (await this._objects.getObjectAsync(`system.adapter.${this.adapterName}.0`)).common.stopTimeout;
110
+ stopTimeout = (await this.dbConnection.getObject(`system.adapter.${this.adapterName}.0`)).common.stopTimeout;
162
111
  stopTimeout += 1000;
163
112
  }
164
113
  catch { }
@@ -176,16 +125,7 @@ class TestHarness extends events_1.EventEmitter {
176
125
  else {
177
126
  debug("Adapter failed to start - no need to terminate!");
178
127
  }
179
- debug("Stopping controller instance...");
180
- if (this._objects) {
181
- await this._objects.destroy();
182
- this._objects = null;
183
- }
184
- if (this._states) {
185
- await this._states.destroy();
186
- this._states = null;
187
- }
188
- debug("Controller instance stopped");
128
+ await this.dbConnection.stop();
189
129
  }
190
130
  /**
191
131
  * Starts the adapter in a separate process and monitors its status
@@ -259,13 +199,14 @@ class TestHarness extends events_1.EventEmitter {
259
199
  .on("close", onClose)
260
200
  .on("exit", onClose);
261
201
  // Tell adapter to stop
262
- if (this._states) {
263
- await this._states.setStateAsync(`system.adapter.${this.adapterName}.0.sigKill`, {
202
+ try {
203
+ await this.dbConnection.setState(`system.adapter.${this.adapterName}.0.sigKill`, {
264
204
  val: -1,
265
205
  from: "system.host.testing",
266
206
  });
267
207
  }
268
- else {
208
+ catch {
209
+ // DB connection may be closed already, kill the process
269
210
  (_a = this._adapterProcess) === null || _a === void 0 ? void 0 : _a.kill("SIGTERM");
270
211
  }
271
212
  });
@@ -273,29 +214,27 @@ class TestHarness extends events_1.EventEmitter {
273
214
  /**
274
215
  * Updates the adapter config. The changes can be a subset of the target object
275
216
  */
276
- async changeAdapterConfig(appName, testDir, adapterName, changes) {
277
- const objects = await this.dbConnection.readObjectsDB();
217
+ async changeAdapterConfig(adapterName, changes) {
278
218
  const adapterInstanceId = `system.adapter.${adapterName}.0`;
279
- if (objects && adapterInstanceId in objects) {
280
- const target = objects[adapterInstanceId];
281
- (0, objects_1.extend)(target, changes);
282
- await this.dbConnection.writeObjectsDB(objects);
219
+ const obj = await this.dbConnection.getObject(adapterInstanceId);
220
+ if (obj) {
221
+ (0, objects_1.extend)(obj, changes);
222
+ await this.dbConnection.setObject(adapterInstanceId, obj);
283
223
  }
284
224
  }
285
225
  /** Enables the sendTo method */
286
- enableSendTo() {
287
- return new Promise((resolve) => {
288
- this._objects.extendObject(fromAdapterID, {
289
- common: {},
290
- type: "instance",
291
- }, () => {
292
- this._states.subscribeMessage(fromAdapterID);
293
- resolve();
294
- });
226
+ async enableSendTo() {
227
+ await this.dbConnection.setObject(fromAdapterID, {
228
+ type: "instance",
229
+ common: {},
230
+ native: {},
295
231
  });
232
+ this.dbConnection.subscribeMessage(fromAdapterID);
296
233
  }
297
234
  /** Sends a message to an adapter instance */
298
- sendTo(target, command, message, callback) {
235
+ sendTo(target, command,
236
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
237
+ message, callback) {
299
238
  const stateChangedHandler = (id, state) => {
300
239
  if (id === `messagebox.${fromAdapterID}`) {
301
240
  callback(state.message);
@@ -303,7 +242,7 @@ class TestHarness extends events_1.EventEmitter {
303
242
  }
304
243
  };
305
244
  this.addListener("stateChange", stateChangedHandler);
306
- this._states.pushMessage(`system.adapter.${target}`, {
245
+ this.dbConnection.pushMessage(`system.adapter.${target}`, {
307
246
  command: command,
308
247
  message: message,
309
248
  from: fromAdapterID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iobroker/testing",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "Shared utilities for adapter and module testing in ioBroker",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -42,7 +42,7 @@
42
42
  "@alcalzone/release-script-plugin-license": "^3.5.0",
43
43
  "@iobroker/adapter-core": "^2.5.1",
44
44
  "@tsconfig/node12": "^1.0.9",
45
- "@types/chai": "^4.2.22",
45
+ "@types/chai": "^4.3.0",
46
46
  "@types/chai-as-promised": "^7.1.4",
47
47
  "@types/debug": "4.1.7",
48
48
  "@types/fs-extra": "^9.0.13",
@@ -56,11 +56,11 @@
56
56
  "eslint": "^7.32.0",
57
57
  "eslint-config-prettier": "^8.3.0",
58
58
  "eslint-plugin-prettier": "^4.0.0",
59
- "prettier": "^2.4.1",
59
+ "prettier": "^2.5.1",
60
60
  "rimraf": "^3.0.2",
61
61
  "source-map-support": "^0.5.21",
62
62
  "ts-node": "^10.2.1",
63
- "typescript": "~4.4.4"
63
+ "typescript": "~4.5.5"
64
64
  },
65
65
  "dependencies": {
66
66
  "alcalzone-shared": "~4.0.1",