@@ -6,6 +6,25 @@ import { Diagnostic, DiagnosticReason, DestructiveChange, SnapshotVerificationOp
6
6
import { AssemblyManifestReader } from './private/cloud-assembly' ;
7
7
import { IntegRunnerOptions , IntegRunner , DEFAULT_SYNTH_OPTIONS } from './runner-base' ;
8
8
9
+ interface SnapshotAssembly {
10
+ /**
11
+ * Map of stacks that are part of this assembly
12
+ */
13
+ [ stackName : string ] : {
14
+ /**
15
+ * All templates for this stack, including nested stacks
16
+ */
17
+ templates : {
18
+ [ templateId : string ] : any
19
+ } ,
20
+
21
+ /**
22
+ * List of asset Ids that are used by this assembly
23
+ */
24
+ assets : string [ ]
25
+ }
26
+ }
27
+
9
28
/**
10
29
* Runner for snapshot tests. This handles orchestrating
11
30
* the validation of the integration test snapshots
@@ -24,15 +43,7 @@ export class IntegSnapshotRunner extends IntegRunner {
24
43
public testSnapshot ( options : SnapshotVerificationOptions = { } ) : { diagnostics : Diagnostic [ ] , destructiveChanges : DestructiveChange [ ] } {
25
44
let doClean = true ;
26
45
try {
27
- // read the existing snapshot
28
- const expectedStacks = this . readAssembly ( this . snapshotDir ) ;
29
- // only diff stacks that are part of the test case
30
- const expectedStacksToDiff : Record < string , any > = { } ;
31
- for ( const [ stackName , template ] of Object . entries ( expectedStacks ) ) {
32
- if ( this . expectedTestSuite ?. stacks . includes ( stackName ) ) {
33
- expectedStacksToDiff [ stackName ] = template ;
34
- }
35
- }
46
+ const expectedSnapshotAssembly = this . getSnapshotAssembly ( this . snapshotDir , this . expectedTestSuite ?. stacks ) ;
36
47
37
48
// synth the integration test
38
49
// FIXME: ideally we should not need to run this again if
@@ -52,18 +63,10 @@ export class IntegSnapshotRunner extends IntegRunner {
52
63
} ) ;
53
64
54
65
// read the "actual" snapshot
55
- const actualDir = this . cdkOutDir ;
56
- const actualStacks = this . readAssembly ( actualDir ) ;
57
- // only diff stacks that are part of the test case
58
- const actualStacksToDiff : Record < string , any > = { } ;
59
- for ( const [ stackName , template ] of Object . entries ( actualStacks ) ) {
60
- if ( this . actualTestSuite . stacks . includes ( stackName ) ) {
61
- actualStacksToDiff [ stackName ] = template ;
62
- }
63
- }
66
+ const actualSnapshotAssembly = this . getSnapshotAssembly ( this . cdkOutDir , this . actualTestSuite . stacks ) ;
64
67
65
68
// diff the existing snapshot (expected) with the integration test (actual)
66
- const diagnostics = this . diffAssembly ( expectedStacksToDiff , actualStacksToDiff ) ;
69
+ const diagnostics = this . diffAssembly ( expectedSnapshotAssembly , actualSnapshotAssembly ) ;
67
70
68
71
if ( diagnostics . diagnostics . length ) {
69
72
// Attach additional messages to the first diagnostic
@@ -72,7 +75,7 @@ export class IntegSnapshotRunner extends IntegRunner {
72
75
if ( options . retain ) {
73
76
additionalMessages . push (
74
77
`(Failure retained) Expected: ${ path . relative ( process . cwd ( ) , this . snapshotDir ) } ` ,
75
- ` Actual: ${ path . relative ( process . cwd ( ) , actualDir ) } ` ,
78
+ ` Actual: ${ path . relative ( process . cwd ( ) , this . cdkOutDir ) } ` ,
76
79
) ,
77
80
doClean = false ;
78
81
}
@@ -107,6 +110,36 @@ export class IntegSnapshotRunner extends IntegRunner {
107
110
}
108
111
}
109
112
113
+ /**
114
+ * For a given cloud assembly return a collection of all templates
115
+ * that should be part of the snapshot and any required meta data.
116
+ *
117
+ * @param cloudAssemblyDir The directory of the cloud assembly to look for snapshots
118
+ * @param pickStacks Pick only these stacks from the cloud assembly
119
+ * @returns A SnapshotAssembly, the collection of all templates in this snapshot and required meta data
120
+ */
121
+ private getSnapshotAssembly ( cloudAssemblyDir : string , pickStacks : string [ ] = [ ] ) : SnapshotAssembly {
122
+ const assembly = this . readAssembly ( cloudAssemblyDir ) ;
123
+ const stacks = assembly . stacks ;
124
+ const snapshots : SnapshotAssembly = { } ;
125
+ for ( const [ stackName , stackTemplate ] of Object . entries ( stacks ) ) {
126
+ if ( pickStacks . includes ( stackName ) ) {
127
+ const manifest = AssemblyManifestReader . fromPath ( cloudAssemblyDir ) ;
128
+ const assets = manifest . getAssetIdsForStack ( stackName ) ;
129
+
130
+ snapshots [ stackName ] = {
131
+ templates : {
132
+ [ stackName ] : stackTemplate ,
133
+ ...assembly . getNestedStacksForStack ( stackName ) ,
134
+ } ,
135
+ assets,
136
+ } ;
137
+ }
138
+ }
139
+
140
+ return snapshots ;
141
+ }
142
+
110
143
/**
111
144
* For a given stack return all resource types that are allowed to be destroyed
112
145
* as part of a stack update
@@ -131,86 +164,98 @@ export class IntegSnapshotRunner extends IntegRunner {
131
164
* @returns any diagnostics and any destructive changes
132
165
*/
133
166
private diffAssembly (
134
- expected : Record < string , any > ,
135
- actual : Record < string , any > ,
167
+ expected : SnapshotAssembly ,
168
+ actual : SnapshotAssembly ,
136
169
) : { diagnostics : Diagnostic [ ] , destructiveChanges : DestructiveChange [ ] } {
137
170
const failures : Diagnostic [ ] = [ ] ;
138
171
const destructiveChanges : DestructiveChange [ ] = [ ] ;
139
172
140
173
// check if there is a CFN template in the current snapshot
141
174
// that does not exist in the "actual" snapshot
142
- for ( const templateId of Object . keys ( expected ) ) {
143
- if ( ! actual . hasOwnProperty ( templateId ) ) {
144
- failures . push ( {
145
- testName : this . testName ,
146
- reason : DiagnosticReason . SNAPSHOT_FAILED ,
147
- message : `${ templateId } exists in snapshot, but not in actual` ,
148
- } ) ;
175
+ for ( const [ stackId , stack ] of Object . entries ( expected ) ) {
176
+ for ( const templateId of Object . keys ( stack . templates ) ) {
177
+ if ( ! actual [ stackId ] ?. templates [ templateId ] ) {
178
+ failures . push ( {
179
+ testName : this . testName ,
180
+ stackName : templateId ,
181
+ reason : DiagnosticReason . SNAPSHOT_FAILED ,
182
+ message : `${ templateId } exists in snapshot, but not in actual` ,
183
+ } ) ;
184
+ }
149
185
}
150
186
}
151
187
152
- for ( const templateId of Object . keys ( actual ) ) {
188
+ for ( const [ stackId , stack ] of Object . entries ( actual ) ) {
189
+ for ( const templateId of Object . keys ( stack . templates ) ) {
153
190
// check if there is a CFN template in the "actual" snapshot
154
191
// that does not exist in the current snapshot
155
- if ( ! expected . hasOwnProperty ( templateId ) ) {
156
- failures . push ( {
157
- testName : this . testName ,
158
- reason : DiagnosticReason . SNAPSHOT_FAILED ,
159
- message : `${ templateId } does not exist in snapshot, but does in actual` ,
160
- } ) ;
161
- continue ;
162
- } else {
163
- let actualTemplate = actual [ templateId ] ;
164
- let expectedTemplate = expected [ templateId ] ;
165
- const allowedDestroyTypes = this . getAllowedDestroyTypesForStack ( templateId ) ?? [ ] ;
166
-
167
- // if we are not verifying asset hashes then remove the specific
168
- // asset hashes from the templates so they are not part of the diff
169
- // comparison
170
- if ( ! this . actualTestSuite . getOptionsForStack ( templateId ) ?. diffAssets ) {
171
- actualTemplate = this . canonicalizeTemplate ( actualTemplate , templateId , this . cdkOutDir ) ;
172
- expectedTemplate = this . canonicalizeTemplate ( expectedTemplate , templateId , this . snapshotDir ) ;
173
- }
174
- const templateDiff = diffTemplate ( expectedTemplate , actualTemplate ) ;
175
- if ( ! templateDiff . isEmpty ) {
176
- // go through all the resource differences and check for any
177
- // "destructive" changes
178
- templateDiff . resources . forEachDifference ( ( logicalId : string , change : ResourceDifference ) => {
192
+ if ( ! expected [ stackId ] ?. templates [ templateId ] ) {
193
+ failures . push ( {
194
+ testName : this . testName ,
195
+ stackName : templateId ,
196
+ reason : DiagnosticReason . SNAPSHOT_FAILED ,
197
+ message : `${ templateId } does not exist in snapshot, but does in actual` ,
198
+ } ) ;
199
+ continue ;
200
+ } else {
201
+ const config = {
202
+ diffAssets : this . actualTestSuite . getOptionsForStack ( stackId ) ?. diffAssets ,
203
+ } ;
204
+ let actualTemplate = actual [ stackId ] . templates [ templateId ] ;
205
+ let expectedTemplate = expected [ stackId ] . templates [ templateId ] ;
206
+
207
+ // if we are not verifying asset hashes then remove the specific
208
+ // asset hashes from the templates so they are not part of the diff
209
+ // comparison
210
+ if ( ! config . diffAssets ) {
211
+ actualTemplate = this . canonicalizeTemplate ( actualTemplate , actual [ stackId ] . assets ) ;
212
+ expectedTemplate = this . canonicalizeTemplate ( expectedTemplate , expected [ stackId ] . assets ) ;
213
+ }
214
+ const templateDiff = diffTemplate ( expectedTemplate , actualTemplate ) ;
215
+ if ( ! templateDiff . isEmpty ) {
216
+ const allowedDestroyTypes = this . getAllowedDestroyTypesForStack ( stackId ) ?? [ ] ;
217
+
218
+ // go through all the resource differences and check for any
219
+ // "destructive" changes
220
+ templateDiff . resources . forEachDifference ( ( logicalId : string , change : ResourceDifference ) => {
179
221
// if the change is a removal it will not show up as a 'changeImpact'
180
222
// so need to check for it separately, unless it is a resourceType that
181
223
// has been "allowed" to be destroyed
182
- const resourceType = change . oldValue ?. Type ?? change . newValue ?. Type ;
183
- if ( resourceType && allowedDestroyTypes . includes ( resourceType ) ) {
184
- return ;
185
- }
186
- if ( change . isRemoval ) {
187
- destructiveChanges . push ( {
188
- impact : ResourceImpact . WILL_DESTROY ,
189
- logicalId,
190
- stackName : templateId ,
191
- } ) ;
192
- } else {
193
- switch ( change . changeImpact ) {
194
- case ResourceImpact . MAY_REPLACE :
195
- case ResourceImpact . WILL_ORPHAN :
196
- case ResourceImpact . WILL_DESTROY :
197
- case ResourceImpact . WILL_REPLACE :
198
- destructiveChanges . push ( {
199
- impact : change . changeImpact ,
200
- logicalId,
201
- stackName : templateId ,
202
- } ) ;
203
- break ;
224
+ const resourceType = change . oldValue ?. Type ?? change . newValue ?. Type ;
225
+ if ( resourceType && allowedDestroyTypes . includes ( resourceType ) ) {
226
+ return ;
204
227
}
205
- }
206
- } ) ;
207
- const writable = new StringWritable ( { } ) ;
208
- formatDifferences ( writable , templateDiff ) ;
209
- failures . push ( {
210
- reason : DiagnosticReason . SNAPSHOT_FAILED ,
211
- message : writable . data ,
212
- testName : this . testName ,
213
- } ) ;
228
+ if ( change . isRemoval ) {
229
+ destructiveChanges . push ( {
230
+ impact : ResourceImpact . WILL_DESTROY ,
231
+ logicalId,
232
+ stackName : templateId ,
233
+ } ) ;
234
+ } else {
235
+ switch ( change . changeImpact ) {
236
+ case ResourceImpact . MAY_REPLACE :
237
+ case ResourceImpact . WILL_ORPHAN :
238
+ case ResourceImpact . WILL_DESTROY :
239
+ case ResourceImpact . WILL_REPLACE :
240
+ destructiveChanges . push ( {
241
+ impact : change . changeImpact ,
242
+ logicalId,
243
+ stackName : templateId ,
244
+ } ) ;
245
+ break ;
246
+ }
247
+ }
248
+ } ) ;
249
+ const writable = new StringWritable ( { } ) ;
250
+ formatDifferences ( writable , templateDiff ) ;
251
+ failures . push ( {
252
+ reason : DiagnosticReason . SNAPSHOT_FAILED ,
253
+ message : writable . data ,
254
+ stackName : templateId ,
255
+ testName : this . testName ,
256
+ config,
257
+ } ) ;
258
+ }
214
259
}
215
260
}
216
261
}
@@ -221,11 +266,8 @@ export class IntegSnapshotRunner extends IntegRunner {
221
266
} ;
222
267
}
223
268
224
- private readAssembly ( dir : string ) : Record < string , any > {
225
- const assembly = AssemblyManifestReader . fromPath ( dir ) ;
226
- const stacks = assembly . stacks ;
227
-
228
- return stacks ;
269
+ private readAssembly ( dir : string ) : AssemblyManifestReader {
270
+ return AssemblyManifestReader . fromPath ( dir ) ;
229
271
}
230
272
231
273
/**
@@ -234,7 +276,7 @@ export class IntegSnapshotRunner extends IntegRunner {
234
276
* This makes it possible to compare templates if all that's different between
235
277
* them is the hashes of the asset values.
236
278
*/
237
- private canonicalizeTemplate ( template : any , stackName : string , manifestDir : string ) : any {
279
+ private canonicalizeTemplate ( template : any , assets : string [ ] ) : any {
238
280
const assetsSeen = new Set < string > ( ) ;
239
281
const stringSubstitutions = new Array < [ RegExp , string ] > ( ) ;
240
282
@@ -262,8 +304,6 @@ export class IntegSnapshotRunner extends IntegRunner {
262
304
263
305
// find assets defined in the asset manifest
264
306
try {
265
- const manifest = AssemblyManifestReader . fromPath ( manifestDir ) ;
266
- const assets = manifest . getAssetIdsForStack ( stackName ) ;
267
307
assets . forEach ( asset => {
268
308
if ( ! assetsSeen . has ( asset ) ) {
269
309
assetsSeen . add ( asset ) ;
0 commit comments