@@ -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
+ parseTestDevices ,
21
+ parseIntoStringArray ,
21
22
} from "../appdistribution/options-parser-util" ;
22
23
23
24
const TEST_MAX_POLLING_RETRIES = 40 ;
@@ -35,19 +36,21 @@ 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 and optionally distribute it to testers and 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" )
42
- . option ( "--testers <string>" , "a comma separated list of tester emails to distribute to" )
45
+ . option ( "--testers <string>" , "a comma- separated list of tester emails to distribute to" )
43
46
. option (
44
47
"--testers-file <file>" ,
45
- "path to file with a comma separated list of tester emails to distribute to" ,
48
+ "path to file with a comma- or newline- separated list of tester emails to distribute to" ,
46
49
)
47
- . option ( "--groups <string>" , "a comma separated list of group aliases to distribute to" )
50
+ . option ( "--groups <string>" , "a comma- separated list of group aliases to distribute to" )
48
51
. option (
49
52
"--groups-file <file>" ,
50
- "path to file with a comma separated list of group aliases to distribute to" ,
53
+ "path to file with a comma- or newline- separated list of group aliases to distribute to" ,
51
54
)
52
55
. option (
53
56
"--test-devices <string>" ,
@@ -75,14 +78,25 @@ 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- or newline-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 = parseIntoStringArray ( options . testers , options . testersFile ) ;
92
+ const groups = parseIntoStringArray ( options . groups , options . groupsFile ) ;
93
+ const testCases = parseIntoStringArray ( options . testCaseIds , options . testCaseIdsFile ) ;
94
+ const testDevices = parseTestDevices ( options . testDevices , options . testDevicesFile ) ;
95
+ if ( testCases . length && ( options . testUsernameResource || options . testPasswordResource ) ) {
96
+ throw new FirebaseError (
97
+ "Password and username resource names are not supported for the AI testing agent." ,
98
+ ) ;
99
+ }
86
100
const loginCredential = getLoginCredential ( {
87
101
username : options . testUsername ,
88
102
password : options . testPassword ,
@@ -210,56 +224,78 @@ export const command = new Command("appdistribution:distribute <release-binary-f
210
224
await requests . distribute ( releaseName , testers , groups ) ;
211
225
212
226
// 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` ) ;
227
+ if ( testDevices . length ) {
228
+ utils . logBullet ( "starting automated test (note: this feature is in beta)" ) ;
229
+ const releaseTestPromises : Promise < ReleaseTest > [ ] = [ ] ;
230
+ if ( ! testCases . length ) {
231
+ // fallback to basic automated test
232
+ releaseTestPromises . push (
233
+ requests . createReleaseTest ( releaseName , testDevices , loginCredential ) ,
234
+ ) ;
235
+ } else {
236
+ for ( const testCaseId of testCases ) {
237
+ releaseTestPromises . push (
238
+ requests . createReleaseTest (
239
+ releaseName ,
240
+ testDevices ,
241
+ loginCredential ,
242
+ `${ appName } /testCases/${ testCaseId } ` ,
243
+ ) ,
244
+ ) ;
245
+ }
246
+ }
247
+ const releaseTests = await Promise . all ( releaseTestPromises ) ;
248
+ utils . logSuccess ( `${ releaseTests . length } release test(s) started successfully` ) ;
221
249
if ( ! options . testNonBlocking ) {
222
- await awaitTestResults ( releaseTest . name ! , requests ) ;
250
+ await awaitTestResults ( releaseTests , requests ) ;
223
251
}
224
252
}
225
253
} ) ;
226
254
227
255
async function awaitTestResults (
228
- releaseTestName : string ,
256
+ releaseTests : ReleaseTest [ ] ,
229
257
requests : AppDistributionClient ,
230
258
) : Promise < void > {
259
+ const releaseTestNames = new Set ( releaseTests . map ( ( rt ) => rt . name ! ) ) ;
231
260
for ( let i = 0 ; i < TEST_MAX_POLLING_RETRIES ; i ++ ) {
232
- utils . logBullet ( "the automated tests results are pending" ) ;
261
+ utils . logBullet ( ` ${ releaseTestNames . size } automated test results are pending...` ) ;
233
262
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" :
263
+ for ( const releaseTestName of releaseTestNames ) {
264
+ const releaseTest = await requests . getReleaseTest ( releaseTestName ) ;
265
+ if ( releaseTest . deviceExecutions . every ( ( e ) => e . state === "PASSED" ) ) {
266
+ releaseTestNames . delete ( releaseTestName ) ;
267
+ if ( releaseTestNames . size === 0 ) {
268
+ utils . logSuccess ( "Automated test(s) passed!" ) ;
269
+ return ;
270
+ } else {
243
271
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
- ) ;
272
+ }
273
+ }
274
+ for ( const execution of releaseTest . deviceExecutions ) {
275
+ switch ( execution . state ) {
276
+ case "PASSED" :
277
+ case "IN_PROGRESS" :
278
+ continue ;
279
+ case "FAILED" :
280
+ throw new FirebaseError (
281
+ `Automated test failed for ${ deviceToString ( execution . device ) } : ${ execution . failedReason } ` ,
282
+ { exit : 1 } ,
283
+ ) ;
284
+ case "INCONCLUSIVE" :
285
+ throw new FirebaseError (
286
+ `Automated test inconclusive for ${ deviceToString ( execution . device ) } : ${ execution . inconclusiveReason } ` ,
287
+ { exit : 1 } ,
288
+ ) ;
289
+ default :
290
+ throw new FirebaseError (
291
+ `Unsupported automated test state for ${ deviceToString ( execution . device ) } : ${ execution . state } ` ,
292
+ { exit : 1 } ,
293
+ ) ;
294
+ }
259
295
}
260
296
}
261
297
}
262
- throw new FirebaseError ( "It took longer than expected to process your test, please try again." , {
298
+ throw new FirebaseError ( "It took longer than expected to run your test(s) , please try again." , {
263
299
exit : 1 ,
264
300
} ) ;
265
301
}
0 commit comments