@@ -9,15 +9,16 @@ import {
9
9
IntegrationState ,
10
10
UploadReleaseResult ,
11
11
TestDevice ,
12
+ ReleaseTest ,
12
13
} from "../appdistribution/types" ;
13
14
import { FirebaseError , getErrMsg , getErrStatus } from "../error" ;
14
15
import { Distribution , DistributionFileType } from "../appdistribution/distribution" ;
15
16
import {
16
17
ensureFileExists ,
17
18
getAppName ,
18
19
getLoginCredential ,
19
- getTestDevices ,
20
- getTestersOrGroups ,
20
+ parseTestDevicesFromStringOrFile ,
21
+ parseCommaSeparatedStringOrFile ,
21
22
} from "../appdistribution/options-parser-util" ;
22
23
23
24
const TEST_MAX_POLLING_RETRIES = 40 ;
@@ -35,7 +36,9 @@ function getReleaseNotes(releaseNotes: string, releaseNotesFile: string): string
35
36
}
36
37
37
38
export const command = new Command ( "appdistribution:distribute <release-binary-file>" )
38
- . description ( "upload a release binary" )
39
+ . description (
40
+ "upload a release binary, optionally distribute it to testers and/or run automated tests" ,
41
+ )
39
42
. option ( "--app <app_id>" , "the app id of your Firebase app" )
40
43
. option ( "--release-notes <string>" , "release notes to include" )
41
44
. option ( "--release-notes-file <file>" , "path to file with release notes" )
@@ -75,14 +78,28 @@ export const command = new Command("appdistribution:distribute <release-binary-f
75
78
"--test-non-blocking" ,
76
79
"run automated tests without waiting for them to complete. Visit the Firebase console for the test results." ,
77
80
)
81
+ . option ( "--test-case-ids <string>" , "a comma separated list of test case IDs." )
82
+ . option (
83
+ "--test-case-ids-file <file>" ,
84
+ "path to file with a comma separated list of test case IDs." ,
85
+ )
78
86
. before ( requireAuth )
79
87
. action ( async ( file : string , options : any ) => {
80
88
const appName = getAppName ( options ) ;
81
89
const distribution = new Distribution ( file ) ;
82
90
const releaseNotes = getReleaseNotes ( options . releaseNotes , options . releaseNotesFile ) ;
83
- const testers = getTestersOrGroups ( options . testers , options . testersFile ) ;
84
- const groups = getTestersOrGroups ( options . groups , options . groupsFile ) ;
85
- const testDevices = getTestDevices ( options . testDevices , options . testDevicesFile ) ;
91
+ const testers = parseCommaSeparatedStringOrFile ( options . testers , options . testersFile ) ;
92
+ const groups = parseCommaSeparatedStringOrFile ( options . groups , options . groupsFile ) ;
93
+ const testCases = parseCommaSeparatedStringOrFile ( options . testCaseIds , options . testCaseIdsFile ) ;
94
+ const testDevices = parseTestDevicesFromStringOrFile (
95
+ options . testDevices ,
96
+ options . testDevicesFile ,
97
+ ) ;
98
+ if ( testCases . length && ( options . testUsernameResource || options . testPasswordResource ) ) {
99
+ throw new FirebaseError (
100
+ "Username and password resource names are not supported for the AI testing agent." ,
101
+ ) ;
102
+ }
86
103
const loginCredential = getLoginCredential ( {
87
104
username : options . testUsername ,
88
105
password : options . testPassword ,
@@ -210,56 +227,78 @@ export const command = new Command("appdistribution:distribute <release-binary-f
210
227
await requests . distribute ( releaseName , testers , groups ) ;
211
228
212
229
// Run automated tests
213
- if ( testDevices ?. length ) {
214
- utils . logBullet ( "starting automated tests (note: this feature is in beta)" ) ;
215
- const releaseTest = await requests . createReleaseTest (
216
- releaseName ,
217
- testDevices ,
218
- loginCredential ,
219
- ) ;
220
- utils . logSuccess ( `Release test created successfully` ) ;
230
+ if ( testDevices . length ) {
231
+ utils . logBullet ( "starting automated test (note: this feature is in beta)" ) ;
232
+ const releaseTestPromises : Promise < ReleaseTest > [ ] = [ ] ;
233
+ if ( ! testCases . length ) {
234
+ // fallback to basic automated test
235
+ releaseTestPromises . push (
236
+ requests . createReleaseTest ( releaseName , testDevices , loginCredential ) ,
237
+ ) ;
238
+ } else {
239
+ for ( const testCaseId of testCases ) {
240
+ releaseTestPromises . push (
241
+ requests . createReleaseTest (
242
+ releaseName ,
243
+ testDevices ,
244
+ loginCredential ,
245
+ `${ appName } /testCases/${ testCaseId } ` ,
246
+ ) ,
247
+ ) ;
248
+ }
249
+ }
250
+ const releaseTests = await Promise . all ( releaseTestPromises ) ;
251
+ utils . logSuccess ( `${ releaseTests . length } Release test(s) started successfully` ) ;
221
252
if ( ! options . testNonBlocking ) {
222
- await awaitTestResults ( releaseTest . name ! , requests ) ;
253
+ const releaseTestNames = new Set ( releaseTests . map ( ( rt ) => rt . name ! ! ) ) ;
254
+ await awaitTestResults ( releaseTestNames , requests ) ;
223
255
}
224
256
}
225
257
} ) ;
226
258
227
259
async function awaitTestResults (
228
- releaseTestName : string ,
260
+ releaseTestNames : Set < string > ,
229
261
requests : AppDistributionClient ,
230
262
) : Promise < void > {
231
263
for ( let i = 0 ; i < TEST_MAX_POLLING_RETRIES ; i ++ ) {
232
- utils . logBullet ( "the automated tests results are pending" ) ;
264
+ utils . logBullet ( "the automated test results are pending" ) ;
233
265
await delay ( TEST_POLLING_INTERVAL_MILLIS ) ;
234
- const releaseTest = await requests . getReleaseTest ( releaseTestName ) ;
235
- if ( releaseTest . deviceExecutions . every ( ( e ) => e . state === "PASSED" ) ) {
236
- utils . logSuccess ( "automated test(s) passed!" ) ;
237
- return ;
238
- }
239
- for ( const execution of releaseTest . deviceExecutions ) {
240
- switch ( execution . state ) {
241
- case "PASSED" :
242
- case "IN_PROGRESS" :
266
+ for ( const releaseTestName of releaseTestNames ) {
267
+ const releaseTest = await requests . getReleaseTest ( releaseTestName ) ;
268
+ if ( releaseTest . deviceExecutions . every ( ( e ) => e . state === "PASSED" ) ) {
269
+ releaseTestNames . delete ( releaseTestName ) ;
270
+ if ( releaseTestNames . size === 0 ) {
271
+ utils . logSuccess ( "automated test(s) passed!" ) ;
272
+ return ;
273
+ } else {
243
274
continue ;
244
- case "FAILED" :
245
- throw new FirebaseError (
246
- `Automated test failed for ${ deviceToString ( execution . device ) } : ${ execution . failedReason } ` ,
247
- { exit : 1 } ,
248
- ) ;
249
- case "INCONCLUSIVE" :
250
- throw new FirebaseError (
251
- `Automated test inconclusive for ${ deviceToString ( execution . device ) } : ${ execution . inconclusiveReason } ` ,
252
- { exit : 1 } ,
253
- ) ;
254
- default :
255
- throw new FirebaseError (
256
- `Unsupported automated test state for ${ deviceToString ( execution . device ) } : ${ execution . state } ` ,
257
- { exit : 1 } ,
258
- ) ;
275
+ }
276
+ }
277
+ for ( const execution of releaseTest . deviceExecutions ) {
278
+ switch ( execution . state ) {
279
+ case "PASSED" :
280
+ case "IN_PROGRESS" :
281
+ continue ;
282
+ case "FAILED" :
283
+ throw new FirebaseError (
284
+ `Automated test failed for ${ deviceToString ( execution . device ) } : ${ execution . failedReason } ` ,
285
+ { exit : 1 } ,
286
+ ) ;
287
+ case "INCONCLUSIVE" :
288
+ throw new FirebaseError (
289
+ `Automated test inconclusive for ${ deviceToString ( execution . device ) } : ${ execution . inconclusiveReason } ` ,
290
+ { exit : 1 } ,
291
+ ) ;
292
+ default :
293
+ throw new FirebaseError (
294
+ `Unsupported automated test state for ${ deviceToString ( execution . device ) } : ${ execution . state } ` ,
295
+ { exit : 1 } ,
296
+ ) ;
297
+ }
259
298
}
260
299
}
261
300
}
262
- throw new FirebaseError ( "It took longer than expected to process your test, please try again." , {
301
+ throw new FirebaseError ( "It took longer than expected to run your test(s) , please try again." , {
263
302
exit : 1 ,
264
303
} ) ;
265
304
}
0 commit comments