Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
## **WORK IN PROGRESS**
* (@Apollon77/@copilot) Add validation for JSON files in admin/ and admin/i18n/ directories
* (@Apollon77/@copilot) Logs npm and installation errors to console for easier debugging
* (@copilot) Add log capture methods to TestHarness for integration tests (`getLogs()`, `assertLog()`, `clearLogs()`)

## 5.1.1 (2025-08-31)
* (@Apollon77) Downgrades chai-as-promised type dependency to same major as main dependency
Expand Down
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,35 @@ tests.integration(path.join(__dirname, ".."), {
});
});

// Checking adapter logs
suite("Check adapter logs", (getHarness) => {
let harness;
before(() => {
harness = getHarness();
});

it("Should check for specific log messages", async () => {
// Start the adapter and wait until it has started
await harness.startAdapterAndWait();

// Check if a specific log message exists (using regex)
const hasErrorLog = harness.assertLog(/error during startup/, "error");
expect(hasErrorLog).to.be.false; // No error should occur

// Check if an info message exists
const hasStartupLog = harness.assertLog(/Adapter.*started/);
expect(hasStartupLog).to.be.true;

// Get all logs for more detailed inspection
const logs = harness.getLogs();
const errorLogs = logs.filter((log) => log.level === "error");
expect(errorLogs).to.have.lengthOf(0); // Should have no errors

// You can also clear logs if needed
harness.clearLogs();
});
});

// While developing the tests, you can run only a single suite using `suite.only`...
suite.only("Only this will run", (getHarness) => {
// ...
Expand Down Expand Up @@ -146,6 +175,58 @@ These methods take a mock database and adapter and create a set of asserts for y
- `assertObjectCommon(id: string | string[], common: ioBroker.ObjectCommon)` asserts that an object's common part includes the given `common` object.
- `assertObjectNative(id: string | string[], native: object)` asserts that an object's native part includes the given `native` object.

### TestHarness log methods (Integration tests)

The `TestHarness` class (available in integration tests via `getHarness()`) provides methods to capture and inspect adapter logs:

#### getLogs()

```ts
const logs = harness.getLogs();
```

Returns an array of all captured adapter logs. Each log entry is an object with the following structure:

```ts
interface AdapterLog {
level: "silly" | "debug" | "info" | "warn" | "error";
timestamp: Date;
message: string;
}
```

#### clearLogs()

```ts
harness.clearLogs();
```

Clears all captured logs. Useful when you want to check only logs generated after a specific point in your test.

#### assertLog()

```ts
const found = harness.assertLog(pattern, level?);
```

Checks if a log message matching the given criteria exists. Returns `true` if a matching log entry was found, `false` otherwise.

- `pattern`: A string or RegExp to match against log messages
- `level` (optional): Filter by specific log level (e.g., `"error"`, `"warn"`, `"info"`)

**Examples:**

```ts
// Check if any log contains "started"
harness.assertLog(/started/);

// Check if there's an error containing "connection failed"
harness.assertLog(/connection failed/, "error");

// Check if there's a specific info message
harness.assertLog("Adapter initialized successfully", "info");
```

#### MockDatabase

TODO
Expand Down
2 changes: 1 addition & 1 deletion build/tests/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { validatePackageFiles } from './packageFiles';
import { testAdapterWithMocks } from './unit';
import { createMocks } from './unit/harness/createMocks';
import { createAsserts } from './unit/mocks/mockDatabase';
export { TestHarness as IntegrationTestHarness } from './integration/lib/harness';
export { TestHarness as IntegrationTestHarness, type AdapterLog } from './integration/lib/harness';
export type { MockAdapter } from './unit/mocks/mockAdapter';
export { MockDatabase } from './unit/mocks/mockDatabase';
/** Predefined test sets */
Expand Down
31 changes: 31 additions & 0 deletions build/tests/integration/lib/harness.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { type ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import type { DBConnection } from './dbConnection';
export interface AdapterLog {
/** The log level (error, warn, info, debug, silly) */
level: ioBroker.LogLevel;
/** The timestamp when the log was created */
timestamp: Date;
/** The log message */
message: string;
}
export interface TestHarness {
on(event: 'objectChange', handler: ioBroker.ObjectChangeHandler): this;
on(event: 'stateChange', handler: ioBroker.StateChangeHandler): this;
Expand All @@ -23,6 +31,8 @@ export declare class TestHarness extends EventEmitter {
private appName;
private testControllerDir;
private testAdapterDir;
/** Storage for captured adapter logs */
private _logs;
/** Gives direct access to the Objects DB */
get objects(): any;
/** Gives direct access to the States DB */
Expand Down Expand Up @@ -68,4 +78,25 @@ export declare class TestHarness extends EventEmitter {
private sendToID;
/** Sends a message to an adapter instance */
sendTo(target: string, command: string, message: any, callback: ioBroker.MessageCallback): void;
/**
* Parses a log line from the adapter output into a structured log object
* Expected format: "YYYY-MM-DD HH:MM:SS.mmm <level> <adapter>.<instance> <message>"
*/
private parseLogLine;
/**
* Returns all captured adapter logs
*/
getLogs(): AdapterLog[];
/**
* Clears all captured logs
*/
clearLogs(): void;
/**
* Checks if a log message matching the given criteria exists
*
* @param pattern RegExp or string to match against log messages
* @param level Optional log level to filter by
* @returns true if a matching log entry was found
*/
assertLog(pattern: string | RegExp, level?: ioBroker.LogLevel): boolean;
}
91 changes: 90 additions & 1 deletion build/tests/integration/lib/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class TestHarness extends node_events_1.EventEmitter {
this.adapterDir = adapterDir;
this.testDir = testDir;
this.dbConnection = dbConnection;
/** Storage for captured adapter logs */
this._logs = [];
this.sendToID = 1;
debug('Creating instance');
this.adapterName = (0, adapterTools_1.getAdapterName)(this.adapterDir);
Expand Down Expand Up @@ -166,11 +168,42 @@ class TestHarness extends node_events_1.EventEmitter {
};
this._adapterProcess = (0, node_child_process_1.spawn)(isWindows ? 'node.exe' : 'node', [mainFileRelative, '--console'], {
cwd: this.testAdapterDir,
stdio: ['inherit', 'inherit', 'inherit'],
stdio: ['inherit', 'pipe', 'pipe'],
env: { ...process.env, ...env },
})
.on('close', onClose)
.on('exit', onClose);
// Capture stdout and stderr
if (this._adapterProcess.stdout) {
this._adapterProcess.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
const logEntry = this.parseLogLine(line);
if (logEntry) {
this._logs.push(logEntry);
}
// Also output to console for visibility
console.log(line);
}
}
});
}
if (this._adapterProcess.stderr) {
this._adapterProcess.stderr.on('data', (data) => {
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
const logEntry = this.parseLogLine(line);
if (logEntry) {
this._logs.push(logEntry);
}
// Also output to console for visibility
console.error(line);
}
}
});
}
}
/**
* Starts the adapter in a separate process and resolves after it has started
Expand Down Expand Up @@ -281,5 +314,61 @@ class TestHarness extends node_events_1.EventEmitter {
},
}, (err, id) => console.log(`published message ${id}`));
}
/**
* Parses a log line from the adapter output into a structured log object
* Expected format: "YYYY-MM-DD HH:MM:SS.mmm <level> <adapter>.<instance> <message>"
*/
parseLogLine(line) {
// Match typical ioBroker log format
// Example: "2023-11-08 13:31:57.123 info adapter.0 Adapter started"
const logRegex = /^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\w+)\s+\S+\s+(.*)$/;
const match = line.match(logRegex);
if (match) {
const [, timestamp, level, message] = match;
const logLevel = level.toLowerCase();
// Validate log level
if (['silly', 'debug', 'info', 'warn', 'error'].includes(logLevel)) {
return {
level: logLevel,
timestamp: new Date(timestamp),
message: message.trim(),
};
}
}
// If the line doesn't match the expected format or has an invalid log level,
// treat it as info level (fallback for plain console.log statements)
return {
level: 'info',
timestamp: new Date(),
message: line.trim(),
};
}
/**
* Returns all captured adapter logs
*/
getLogs() {
return [...this._logs];
}
/**
* Clears all captured logs
*/
clearLogs() {
this._logs = [];
}
/**
* Checks if a log message matching the given criteria exists
*
* @param pattern RegExp or string to match against log messages
* @param level Optional log level to filter by
* @returns true if a matching log entry was found
*/
assertLog(pattern, level) {
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
return this._logs.some(log => {
const levelMatches = !level || log.level === level;
const messageMatches = regex.test(log.message);
return levelMatches && messageMatches;
});
}
}
exports.TestHarness = TestHarness;
2 changes: 1 addition & 1 deletion src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { testAdapterWithMocks } from './unit';
import { createMocks } from './unit/harness/createMocks';
import { createAsserts } from './unit/mocks/mockDatabase';

export { TestHarness as IntegrationTestHarness } from './integration/lib/harness';
export { TestHarness as IntegrationTestHarness, type AdapterLog } from './integration/lib/harness';
export type { MockAdapter } from './unit/mocks/mockAdapter';
export { MockDatabase } from './unit/mocks/mockDatabase';

Expand Down
50 changes: 50 additions & 0 deletions src/tests/integration/lib/harness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect } from 'chai';
import { TestHarness, type AdapterLog } from './harness';

describe('TestHarness - Log Capture', () => {
describe('parseLogLine', () => {
// We need to access the private parseLogLine method for testing
// Since it's private, we'll test it through the public interface

it('should capture logs in the logs array', () => {
// This is a basic structural test to ensure the logs array exists
// Full integration testing would require spawning an actual adapter
const logs: AdapterLog[] = [];
expect(logs).to.be.an('array');
});

it('should validate AdapterLog structure', () => {
const log: AdapterLog = {
level: 'info',
timestamp: new Date(),
message: 'test message',
};

expect(log).to.have.property('level');
expect(log).to.have.property('timestamp');
expect(log).to.have.property('message');
expect(log.level).to.be.oneOf(['silly', 'debug', 'info', 'warn', 'error']);
expect(log.timestamp).to.be.instanceOf(Date);
expect(log.message).to.be.a('string');
});
});

describe('Log retrieval methods', () => {
it('should have getLogs method signature', () => {
// This test verifies the method exists in the type system
// We can't test it fully without spawning an adapter
const methodExists = Object.prototype.hasOwnProperty.call(TestHarness.prototype, 'getLogs');
expect(methodExists || 'getLogs' in TestHarness.prototype).to.be.true;
});

it('should have clearLogs method signature', () => {
const methodExists = Object.prototype.hasOwnProperty.call(TestHarness.prototype, 'clearLogs');
expect(methodExists || 'clearLogs' in TestHarness.prototype).to.be.true;
});

it('should have assertLog method signature', () => {
const methodExists = Object.prototype.hasOwnProperty.call(TestHarness.prototype, 'assertLog');
expect(methodExists || 'assertLog' in TestHarness.prototype).to.be.true;
});
});
});
Loading