Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 44f3d5d

Browse files
[flutter_plugin_tools] Migrate firebase-test-lab to new base command (#4116)
Migrates firebase-test-lab to use the new package-looping base command. Other changes: - Extracts several helpers to make the main flow easier to follow - Removes support for finding and running `*_e2e.dart` files, since we no longer use that file structure for integration tests. Part of flutter/flutter#83413
1 parent 0139586 commit 44f3d5d

File tree

3 files changed

+246
-226
lines changed

3 files changed

+246
-226
lines changed

script/tool/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## NEXT
2+
3+
- Modified the output format of many commands
4+
- **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart`
5+
files, only `integration_test/*_test.dart`.
6+
17
## 0.3.0
28

39
- Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of

script/tool/lib/src/firebase_test_lab_command.dart

Lines changed: 132 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,16 @@ import 'package:path/path.dart' as p;
1010
import 'package:uuid/uuid.dart';
1111

1212
import 'common/core.dart';
13-
import 'common/plugin_command.dart';
13+
import 'common/package_looping_command.dart';
1414
import 'common/process_runner.dart';
1515

1616
/// A command to run tests via Firebase test lab.
17-
class FirebaseTestLabCommand extends PluginCommand {
17+
class FirebaseTestLabCommand extends PackageLoopingCommand {
1818
/// Creates an instance of the test runner command.
1919
FirebaseTestLabCommand(
2020
Directory packagesDir, {
2121
ProcessRunner processRunner = const ProcessRunner(),
22-
Print print = print,
23-
}) : _print = print,
24-
super(packagesDir, processRunner: processRunner) {
22+
}) : super(packagesDir, processRunner: processRunner) {
2523
argParser.addOption(
2624
'project',
2725
defaultsTo: 'flutter-infra',
@@ -74,8 +72,6 @@ class FirebaseTestLabCommand extends PluginCommand {
7472

7573
static const String _gradleWrapper = 'gradlew';
7674

77-
final Print _print;
78-
7975
Completer<void>? _firebaseProjectConfigured;
8076

8177
Future<void> _configureFirebaseProject() async {
@@ -86,7 +82,7 @@ class FirebaseTestLabCommand extends PluginCommand {
8682

8783
final String serviceKey = getStringArg('service-key');
8884
if (serviceKey.isEmpty) {
89-
_print('No --service-key provided; skipping gcloud authorization');
85+
print('No --service-key provided; skipping gcloud authorization');
9086
} else {
9187
await processRunner.run(
9288
'gcloud',
@@ -105,183 +101,166 @@ class FirebaseTestLabCommand extends PluginCommand {
105101
getStringArg('project'),
106102
]);
107103
if (exitCode == 0) {
108-
_print('\nFirebase project configured.');
104+
print('\nFirebase project configured.');
109105
return;
110106
} else {
111-
_print(
107+
print(
112108
'\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.');
113109
}
114110
}
115111
_firebaseProjectConfigured!.complete(null);
116112
}
117113

118114
@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+
}
130126

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>[];
137128

138-
final Directory exampleDirectory = package.childDirectory('example');
139-
final String packageName =
140-
p.relative(package.path, from: packagesDir.path);
141-
_print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName');
129+
final Directory exampleDirectory = package.childDirectory('example');
130+
final Directory androidDirectory =
131+
exampleDirectory.childDirectory('android');
142132

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+
}
145138

146-
final String enableExperiment = getStringArg(kEnableExperiment);
147-
final String encodedEnableExperiment =
148-
Uri.encodeComponent('--enable-experiment=$enableExperiment');
139+
await _configureFirebaseProject();
149140

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+
}
161145

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');
166156
continue;
167157
}
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);
168183

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+
}
170191

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',
173202
<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',
180206
],
181207
workingDir: androidDirectory);
182208

183209
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;
194211
}
212+
}
213+
return true;
214+
}
195215

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;
225232

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);
255244

256-
if (exitCode != 0) {
257-
failingPackages.add(packageName);
258-
continue;
259-
}
260-
}
261-
}
245+
if (exitCode != 0) {
246+
return false;
262247
}
248+
return true;
249+
}
263250

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');
280255

281-
if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) {
282-
throw ToolExit(1);
256+
if (!integrationTestDir.existsSync()) {
257+
return;
283258
}
284259

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>();
286265
}
287266
}

0 commit comments

Comments
 (0)