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
26 changes: 24 additions & 2 deletions test/js-adapters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ const fs = require('node:fs');
const path = require('node:path');
const {
runCommand,
runCommandWithSignal,
runCommandWithSignal,
runCommandWithFileChange,
setupTestAdapter,
cleanupTestAdapter,
validateIoPackageJson,
validatePackageJson,
validateTypeScriptConfig,
runDevServerSetupTest,
validateRunTestOutput,
validateWatchTestOutput
validateWatchTestOutput,
validateWatchRestartOutput
} = require('./test-utils');

const DEV_SERVER_ROOT = path.resolve(__dirname, '..');
Expand Down Expand Up @@ -100,5 +102,25 @@ describe('dev-server integration tests', function () {
const output = result.stdout + result.stderr;
validateWatchTestOutput(output, 'test-js');
});

it('should restart adapter when main file changes', async () => {
this.timeout(WATCH_TIMEOUT + 60000); // Extra time for file change and restart

const devServerPath = path.join(DEV_SERVER_ROOT, 'dist', 'index.js');
const mainFile = path.join(JS_ADAPTER_DIR, 'main.js');

const result = await runCommandWithFileChange('node', [devServerPath, 'watch'], {
cwd: JS_ADAPTER_DIR,
timeout: WATCH_TIMEOUT + 30000,
verbose: true,
initialMessage: /test-js\.0 \([\d]+\) state test-js\.0\.testVariable deleted/g,
finalMessage: /test-js\.0 \([\d]+\) state test-js\.0\.testVariable deleted/g,
fileToChange: mainFile,
});

const output = result.stdout + result.stderr;
validateWatchTestOutput(output, 'test-js');
validateWatchRestartOutput(output, 'test-js');
});
});
});
32 changes: 30 additions & 2 deletions test/pure-ts-adapters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ const fs = require('node:fs');
const path = require('node:path');
const {
runCommand,
runCommandWithSignal,
runCommandWithSignal,
runCommandWithFileChange,
setupTestAdapter,
cleanupTestAdapter,
validateIoPackageJson,
validatePackageJson,
validateTypeScriptConfig,
runDevServerSetupTest,
validateRunTestOutput,
validateWatchTestOutput
validateWatchTestOutput,
validateWatchRestartOutput
} = require('./test-utils');

const DEV_SERVER_ROOT = path.resolve(__dirname, '..');
Expand Down Expand Up @@ -161,5 +163,31 @@ describe('dev-server integration tests - Pure TypeScript', function () {
'esbuild-register should successfully transpile and execute TypeScript files'
);
});

it('should restart adapter when TypeScript source file changes', async () => {
this.timeout(WATCH_TIMEOUT + 60000); // Extra time for file change and restart

const devServerPath = path.join(DEV_SERVER_ROOT, 'dist', 'index.js');
const mainFile = path.join(PURE_TS_ADAPTER_DIR, 'src', 'main.ts');

const result = await runCommandWithFileChange('node', [devServerPath, 'watch'], {
cwd: PURE_TS_ADAPTER_DIR,
timeout: WATCH_TIMEOUT + 30000,
verbose: true,
initialMessage: /test-pure-ts\.0 \([\d]+\) state test-pure-ts\.0\.testVariable deleted/g,
finalMessage: /test-pure-ts\.0 \([\d]+\) state test-pure-ts\.0\.testVariable deleted/g,
fileToChange: mainFile,
});

const output = result.stdout + result.stderr;
validateWatchTestOutput(output, 'test-pure-ts');
validateWatchRestartOutput(output, 'test-pure-ts');

// Verify that esbuild-register is working by checking that TypeScript files are being executed
assert.ok(
output.includes('starting. Version 0.0.1'),
'esbuild-register should successfully transpile and execute TypeScript files after restart'
);
});
});
});
105 changes: 100 additions & 5 deletions test/test-utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { spawn } = require('node:child_process');
const path = require('node:path');
const fs = require('node:fs');
const assert = require('node:assert');

/**
* Run a command and return promise
Expand Down Expand Up @@ -59,11 +60,16 @@ function runCommand(command, args, options = {}) {
}

/**
* Run a command with timeout and signal handling
* Core function to run a command with timeout and signal handling
* @param {string} command - Command to run
* @param {string[]} args - Command arguments
* @param {object} options - Options including timeout, verbose, finalMessage, onStdout callback
* @returns {Promise<{stdout: string, stderr: string, code: number, killed?: boolean}>}
*/
function runCommandWithSignal(command, args, options = {}) {
function runCommandWithTimeout(command, args, options = {}) {
return new Promise((resolve, reject) => {
console.log(`Running with signal handling: ${command} ${args.join(' ')}`);
const logPrefix = options.logPrefix || 'Running with signal handling';
console.log(`${logPrefix}: ${command} ${args.join(' ')}`);
const proc = spawn(command, args, {
stdio: ['pipe', 'pipe', 'pipe'],
timeout: options.timeout || 30000,
Expand Down Expand Up @@ -110,7 +116,14 @@ function runCommandWithSignal(command, args, options = {}) {
if (options.verbose) {
console.log('STDOUT:', str.trim());
}
if (options.finalMessage && str.match(options.finalMessage) && !closed && !resolvedOrRejected) {

// Call custom stdout handler if provided
if (options.onStdout) {
options.onStdout(str, shutDown);
}

// Default behavior: shutdown on final message
if (!options.onStdout && options.finalMessage && str.match(options.finalMessage) && !closed && !resolvedOrRejected) {
console.log('Final message detected, shutting down...');
setTimeout(shutDown, 10000);
}
Expand Down Expand Up @@ -153,6 +166,13 @@ function runCommandWithSignal(command, args, options = {}) {
});
}

/**
* Run a command with timeout and signal handling
*/
function runCommandWithSignal(command, args, options = {}) {
return runCommandWithTimeout(command, args, options);
}

/**
* Create test adapter using @iobroker/create-adapter
*/
Expand Down Expand Up @@ -429,9 +449,83 @@ function validateWatchTestOutput(output, adapterPrefix) {
assert.ok(infoLines.length > 0, `No info logs found from ${adapterPrefix}.0 adapter`);
}

/**
* Run watch command with file change trigger to test adapter restart
*/
function runCommandWithFileChange(command, args, options = {}) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot seems the new function is like the runCommandWithSignal and just adds a new file change logic but now duplicates a lot code. Refactor please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use common runCommandWithTimeout function, eliminating code duplication (commit: {{COMMIT_HASH}})

let fileChanged = false;
let restartDetected = false;

const onStdout = (str, shutDown) => {
// Trigger file change after initial startup
if (!fileChanged && options.initialMessage && str.match(options.initialMessage)) {
console.log('Initial message detected, triggering file change...');
fileChanged = true;

// Wait a bit then trigger file change
setTimeout(() => {
if (options.fileToChange) {
console.log(`Touching file: ${options.fileToChange}`);
try {
// Touch the file to trigger nodemon restart
const now = new Date();
fs.utimesSync(options.fileToChange, now, now);
} catch (error) {
console.error('Error touching file:', error);
}
}
}, 5000);
}

// Detect restart and wait for it to complete
if (fileChanged && !restartDetected && str.match(/restarting|restart/i)) {
console.log('Restart detected...');
restartDetected = true;
}

// After restart, wait for final message
if (restartDetected && options.finalMessage && str.match(options.finalMessage)) {
console.log('Final message after restart detected, shutting down...');
setTimeout(shutDown, 10000);
}
};

return runCommandWithTimeout(command, args, {
...options,
logPrefix: 'Running with file change trigger',
onStdout
});
}

/**
* Validate that adapter restart occurred in watch mode
*/
function validateWatchRestartOutput(output, adapterPrefix) {
// Should see nodemon restart messages
assert.ok(
output.includes('restarting'),
'No nodemon restart message found in output'
);

// Should see adapter starting exactly twice (initial + after restart)
const startingMatches = output.match(/starting\. Version 0\.0\.1/g);
assert.ok(
startingMatches && startingMatches.length === 2,
`Adapter should start exactly twice (initial + restart), but found ${startingMatches ? startingMatches.length : 0} instances`
);

// Should see the test variable deletion message exactly twice
const testVarMatches = output.match(new RegExp(`state ${adapterPrefix}\\.0\\.testVariable deleted`, 'g'));
assert.ok(
testVarMatches && testVarMatches.length === 2,
`Should see testVariable deletion exactly twice (initial + restart), but found ${testVarMatches ? testVarMatches.length : 0} instances`
);
}

module.exports = {
runCommand,
runCommandWithSignal,
runCommandWithFileChange,
createTestAdapter,
logSetupInfo,
setupTestAdapter,
Expand All @@ -443,5 +537,6 @@ module.exports = {
validateTypeScriptConfig,
runDevServerSetupTest,
validateRunTestOutput,
validateWatchTestOutput
validateWatchTestOutput,
validateWatchRestartOutput
};
26 changes: 24 additions & 2 deletions test/ts-adapters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ const fs = require('node:fs');
const path = require('node:path');
const {
runCommand,
runCommandWithSignal,
runCommandWithSignal,
runCommandWithFileChange,
setupTestAdapter,
cleanupTestAdapter,
validateIoPackageJson,
validatePackageJson,
validateTypeScriptConfig,
runDevServerSetupTest,
validateRunTestOutput,
validateWatchTestOutput
validateWatchTestOutput,
validateWatchRestartOutput
} = require('./test-utils');

const DEV_SERVER_ROOT = path.resolve(__dirname, '..');
Expand Down Expand Up @@ -95,5 +97,25 @@ describe('dev-server integration tests', function () {
const output = result.stdout + result.stderr;
validateWatchTestOutput(output, 'test-ts');
});

it('should restart adapter when main file changes', async () => {
this.timeout(WATCH_TIMEOUT + 60000); // Extra time for file change and restart

const devServerPath = path.join(DEV_SERVER_ROOT, 'dist', 'index.js');
const mainFile = path.join(TS_ADAPTER_DIR, 'build', 'main.js');

const result = await runCommandWithFileChange('node', [devServerPath, 'watch'], {
cwd: TS_ADAPTER_DIR,
timeout: WATCH_TIMEOUT + 30000,
verbose: true,
initialMessage: /test-ts\.0 \([\d]+\) state test-ts\.0\.testVariable deleted/g,
finalMessage: /test-ts\.0 \([\d]+\) state test-ts\.0\.testVariable deleted/g,
fileToChange: mainFile,
});

const output = result.stdout + result.stderr;
validateWatchTestOutput(output, 'test-ts');
validateWatchRestartOutput(output, 'test-ts');
});
});
});