@iobroker/testing 6.0.0 → 6.1.0

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/README.md CHANGED
@@ -75,6 +75,20 @@ tests.integration(path.join(__dirname, ".."), {
75
75
  });
76
76
  });
77
77
 
78
+ // Checking the adapter log
79
+ suite("Check the log", (getHarness) => {
80
+ let harness;
81
+ before(() => {
82
+ harness = getHarness();
83
+ });
84
+
85
+ it("Should not log the follow-up error", async () => {
86
+ await harness.startAdapterAndWait();
87
+
88
+ expect(harness.hasLog(/follow-up-bug detected/, "error")).to.be.false;
89
+ });
90
+ });
91
+
78
92
  // While developing the tests, you can run only a single suite using `suite.only`...
79
93
  suite.only("Only this will run", (getHarness) => {
80
94
  // ...
@@ -87,6 +101,43 @@ tests.integration(path.join(__dirname, ".."), {
87
101
  });
88
102
  ```
89
103
 
104
+ ### Checking the adapter log (Integration test)
105
+
106
+ The test harness captures everything the adapter under test prints while it is running, so tests can check
107
+ that a specific message was (or was not) logged:
108
+
109
+ - `getLogs(level?)` returns the captured log messages, optionally filtered by log level. Each entry looks like this:
110
+
111
+ ```ts
112
+ interface AdapterLog {
113
+ /** The log level the message was logged with */
114
+ level: "silly" | "debug" | "info" | "warn" | "error";
115
+ /** The time the message was logged at */
116
+ timestamp: Date;
117
+ /** The source of the message, e.g. `my-adapter.0`. Undefined if it could not be determined */
118
+ from: string | undefined;
119
+ /** The logged message without timestamp, level and source */
120
+ message: string;
121
+ /** The unparsed log line as it was printed by the adapter */
122
+ raw: string;
123
+ }
124
+ ```
125
+
126
+ - `hasLog(pattern, level?)` returns whether a message matching `pattern` was logged. `pattern` is either a `RegExp`
127
+ or a string that must be contained in the message. If `level` is given, only messages with that log level are checked.
128
+ - `clearLogs()` forgets all messages that were captured so far, e.g. to check only the messages of the next step.
129
+
130
+ ```ts
131
+ await harness.startAdapterAndWait();
132
+
133
+ expect(harness.hasLog(/connection failed/, "error")).to.be.false;
134
+ expect(harness.getLogs("error")).to.be.empty;
135
+
136
+ harness.clearLogs();
137
+ ```
138
+
139
+ Lines that are not in the ioBroker log format - e.g. output of a plain `console.log()` - are captured as `info` messages.
140
+
90
141
  ### Adapter startup (Unit test)
91
142
 
92
143
  **Unit tests for adapter startup were removed and are essentially a no-op now.**
@@ -3,7 +3,7 @@ import { validatePackageFiles } from './packageFiles';
3
3
  import { testAdapterWithMocks } from './unit';
4
4
  import { createMocks } from './unit/harness/createMocks';
5
5
  import { createAsserts } from './unit/mocks/mockDatabase';
6
- export { TestHarness as IntegrationTestHarness } from './integration/lib/harness';
6
+ export { TestHarness as IntegrationTestHarness, type AdapterLog } from './integration/lib/harness';
7
7
  export type { MockAdapter } from './unit/mocks/mockAdapter';
8
8
  export { MockDatabase } from './unit/mocks/mockDatabase';
9
9
  /** Predefined test sets */
@@ -8,6 +8,24 @@ export declare class ControllerSetup {
8
8
  private testAdapterDir;
9
9
  private testControllerDir;
10
10
  private testDataDir;
11
+ /**
12
+ * Returns the path of the file that stores which JS-Controller version is installed in the test directory
13
+ */
14
+ private getControllerVersionFilePath;
15
+ /**
16
+ * Reads which JS-Controller version was installed in the test directory during the previous run.
17
+ * Returns `null` if this is unknown.
18
+ */
19
+ private getInstalledControllerVersion;
20
+ /**
21
+ * Remembers which JS-Controller version is installed in the test directory
22
+ */
23
+ private saveInstalledControllerVersion;
24
+ /**
25
+ * Removes the installed dependencies and the data directory from the test directory.
26
+ * This is necessary when switching JS-Controller versions, so no stale files and states are left behind.
27
+ */
28
+ private clearTestDir;
11
29
  prepareTestDir(controllerVersion?: string): Promise<void>;
12
30
  /**
13
31
  * Tests if JS-Controller is already installed
@@ -70,6 +70,52 @@ class ControllerSetup {
70
70
  testAdapterDir;
71
71
  testControllerDir;
72
72
  testDataDir;
73
+ /**
74
+ * Returns the path of the file that stores which JS-Controller version is installed in the test directory
75
+ */
76
+ getControllerVersionFilePath() {
77
+ return path.join(this.testDir, '.controller-version');
78
+ }
79
+ /**
80
+ * Reads which JS-Controller version was installed in the test directory during the previous run.
81
+ * Returns `null` if this is unknown.
82
+ */
83
+ async getInstalledControllerVersion() {
84
+ const versionFilePath = this.getControllerVersionFilePath();
85
+ try {
86
+ if (!(await (0, fs_extra_1.pathExists)(versionFilePath))) {
87
+ return null;
88
+ }
89
+ const version = await (0, fs_extra_1.readFile)(versionFilePath, 'utf8');
90
+ return version.trim() || null;
91
+ }
92
+ catch (e) {
93
+ debug(`Could not read the installed JS-Controller version: ${e}`);
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * Remembers which JS-Controller version is installed in the test directory
99
+ */
100
+ async saveInstalledControllerVersion(controllerVersion) {
101
+ try {
102
+ await (0, fs_extra_1.writeFile)(this.getControllerVersionFilePath(), controllerVersion, 'utf8');
103
+ }
104
+ catch (e) {
105
+ debug(`Could not save the installed JS-Controller version: ${e}`);
106
+ }
107
+ }
108
+ /**
109
+ * Removes the installed dependencies and the data directory from the test directory.
110
+ * This is necessary when switching JS-Controller versions, so no stale files and states are left behind.
111
+ */
112
+ async clearTestDir() {
113
+ debug('Clearing the test directory...');
114
+ await (0, fs_extra_1.emptyDir)(path.join(this.testDir, 'node_modules'));
115
+ await (0, fs_extra_1.emptyDir)(this.testDataDir);
116
+ await this.clearLogDir();
117
+ debug(' => done!');
118
+ }
73
119
  async prepareTestDir(controllerVersion) {
74
120
  const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10);
75
121
  // js-controller 7.2.3 dropped support for Node.js 18 and 20. If no specific
@@ -81,6 +127,13 @@ class ControllerSetup {
81
127
  debug(`Preparing the test directory. JS-Controller version: "${controllerVersion}"...`);
82
128
  // Make sure the test dir exists
83
129
  await (0, fs_extra_1.ensureDir)(this.testDir);
130
+ // If the test directory was previously used with a different JS-Controller version,
131
+ // remove the installed files and the data directory, so no stale state is left behind
132
+ const installedControllerVersion = await this.getInstalledControllerVersion();
133
+ if (installedControllerVersion && installedControllerVersion !== controllerVersion) {
134
+ debug(`JS-Controller version changed from "${installedControllerVersion}" to "${controllerVersion}", cleaning up...`);
135
+ await this.clearTestDir();
136
+ }
84
137
  // Write the package.json
85
138
  const packageJson = {
86
139
  name: path.basename(this.testDir),
@@ -123,6 +176,8 @@ class ControllerSetup {
123
176
  if (wasJsControllerInstalled) {
124
177
  await this.setupJsController();
125
178
  }
179
+ // Remember which version is installed now, so we can detect a version change on the next run
180
+ await this.saveInstalledControllerVersion(controllerVersion);
126
181
  debug(' => done!');
127
182
  }
128
183
  /**
@@ -1,6 +1,27 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
2
  import { EventEmitter } from 'node:events';
3
3
  import type { DBConnection } from './dbConnection';
4
+ /** A single log message of the adapter under test */
5
+ export interface AdapterLog {
6
+ /** The log level the message was logged with */
7
+ level: ioBroker.LogLevel;
8
+ /** The time the message was logged at */
9
+ timestamp: Date;
10
+ /** The source of the message, e.g. `my-adapter.0`. Undefined if it could not be determined */
11
+ from: string | undefined;
12
+ /** The logged message without timestamp, level and source */
13
+ message: string;
14
+ /** The unparsed log line as it was printed by the adapter */
15
+ raw: string;
16
+ }
17
+ /**
18
+ * Parses a line of the adapter output into a structured log message.
19
+ * Lines that are not in the ioBroker log format (e.g. plain `console.log` output)
20
+ * are returned as `info` messages.
21
+ *
22
+ * @param line A single line of the adapter's stdout/stderr
23
+ */
24
+ export declare function parseAdapterLogLine(line: string): AdapterLog;
4
25
  export interface TestHarness {
5
26
  on(event: 'objectChange', handler: ioBroker.ObjectChangeHandler): this;
6
27
  on(event: 'stateChange', handler: ioBroker.StateChangeHandler): this;
@@ -59,13 +80,75 @@ export declare class TestHarness extends EventEmitter {
59
80
  /** Stops the adapter process */
60
81
  stopAdapter(): Promise<void> | undefined;
61
82
  /**
62
- * Updates the adapter config. The changes can be a subset of the target object
83
+ * Updates the adapter config. The changes can be a subset of the target object.
84
+ * The `native` properties that are listed in the instance object's `encryptedNative`
85
+ * are encrypted automatically, so they can be passed in plain text.
63
86
  */
64
87
  changeAdapterConfig(adapterName: string, changes: Record<string, any>): Promise<void>;
88
+ /**
89
+ * Reads the config of an adapter instance. The `native` properties that are listed in the
90
+ * instance object's `encryptedNative` are decrypted automatically, so they are returned in plain text.
91
+ *
92
+ * @param adapterName The name of the adapter. Defaults to the adapter under test.
93
+ */
94
+ getAdapterConfig(adapterName?: string): Promise<ioBroker.InstanceObject | null>;
95
+ /**
96
+ * Returns the names of all `native` properties in the given config that must be en-/decrypted
97
+ */
98
+ private getEncryptedFields;
99
+ /**
100
+ * Encrypts all `native` properties of the given changes that are listed in the instance
101
+ * object's `encryptedNative`. Returns the changes to apply - the passed object is not modified.
102
+ */
103
+ private encryptNativeChanges;
104
+ private _systemSecret;
105
+ /**
106
+ * Reads the secret from the `system.config` object. The secret is cached after the first read.
107
+ */
108
+ private getSystemSecret;
109
+ /**
110
+ * Encrypts a value the same way the JS-Controller does for `encryptedNative` properties
111
+ */
112
+ encryptValue(value: string): Promise<string>;
113
+ /**
114
+ * Decrypts a value that was encrypted for an `encryptedNative` property
115
+ */
116
+ decryptValue(value: string): Promise<string>;
65
117
  getAdapterExecutionMode(): ioBroker.AdapterCommon['mode'];
66
118
  /** Enables the sendTo method */
67
119
  enableSendTo(): Promise<void>;
68
120
  private sendToID;
69
121
  /** Sends a message to an adapter instance */
70
122
  sendTo(target: string, command: string, message: any, callback: ioBroker.MessageCallback): void;
123
+ /** The log messages of the adapter under test */
124
+ private _logs;
125
+ /** The incomplete last line of each output stream, waiting for the rest to arrive */
126
+ private _outputBuffer;
127
+ /**
128
+ * Handles a chunk of the adapter's output. Because a chunk may end in the middle of a line,
129
+ * the incomplete rest is buffered until the remainder arrives.
130
+ *
131
+ * @param chunk The received chunk
132
+ * @param stream Which of the adapter's output streams the chunk was received on
133
+ */
134
+ private handleAdapterOutput;
135
+ /** Prints a line of the adapter's output and remembers it as a log message */
136
+ private handleAdapterOutputLine;
137
+ /** Handles the incomplete lines that were left over when the adapter exited */
138
+ private flushAdapterOutput;
139
+ /**
140
+ * Returns the log messages the adapter has printed so far
141
+ *
142
+ * @param level If given, only the messages with this log level are returned
143
+ */
144
+ getLogs(level?: ioBroker.LogLevel): AdapterLog[];
145
+ /** Forgets all log messages that were captured so far */
146
+ clearLogs(): void;
147
+ /**
148
+ * Tests if the adapter has logged a message matching the given pattern
149
+ *
150
+ * @param pattern A RegExp or a string that must be contained in the message
151
+ * @param level If given, only the messages with this log level are checked
152
+ */
153
+ hasLog(pattern: string | RegExp, level?: ioBroker.LogLevel): boolean;
71
154
  }
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.TestHarness = void 0;
40
+ exports.parseAdapterLogLine = parseAdapterLogLine;
40
41
  const async_1 = require("alcalzone-shared/async");
41
42
  const objects_1 = require("alcalzone-shared/objects");
42
43
  const node_child_process_1 = require("node:child_process");
@@ -47,7 +48,54 @@ const adapterTools_1 = require("../../../lib/adapterTools");
47
48
  const tools_1 = require("./tools");
48
49
  const debug = (0, debug_1.default)('testing:integration:TestHarness');
49
50
  const isWindows = /^win/.test(process.platform);
51
+ const logLevels = ['silly', 'debug', 'info', 'warn', 'error'];
52
+ /** Matches the color codes the adapter logger adds to the console output */
53
+ // eslint-disable-next-line no-control-regex
54
+ const ansiRegex = /\x1B\[\d+m/g;
55
+ /** Matches `2023-11-08 13:31:57.123 - info: my-adapter.0 (1234) The message` */
56
+ const logLineRegex = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+-\s+(\w+):\s+([\s\S]*)$/;
57
+ /** Matches the `my-adapter.0 (1234) ` prefix the adapter logger prepends to each message */
58
+ const logSourceRegex = /^(\S+\.\d+)(?: \(\d+\))? (.*)$/;
59
+ /**
60
+ * Parses a line of the adapter output into a structured log message.
61
+ * Lines that are not in the ioBroker log format (e.g. plain `console.log` output)
62
+ * are returned as `info` messages.
63
+ *
64
+ * @param line A single line of the adapter's stdout/stderr
65
+ */
66
+ function parseAdapterLogLine(line) {
67
+ const raw = line.replace(ansiRegex, '').trimEnd();
68
+ const match = logLineRegex.exec(raw);
69
+ const level = match?.[2].toLowerCase();
70
+ if (!match || !level || !logLevels.includes(level)) {
71
+ // Not an ioBroker log line, e.g. plain console output or a stack trace
72
+ return { level: 'info', timestamp: new Date(), from: undefined, message: raw, raw };
73
+ }
74
+ const sourceMatch = logSourceRegex.exec(match[3]);
75
+ return {
76
+ level,
77
+ timestamp: new Date(match[1]),
78
+ from: sourceMatch?.[1],
79
+ message: sourceMatch ? sourceMatch[2] : match[3],
80
+ raw,
81
+ };
82
+ }
50
83
  const fromAdapterID = 'system.adapter.test.0';
84
+ /**
85
+ * Encrypts or decrypts a value with the given secret. This is the same symmetric algorithm
86
+ * the JS-Controller uses for the `native` properties listed in `encryptedNative`,
87
+ * so applying it twice returns the original value.
88
+ *
89
+ * @param secret The secret from the `system.config` object
90
+ * @param value The value to encrypt or decrypt
91
+ */
92
+ function encryptDecrypt(secret, value) {
93
+ let result = '';
94
+ for (let i = 0; i < value.length; ++i) {
95
+ result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ value.charCodeAt(i));
96
+ }
97
+ return result;
98
+ }
51
99
  /**
52
100
  * The test harness capsules the execution of the JS-Controller and the adapter instance and monitors their status.
53
101
  * Use it in every test to start a fresh adapter instance
@@ -169,6 +217,7 @@ class TestHarness extends node_events_1.EventEmitter {
169
217
  const mainFileRelative = path.relative(this.testAdapterDir, mainFileAbsolute);
170
218
  const onClose = (code, signal) => {
171
219
  this._adapterProcess.removeAllListeners();
220
+ this.flushAdapterOutput();
172
221
  this._adapterExit = code != undefined ? code : signal;
173
222
  this.emit('failed', this._adapterExit);
174
223
  };
@@ -180,11 +229,14 @@ class TestHarness extends node_events_1.EventEmitter {
180
229
  : [mainFileRelative, '--console'];
181
230
  this._adapterProcess = (0, node_child_process_1.spawn)(command, args, {
182
231
  cwd: this.testAdapterDir,
183
- stdio: ['inherit', 'inherit', 'inherit'],
232
+ // stdout and stderr are piped, so the log messages can be captured
233
+ stdio: ['inherit', 'pipe', 'pipe'],
184
234
  env: { ...process.env, ...env },
185
235
  })
186
236
  .on('close', onClose)
187
237
  .on('exit', onClose);
238
+ this._adapterProcess.stdout?.on('data', (chunk) => this.handleAdapterOutput(chunk, 'stdout'));
239
+ this._adapterProcess.stderr?.on('data', (chunk) => this.handleAdapterOutput(chunk, 'stderr'));
188
240
  }
189
241
  /**
190
242
  * Starts the adapter in a separate process and resolves after it has started
@@ -228,6 +280,7 @@ class TestHarness extends node_events_1.EventEmitter {
228
280
  return;
229
281
  }
230
282
  this._adapterProcess.removeAllListeners();
283
+ this.flushAdapterOutput();
231
284
  this._adapterExit = code != undefined ? code : signal;
232
285
  this._adapterProcess = undefined;
233
286
  debug('Adapter process terminated:');
@@ -250,16 +303,95 @@ class TestHarness extends node_events_1.EventEmitter {
250
303
  });
251
304
  }
252
305
  /**
253
- * Updates the adapter config. The changes can be a subset of the target object
306
+ * Updates the adapter config. The changes can be a subset of the target object.
307
+ * The `native` properties that are listed in the instance object's `encryptedNative`
308
+ * are encrypted automatically, so they can be passed in plain text.
254
309
  */
255
310
  async changeAdapterConfig(adapterName, changes) {
256
311
  const adapterInstanceId = `system.adapter.${adapterName}.0`;
257
312
  const obj = await this.dbConnection.getObject(adapterInstanceId);
258
313
  if (obj) {
259
- (0, objects_1.extend)(obj, changes);
314
+ (0, objects_1.extend)(obj, await this.encryptNativeChanges(obj, changes));
260
315
  await this.dbConnection.setObject(adapterInstanceId, obj);
261
316
  }
262
317
  }
318
+ /**
319
+ * Reads the config of an adapter instance. The `native` properties that are listed in the
320
+ * instance object's `encryptedNative` are decrypted automatically, so they are returned in plain text.
321
+ *
322
+ * @param adapterName The name of the adapter. Defaults to the adapter under test.
323
+ */
324
+ async getAdapterConfig(adapterName = this.adapterName) {
325
+ const obj = await this.dbConnection.getObject(`system.adapter.${adapterName}.0`);
326
+ if (!obj) {
327
+ return null;
328
+ }
329
+ const fields = this.getEncryptedFields(obj, obj.native);
330
+ if (fields.length) {
331
+ const secret = await this.getSystemSecret();
332
+ const native = { ...obj.native };
333
+ for (const field of fields) {
334
+ native[field] = encryptDecrypt(secret, native[field]);
335
+ }
336
+ debug(`Decrypted the following config fields: ${fields.join(', ')}`);
337
+ return { ...obj, native };
338
+ }
339
+ return obj;
340
+ }
341
+ /**
342
+ * Returns the names of all `native` properties in the given config that must be en-/decrypted
343
+ */
344
+ getEncryptedFields(obj, native) {
345
+ if (!native || !obj.encryptedNative?.length) {
346
+ return [];
347
+ }
348
+ // Only strings can be en-/decrypted, everything else is left untouched
349
+ return obj.encryptedNative.filter(field => typeof native[field] === 'string');
350
+ }
351
+ /**
352
+ * Encrypts all `native` properties of the given changes that are listed in the instance
353
+ * object's `encryptedNative`. Returns the changes to apply - the passed object is not modified.
354
+ */
355
+ async encryptNativeChanges(obj, changes) {
356
+ const fields = this.getEncryptedFields(obj, changes.native);
357
+ if (!fields.length) {
358
+ return changes;
359
+ }
360
+ const secret = await this.getSystemSecret();
361
+ const native = { ...changes.native };
362
+ for (const field of fields) {
363
+ native[field] = encryptDecrypt(secret, native[field]);
364
+ }
365
+ debug(`Encrypted the following config fields: ${fields.join(', ')}`);
366
+ return { ...changes, native };
367
+ }
368
+ _systemSecret;
369
+ /**
370
+ * Reads the secret from the `system.config` object. The secret is cached after the first read.
371
+ */
372
+ async getSystemSecret() {
373
+ if (this._systemSecret === undefined) {
374
+ const systemConfig = await this.dbConnection.getObject('system.config');
375
+ const secret = systemConfig?.native?.secret;
376
+ if (typeof secret !== 'string' || !secret) {
377
+ throw new Error('Could not read the secret from the object "system.config"!');
378
+ }
379
+ this._systemSecret = secret;
380
+ }
381
+ return this._systemSecret;
382
+ }
383
+ /**
384
+ * Encrypts a value the same way the JS-Controller does for `encryptedNative` properties
385
+ */
386
+ async encryptValue(value) {
387
+ return encryptDecrypt(await this.getSystemSecret(), value);
388
+ }
389
+ /**
390
+ * Decrypts a value that was encrypted for an `encryptedNative` property
391
+ */
392
+ async decryptValue(value) {
393
+ return encryptDecrypt(await this.getSystemSecret(), value);
394
+ }
263
395
  getAdapterExecutionMode() {
264
396
  return (0, adapterTools_1.getAdapterExecutionMode)(this.testAdapterDir);
265
397
  }
@@ -296,5 +428,63 @@ class TestHarness extends node_events_1.EventEmitter {
296
428
  },
297
429
  }, (err, id) => console.log(`published message ${id}`));
298
430
  }
431
+ /** The log messages of the adapter under test */
432
+ _logs = [];
433
+ /** The incomplete last line of each output stream, waiting for the rest to arrive */
434
+ _outputBuffer = { stdout: '', stderr: '' };
435
+ /**
436
+ * Handles a chunk of the adapter's output. Because a chunk may end in the middle of a line,
437
+ * the incomplete rest is buffered until the remainder arrives.
438
+ *
439
+ * @param chunk The received chunk
440
+ * @param stream Which of the adapter's output streams the chunk was received on
441
+ */
442
+ handleAdapterOutput(chunk, stream) {
443
+ const lines = (this._outputBuffer[stream] + chunk.toString()).split('\n');
444
+ // The last entry is either an incomplete line or empty - keep it for the next chunk
445
+ this._outputBuffer[stream] = lines.pop() ?? '';
446
+ for (const line of lines) {
447
+ this.handleAdapterOutputLine(line, stream);
448
+ }
449
+ }
450
+ /** Prints a line of the adapter's output and remembers it as a log message */
451
+ handleAdapterOutputLine(line, stream) {
452
+ // Forward the output, so it stays visible while the tests are running
453
+ process[stream].write(`${line}\n`);
454
+ if (line.trim()) {
455
+ this._logs.push(parseAdapterLogLine(line));
456
+ }
457
+ }
458
+ /** Handles the incomplete lines that were left over when the adapter exited */
459
+ flushAdapterOutput() {
460
+ for (const stream of ['stdout', 'stderr']) {
461
+ const rest = this._outputBuffer[stream];
462
+ this._outputBuffer[stream] = '';
463
+ if (rest) {
464
+ this.handleAdapterOutputLine(rest, stream);
465
+ }
466
+ }
467
+ }
468
+ /**
469
+ * Returns the log messages the adapter has printed so far
470
+ *
471
+ * @param level If given, only the messages with this log level are returned
472
+ */
473
+ getLogs(level) {
474
+ return level ? this._logs.filter(log => log.level === level) : [...this._logs];
475
+ }
476
+ /** Forgets all log messages that were captured so far */
477
+ clearLogs() {
478
+ this._logs = [];
479
+ }
480
+ /**
481
+ * Tests if the adapter has logged a message matching the given pattern
482
+ *
483
+ * @param pattern A RegExp or a string that must be contained in the message
484
+ * @param level If given, only the messages with this log level are checked
485
+ */
486
+ hasLog(pattern, level) {
487
+ return this.getLogs(level).some(log => typeof pattern === 'string' ? log.message.includes(pattern) : pattern.test(log.message));
488
+ }
299
489
  }
300
490
  exports.TestHarness = TestHarness;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iobroker/testing",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
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",