-
Notifications
You must be signed in to change notification settings - Fork 78
test: cdk watch integ tests for cli and toolkit lib #1139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3065b0f
2b1ee05
6ca8493
36efe30
8a75e29
d72ce2c
dd94cc5
768aa69
e5bafec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,110 @@ | ||||||
| import * as child_process from 'child_process'; | ||||||
| import * as fs from 'fs'; | ||||||
| import * as path from 'path'; | ||||||
| import { integTest, withDefaultFixture } from '../../../lib'; | ||||||
|
|
||||||
| jest.setTimeout(10 * 60 * 1000); // 10 minutes for watch tests | ||||||
|
|
||||||
| integTest( | ||||||
| 'cdk watch detects file changes with glob patterns', | ||||||
| withDefaultFixture(async (fixture) => { | ||||||
| // Create a test file that will be watched | ||||||
| const testFile = path.join(fixture.integTestDir, 'watch-test-file.ts'); | ||||||
| fs.writeFileSync(testFile, 'export const initial = true;'); | ||||||
|
|
||||||
| // Update cdk.json to include watch configuration | ||||||
| const cdkJsonPath = path.join(fixture.integTestDir, 'cdk.json'); | ||||||
| const cdkJson = JSON.parse(fs.readFileSync(cdkJsonPath, 'utf-8')); | ||||||
| cdkJson.watch = { | ||||||
| include: ['**/*.ts', '**/*.js'], | ||||||
| exclude: ['node_modules/**', 'cdk.out/**', '**/*.d.ts'], | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this part isn't tested. in this test or a new one, we should be testing that changes to |
||||||
| }; | ||||||
| fs.writeFileSync(cdkJsonPath, JSON.stringify(cdkJson, null, 2)); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can also just call |
||||||
|
|
||||||
| await fixture.cli.makeCliAvailable(); | ||||||
|
|
||||||
| let output = ''; | ||||||
|
|
||||||
| // Start cdk watch with detached process group for clean termination | ||||||
| const watchProcess = child_process.spawn('cdk', [ | ||||||
| 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), | ||||||
| ], { | ||||||
| cwd: fixture.integTestDir, | ||||||
| shell: true, | ||||||
| detached: true, | ||||||
| env: { ...process.env, ...fixture.cdkShellEnv() }, | ||||||
| }); | ||||||
|
|
||||||
| watchProcess.stdout?.on('data', (data) => { | ||||||
| output += data.toString(); | ||||||
| fixture.log(data.toString()); | ||||||
| }); | ||||||
| watchProcess.stderr?.on('data', (data) => { | ||||||
| output += data.toString(); | ||||||
| fixture.log(data.toString()); | ||||||
| }); | ||||||
|
|
||||||
| try { | ||||||
| await waitForOutput(() => output, "Triggering initial 'cdk deploy'", 120000); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how did you come up with these timeout durations? |
||||||
| fixture.log('✓ Watch started'); | ||||||
|
|
||||||
| await waitForOutput(() => output, 'deployment time', 300000); | ||||||
| fixture.log('✓ Initial deployment completed'); | ||||||
|
|
||||||
| // Modify the test file to trigger a watch event | ||||||
| fs.writeFileSync(testFile, 'export const modified = true;'); | ||||||
|
|
||||||
| await waitForOutput(() => output, 'Detected change to', 60000); | ||||||
| fixture.log('✓ Watch detected file change'); | ||||||
|
|
||||||
| // Wait for second deployment | ||||||
| await waitForCondition( | ||||||
| () => (output.match(/deployment time/g) || []).length >= 2, | ||||||
| 60000, | ||||||
| 'second deployment to complete', | ||||||
| ); | ||||||
| fixture.log('✓ Deployment triggered after file change'); | ||||||
| } finally { | ||||||
| // Kill entire process group | ||||||
| if (watchProcess.pid) { | ||||||
| try { | ||||||
| process.kill(-watchProcess.pid, 'SIGKILL'); | ||||||
| } catch { | ||||||
| /* ignore */ | ||||||
| } | ||||||
| } | ||||||
| await new Promise(resolve => setTimeout(resolve, 3000)); | ||||||
|
|
||||||
| // Use separate output dir to avoid conflicts with lingering watch process | ||||||
| await fixture.cdkDestroy('test-1', { options: ['--output', 'cdk-destroy.out'] }); | ||||||
|
Comment on lines
+78
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to manually delete. The rest harness is doing this for us.
Suggested change
|
||||||
| } | ||||||
| }), | ||||||
| ); | ||||||
|
|
||||||
| async function waitForOutput(getOutput: () => string, searchString: string, timeoutMs: number): Promise<void> { | ||||||
| const startTime = Date.now(); | ||||||
| return new Promise((resolve, reject) => { | ||||||
| const check = () => { | ||||||
| if (getOutput().includes(searchString)) return resolve(); | ||||||
| if (Date.now() - startTime > timeoutMs) { | ||||||
| return reject(new Error(`Timeout waiting for: "${searchString}"`)); | ||||||
| } | ||||||
| setTimeout(check, 1000); | ||||||
| }; | ||||||
| check(); | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| async function waitForCondition(condition: () => boolean, timeoutMs: number, description: string): Promise<void> { | ||||||
| const startTime = Date.now(); | ||||||
| return new Promise((resolve, reject) => { | ||||||
| const check = () => { | ||||||
| if (condition()) return resolve(); | ||||||
| if (Date.now() - startTime > timeoutMs) { | ||||||
| return reject(new Error(`Timeout waiting for ${description}`)); | ||||||
| } | ||||||
| setTimeout(check, 1000); | ||||||
| }; | ||||||
| check(); | ||||||
| }); | ||||||
| } | ||||||
|
Comment on lines
+84
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there really no existing helpers for this? |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| /* eslint-disable import/no-extraneous-dependencies */ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import * as toolkit from '@aws-cdk/toolkit-lib'; | ||
| import { assemblyFromCdkAppDir, toolkitFromFixture } from './toolkit-helpers'; | ||
| import { integTest, withDefaultFixture } from '../../lib'; | ||
|
|
||
| /** | ||
| * Integration tests for toolkit-lib watch with glob pattern matching. | ||
| * | ||
| * These tests verify that the chokidar v4 glob pattern fix works correctly | ||
| * by using the actual Toolkit.watch() method with real file system operations. | ||
| */ | ||
|
|
||
| integTest( | ||
| 'toolkit watch detects file changes with glob patterns', | ||
| withDefaultFixture(async (fixture) => { | ||
| const tk = toolkitFromFixture(fixture); | ||
| const assembly = await assemblyFromCdkAppDir(tk, fixture); | ||
|
|
||
| // Track events received from the watcher | ||
| const receivedEvents: Array<{ code: string | undefined; message: string }> = []; | ||
|
|
||
| // Create a custom IoHost to capture watch events | ||
| const customTk = new toolkit.Toolkit({ | ||
| ioHost: { | ||
| notify: async (msg) => { | ||
| receivedEvents.push({ code: msg.code, message: msg.message }); | ||
| }, | ||
| requestResponse: async <T>(): Promise<T> => undefined as unknown as T, | ||
| }, | ||
| }); | ||
|
|
||
| // Create a test file in the watch directory | ||
| const testFile = path.join(fixture.integTestDir, 'watch-test-file.ts'); | ||
|
|
||
| // Start watching with specific include patterns | ||
| const watcher = await customTk.watch(assembly, { | ||
| include: ['**/*.ts'], | ||
| exclude: ['**/node_modules/**', '**/*.test.ts'], | ||
| watchDir: fixture.integTestDir, | ||
| // Use a deployment method that won't actually deploy (we just want to test file watching) | ||
| deploymentMethod: { method: 'hotswap' }, | ||
| }); | ||
|
|
||
| try { | ||
| // Wait a bit for the watcher to initialize | ||
| await sleep(1000); | ||
|
|
||
| // Create a new .ts file - this should be detected | ||
| fs.writeFileSync(testFile, 'export const watchTest = true;'); | ||
|
|
||
| // Wait for the file change to be detected | ||
| await sleep(2000); | ||
|
|
||
| // Verify that the watcher detected the file | ||
| const observingEvents = receivedEvents.filter(e => | ||
| e.code === 'CDK_TOOLKIT_I5311' || // observing file | ||
| e.code === 'CDK_TOOLKIT_I5312' || // detected change | ||
| e.code === 'CDK_TOOLKIT_I5314', // triggering deploy | ||
| ); | ||
|
|
||
| fixture.log(`Received ${observingEvents.length} watch-related events`); | ||
| for (const event of observingEvents) { | ||
| fixture.log(` ${event.code}: ${event.message.substring(0, 100)}...`); | ||
| } | ||
|
|
||
| // The watcher should have received the ready event and started observing | ||
| const hasReadyOrObserving = receivedEvents.some(e => | ||
| e.code === 'CDK_TOOLKIT_I5314' || // triggering initial deploy | ||
| e.code === 'CDK_TOOLKIT_I5311', // observing files | ||
| ); | ||
|
|
||
| if (!hasReadyOrObserving) { | ||
| throw new Error('Watcher did not emit ready/observing events'); | ||
| } | ||
|
|
||
| fixture.log('✓ Toolkit watch successfully initialized and detected files'); | ||
| } finally { | ||
| // Clean up - dispose and wait for async operations to settle | ||
| await watcher.dispose(); | ||
| await sleep(1000); // Allow async operations to complete | ||
| if (fs.existsSync(testFile)) { | ||
| fs.unlinkSync(testFile); | ||
| } | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| integTest( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only one test per file. Pls move to separate file. |
||
| 'toolkit watch excludes node_modules and dotfiles by default', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this test not part of the cli integ tests also?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it will add more time to the test, while offering limited utility IMO. Probably another minute or so. See my PR description where I addressed this discrepancy.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i dont understand why this would be the case. i think you need to as for whether this offers utility, i think its worse if we accidentally don't |
||
| withDefaultFixture(async (fixture) => { | ||
| const tk = toolkitFromFixture(fixture); | ||
| const assembly = await assemblyFromCdkAppDir(tk, fixture); | ||
|
|
||
| // Track the exclude patterns that were configured | ||
| const configMessages: string[] = []; | ||
|
|
||
| // Create a custom IoHost to capture configuration messages | ||
| const customTk = new toolkit.Toolkit({ | ||
| ioHost: { | ||
| notify: async (msg) => { | ||
| if (msg.code === 'CDK_TOOLKIT_I5310') { | ||
| configMessages.push(msg.message); | ||
| } | ||
| }, | ||
| requestResponse: async <T>(): Promise<T> => undefined as unknown as T, | ||
| }, | ||
| }); | ||
|
|
||
| // Start watching with default exclude patterns | ||
| const watcher = await customTk.watch(assembly, { | ||
| include: ['**'], | ||
| watchDir: fixture.integTestDir, | ||
| deploymentMethod: { method: 'hotswap' }, | ||
| }); | ||
|
|
||
| try { | ||
| // Wait for initialization | ||
| await sleep(500); | ||
|
|
||
| // Verify that default excludes are applied | ||
| const configMsg = configMessages.find(m => m.includes("'exclude' patterns")); | ||
|
|
||
| if (!configMsg) { | ||
| throw new Error('Did not receive exclude patterns configuration message'); | ||
| } | ||
|
|
||
| // Check that default excludes are present | ||
| if (!configMsg.includes('node_modules')) { | ||
| throw new Error('Default excludes should include node_modules'); | ||
| } | ||
| if (!configMsg.includes('.*')) { | ||
| throw new Error('Default excludes should include dotfiles (.*)'); | ||
| } | ||
|
|
||
| fixture.log('✓ Toolkit watch applies default exclude patterns correctly'); | ||
| fixture.log(` Config: ${configMsg.substring(0, 200)}...`); | ||
| } finally { | ||
| await watcher.dispose(); | ||
| await sleep(1000); // Allow async operations to complete | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| function sleep(ms: number): Promise<void> { | ||
| return new Promise(resolve => setTimeout(resolve, ms)); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: including the
.jsfiles is a bad idea for some use cases (say you're also auto-compiling ts changes, now you're double counting). doesn't seem necessary in your test