@@ -10,18 +10,16 @@ import 'package:path/path.dart' as p;
10
10
import 'package:uuid/uuid.dart' ;
11
11
12
12
import 'common/core.dart' ;
13
- import 'common/plugin_command .dart' ;
13
+ import 'common/package_looping_command .dart' ;
14
14
import 'common/process_runner.dart' ;
15
15
16
16
/// A command to run tests via Firebase test lab.
17
- class FirebaseTestLabCommand extends PluginCommand {
17
+ class FirebaseTestLabCommand extends PackageLoopingCommand {
18
18
/// Creates an instance of the test runner command.
19
19
FirebaseTestLabCommand (
20
20
Directory packagesDir, {
21
21
ProcessRunner processRunner = const ProcessRunner (),
22
- Print print = print,
23
- }) : _print = print,
24
- super (packagesDir, processRunner: processRunner) {
22
+ }) : super (packagesDir, processRunner: processRunner) {
25
23
argParser.addOption (
26
24
'project' ,
27
25
defaultsTo: 'flutter-infra' ,
@@ -74,8 +72,6 @@ class FirebaseTestLabCommand extends PluginCommand {
74
72
75
73
static const String _gradleWrapper = 'gradlew' ;
76
74
77
- final Print _print;
78
-
79
75
Completer <void >? _firebaseProjectConfigured;
80
76
81
77
Future <void > _configureFirebaseProject () async {
@@ -86,7 +82,7 @@ class FirebaseTestLabCommand extends PluginCommand {
86
82
87
83
final String serviceKey = getStringArg ('service-key' );
88
84
if (serviceKey.isEmpty) {
89
- _print ('No --service-key provided; skipping gcloud authorization' );
85
+ print ('No --service-key provided; skipping gcloud authorization' );
90
86
} else {
91
87
await processRunner.run (
92
88
'gcloud' ,
@@ -105,183 +101,166 @@ class FirebaseTestLabCommand extends PluginCommand {
105
101
getStringArg ('project' ),
106
102
]);
107
103
if (exitCode == 0 ) {
108
- _print ('\n Firebase project configured.' );
104
+ print ('\n Firebase project configured.' );
109
105
return ;
110
106
} else {
111
- _print (
107
+ print (
112
108
'\n Warning: gcloud config set returned a non-zero exit code. Continuing anyway.' );
113
109
}
114
110
}
115
111
_firebaseProjectConfigured! .complete (null );
116
112
}
117
113
118
114
@override
119
- Future <void > run ( ) async {
120
- final Stream < Directory > packagesWithTests = getPackages (). where (
121
- ( Directory d) =>
122
- isFlutterPackage (d) &&
123
- d
124
- .childDirectory ('example ' )
125
- .childDirectory ('android ' )
126
- . childDirectory ( 'app' )
127
- . childDirectory ( 'src' )
128
- . childDirectory ( 'androidTest' )
129
- . existsSync ());
115
+ Future <List < String >> runForPackage ( Directory package ) async {
116
+ if ( ! package
117
+ . childDirectory ( 'example' )
118
+ . childDirectory ( 'android' )
119
+ . childDirectory ( 'app' )
120
+ .childDirectory ('src ' )
121
+ .childDirectory ('androidTest ' )
122
+ . existsSync ()) {
123
+ printSkip ( 'No example with androidTest directory' );
124
+ return PackageLoopingCommand .success;
125
+ }
130
126
131
- final List <String > failingPackages = < String > [];
132
- final List <String > missingFlutterBuild = < String > [];
133
- int resultsCounter =
134
- 0 ; // We use a unique GCS bucket for each Firebase Test Lab run
135
- await for (final Directory package in packagesWithTests) {
136
- // See https://github.com/flutter/flutter/issues/38983
127
+ final List <String > errors = < String > [];
137
128
138
- final Directory exampleDirectory = package.childDirectory ('example' );
139
- final String packageName =
140
- p.relative (package.path, from: packagesDir.path);
141
- _print ('\n RUNNING FIREBASE TEST LAB TESTS for $packageName ' );
129
+ final Directory exampleDirectory = package.childDirectory ('example' );
130
+ final Directory androidDirectory =
131
+ exampleDirectory.childDirectory ('android' );
142
132
143
- final Directory androidDirectory =
144
- exampleDirectory.childDirectory ('android' );
133
+ // Ensures that gradle wrapper exists
134
+ if (! await _ensureGradleWrapperExists (androidDirectory)) {
135
+ errors.add ('Unable to build example apk' );
136
+ return errors;
137
+ }
145
138
146
- final String enableExperiment = getStringArg (kEnableExperiment);
147
- final String encodedEnableExperiment =
148
- Uri .encodeComponent ('--enable-experiment=$enableExperiment ' );
139
+ await _configureFirebaseProject ();
149
140
150
- // Ensures that gradle wrapper exists
151
- if (! androidDirectory.childFile (_gradleWrapper).existsSync ()) {
152
- final int exitCode = await processRunner.runAndStream (
153
- 'flutter' ,
154
- < String > [
155
- 'build' ,
156
- 'apk' ,
157
- if (enableExperiment.isNotEmpty)
158
- '--enable-experiment=$enableExperiment ' ,
159
- ],
160
- workingDir: androidDirectory);
141
+ if (! await _runGradle (androidDirectory, 'app:assembleAndroidTest' )) {
142
+ errors.add ('Unable to assemble androidTest' );
143
+ return errors;
144
+ }
161
145
162
- if (exitCode != 0 ) {
163
- failingPackages.add (packageName);
164
- continue ;
165
- }
146
+ // Used within the loop to ensure a unique GCS output location for each
147
+ // test file's run.
148
+ int resultsCounter = 0 ;
149
+ for (final File test in _findIntegrationTestFiles (package)) {
150
+ final String testName = p.relative (test.path, from: package.path);
151
+ print ('Testing $testName ...' );
152
+ if (! await _runGradle (androidDirectory, 'app:assembleDebug' ,
153
+ testFile: test)) {
154
+ printError ('Could not build $testName ' );
155
+ errors.add ('$testName failed to build' );
166
156
continue ;
167
157
}
158
+ final String buildId = getStringArg ('build-id' );
159
+ final String testRunId = getStringArg ('test-run-id' );
160
+ final String resultsDir =
161
+ 'plugins_android_test/${getPackageDescription (package )}/$buildId /$testRunId /${resultsCounter ++}/' ;
162
+ final List <String > args = < String > [
163
+ 'firebase' ,
164
+ 'test' ,
165
+ 'android' ,
166
+ 'run' ,
167
+ '--type' ,
168
+ 'instrumentation' ,
169
+ '--app' ,
170
+ 'build/app/outputs/apk/debug/app-debug.apk' ,
171
+ '--test' ,
172
+ 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk' ,
173
+ '--timeout' ,
174
+ '5m' ,
175
+ '--results-bucket=${getStringArg ('results-bucket' )}' ,
176
+ '--results-dir=$resultsDir ' ,
177
+ ];
178
+ for (final String device in getStringListArg ('device' )) {
179
+ args.addAll (< String > ['--device' , device]);
180
+ }
181
+ final int exitCode = await processRunner.runAndStream ('gcloud' , args,
182
+ workingDir: exampleDirectory);
168
183
169
- await _configureFirebaseProject ();
184
+ if (exitCode != 0 ) {
185
+ printError ('Test failure for $testName ' );
186
+ errors.add ('$testName failed tests' );
187
+ }
188
+ }
189
+ return errors;
190
+ }
170
191
171
- int exitCode = await processRunner.runAndStream (
172
- p.join (androidDirectory.path, _gradleWrapper),
192
+ /// Checks that 'gradlew' exists in [androidDirectory] , and if not runs a
193
+ /// Flutter build to generate it.
194
+ ///
195
+ /// Returns true if either gradlew was already present, or the build succeeds.
196
+ Future <bool > _ensureGradleWrapperExists (Directory androidDirectory) async {
197
+ if (! androidDirectory.childFile (_gradleWrapper).existsSync ()) {
198
+ print ('Running flutter build apk...' );
199
+ final String experiment = getStringArg (kEnableExperiment);
200
+ final int exitCode = await processRunner.runAndStream (
201
+ 'flutter' ,
173
202
< String > [
174
- 'app:assembleAndroidTest' ,
175
- '-Pverbose=true' ,
176
- if (enableExperiment.isNotEmpty)
177
- '-Pextra-front-end-options=$encodedEnableExperiment ' ,
178
- if (enableExperiment.isNotEmpty)
179
- '-Pextra-gen-snapshot-options=$encodedEnableExperiment ' ,
203
+ 'build' ,
204
+ 'apk' ,
205
+ if (experiment.isNotEmpty) '--enable-experiment=$experiment ' ,
180
206
],
181
207
workingDir: androidDirectory);
182
208
183
209
if (exitCode != 0 ) {
184
- failingPackages.add (packageName);
185
- continue ;
186
- }
187
-
188
- // Look for tests recursively in folders that start with 'test' and that
189
- // live in the root or example folders.
190
- bool isTestDir (FileSystemEntity dir) {
191
- return dir is Directory &&
192
- (p.basename (dir.path).startsWith ('test' ) ||
193
- p.basename (dir.path) == 'integration_test' );
210
+ return false ;
194
211
}
212
+ }
213
+ return true ;
214
+ }
195
215
196
- final List <Directory > testDirs =
197
- package.listSync ().where (isTestDir).cast <Directory >().toList ();
198
- final Directory example = package.childDirectory ('example' );
199
- testDirs.addAll (
200
- example.listSync ().where (isTestDir).cast <Directory >().toList ());
201
- for (final Directory testDir in testDirs) {
202
- bool isE2ETest (FileSystemEntity file) {
203
- return file.path.endsWith ('_e2e.dart' ) ||
204
- (file.parent.basename == 'integration_test' &&
205
- file.path.endsWith ('_test.dart' ));
206
- }
207
-
208
- final List <FileSystemEntity > testFiles = testDir
209
- .listSync (recursive: true , followLinks: true )
210
- .where (isE2ETest)
211
- .toList ();
212
- for (final FileSystemEntity test in testFiles) {
213
- exitCode = await processRunner.runAndStream (
214
- p.join (androidDirectory.path, _gradleWrapper),
215
- < String > [
216
- 'app:assembleDebug' ,
217
- '-Pverbose=true' ,
218
- '-Ptarget=${test .path }' ,
219
- if (enableExperiment.isNotEmpty)
220
- '-Pextra-front-end-options=$encodedEnableExperiment ' ,
221
- if (enableExperiment.isNotEmpty)
222
- '-Pextra-gen-snapshot-options=$encodedEnableExperiment ' ,
223
- ],
224
- workingDir: androidDirectory);
216
+ /// Builds [target] using 'gradlew' in the given [directory] . Assumes
217
+ /// 'gradlew' already exists.
218
+ ///
219
+ /// [testFile] optionally does the Flutter build with the given test file as
220
+ /// the build target.
221
+ ///
222
+ /// Returns true if the command succeeds.
223
+ Future <bool > _runGradle (
224
+ Directory directory,
225
+ String target, {
226
+ File ? testFile,
227
+ }) async {
228
+ final String experiment = getStringArg (kEnableExperiment);
229
+ final String ? extraOptions = experiment.isNotEmpty
230
+ ? Uri .encodeComponent ('--enable-experiment=$experiment ' )
231
+ : null ;
225
232
226
- if (exitCode != 0 ) {
227
- failingPackages.add (packageName);
228
- continue ;
229
- }
230
- final String buildId = getStringArg ('build-id' );
231
- final String testRunId = getStringArg ('test-run-id' );
232
- final String resultsDir =
233
- 'plugins_android_test/$packageName /$buildId /$testRunId /${resultsCounter ++}/' ;
234
- final List <String > args = < String > [
235
- 'firebase' ,
236
- 'test' ,
237
- 'android' ,
238
- 'run' ,
239
- '--type' ,
240
- 'instrumentation' ,
241
- '--app' ,
242
- 'build/app/outputs/apk/debug/app-debug.apk' ,
243
- '--test' ,
244
- 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk' ,
245
- '--timeout' ,
246
- '5m' ,
247
- '--results-bucket=${getStringArg ('results-bucket' )}' ,
248
- '--results-dir=$resultsDir ' ,
249
- ];
250
- for (final String device in getStringListArg ('device' )) {
251
- args.addAll (< String > ['--device' , device]);
252
- }
253
- exitCode = await processRunner.runAndStream ('gcloud' , args,
254
- workingDir: exampleDirectory);
233
+ final int exitCode = await processRunner.runAndStream (
234
+ p.join (directory.path, _gradleWrapper),
235
+ < String > [
236
+ target,
237
+ '-Pverbose=true' ,
238
+ if (testFile != null ) '-Ptarget=${testFile .path }' ,
239
+ if (extraOptions != null ) '-Pextra-front-end-options=$extraOptions ' ,
240
+ if (extraOptions != null )
241
+ '-Pextra-gen-snapshot-options=$extraOptions ' ,
242
+ ],
243
+ workingDir: directory);
255
244
256
- if (exitCode != 0 ) {
257
- failingPackages.add (packageName);
258
- continue ;
259
- }
260
- }
261
- }
245
+ if (exitCode != 0 ) {
246
+ return false ;
262
247
}
248
+ return true ;
249
+ }
263
250
264
- _print ('\n\n ' );
265
- if (failingPackages.isNotEmpty) {
266
- _print (
267
- 'The instrumentation tests for the following packages are failing (see above for'
268
- 'details):' );
269
- for (final String package in failingPackages) {
270
- _print (' * $package ' );
271
- }
272
- }
273
- if (missingFlutterBuild.isNotEmpty) {
274
- _print ('Run "pub global run flutter_plugin_tools build-examples --apk" on'
275
- 'the following packages before executing tests again:' );
276
- for (final String package in missingFlutterBuild) {
277
- _print (' * $package ' );
278
- }
279
- }
251
+ /// Finds and returns all integration test files for [package] .
252
+ Iterable <File > _findIntegrationTestFiles (Directory package) sync * {
253
+ final Directory integrationTestDir =
254
+ package.childDirectory ('example' ).childDirectory ('integration_test' );
280
255
281
- if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty ) {
282
- throw ToolExit ( 1 ) ;
256
+ if (! integrationTestDir. existsSync () ) {
257
+ return ;
283
258
}
284
259
285
- _print ('All Firebase Test Lab tests successful!' );
260
+ yield * integrationTestDir
261
+ .listSync (recursive: true , followLinks: true )
262
+ .where ((FileSystemEntity file) =>
263
+ file is File && file.basename.endsWith ('_test.dart' ))
264
+ .cast <File >();
286
265
}
287
266
}
0 commit comments