diff --git a/test/js-adapters.test.js b/test/js-adapters.test.js index 3925699..671983d 100644 --- a/test/js-adapters.test.js +++ b/test/js-adapters.test.js @@ -4,7 +4,8 @@ const fs = require('node:fs'); const path = require('node:path'); const { runCommand, - runCommandWithSignal, + runCommandWithSignal, + runCommandWithFileChange, setupTestAdapter, cleanupTestAdapter, validateIoPackageJson, @@ -12,7 +13,8 @@ const { validateTypeScriptConfig, runDevServerSetupTest, validateRunTestOutput, - validateWatchTestOutput + validateWatchTestOutput, + validateWatchRestartOutput } = require('./test-utils'); const DEV_SERVER_ROOT = path.resolve(__dirname, '..'); @@ -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'); + }); }); }); diff --git a/test/pure-ts-adapters.test.js b/test/pure-ts-adapters.test.js index 5b97476..044f750 100644 --- a/test/pure-ts-adapters.test.js +++ b/test/pure-ts-adapters.test.js @@ -4,7 +4,8 @@ const fs = require('node:fs'); const path = require('node:path'); const { runCommand, - runCommandWithSignal, + runCommandWithSignal, + runCommandWithFileChange, setupTestAdapter, cleanupTestAdapter, validateIoPackageJson, @@ -12,7 +13,8 @@ const { validateTypeScriptConfig, runDevServerSetupTest, validateRunTestOutput, - validateWatchTestOutput + validateWatchTestOutput, + validateWatchRestartOutput } = require('./test-utils'); const DEV_SERVER_ROOT = path.resolve(__dirname, '..'); @@ -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' + ); + }); }); }); diff --git a/test/test-utils.js b/test/test-utils.js index 45949ba..bab35af 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -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 @@ -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, @@ -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); } @@ -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 */ @@ -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 = {}) { + 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, @@ -443,5 +537,6 @@ module.exports = { validateTypeScriptConfig, runDevServerSetupTest, validateRunTestOutput, - validateWatchTestOutput + validateWatchTestOutput, + validateWatchRestartOutput }; diff --git a/test/ts-adapters.test.js b/test/ts-adapters.test.js index 59c78ab..7edda54 100644 --- a/test/ts-adapters.test.js +++ b/test/ts-adapters.test.js @@ -4,7 +4,8 @@ const fs = require('node:fs'); const path = require('node:path'); const { runCommand, - runCommandWithSignal, + runCommandWithSignal, + runCommandWithFileChange, setupTestAdapter, cleanupTestAdapter, validateIoPackageJson, @@ -12,7 +13,8 @@ const { validateTypeScriptConfig, runDevServerSetupTest, validateRunTestOutput, - validateWatchTestOutput + validateWatchTestOutput, + validateWatchRestartOutput } = require('./test-utils'); const DEV_SERVER_ROOT = path.resolve(__dirname, '..'); @@ -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'); + }); }); });