Skip to content

Commit 44d7c07

Browse files
[flutter_plugin_tools] Make firebase-test-lab fail when no tests run (flutter#4172)
If a package supports Android, it will now report failure instead of skip if no tests run. This matches the new behavior of drive-examples, and is intended to prevent recurrance of situations where we are silently failing to run tests because of, e.g., tests being in the wrong directory. Also fixes a long-standing but unnoticed problem where if a run tried to run more than one package's tests, it would hang forever (although on the bots it doesn't seem to time out, just end logs abruptly) due to a logic error in the call to configure gcloud. Fixes flutter#86732
1 parent 09ec519 commit 44d7c07

File tree

4 files changed

+202
-18
lines changed

4 files changed

+202
-18
lines changed

.cirrus.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ task:
137137
CHANNEL: "stable"
138138
MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550]
139139
GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4]
140+
# Currently missing harness files (https://github.com/flutter/flutter/issues/86749):
141+
# camera/camera
142+
# google_sign_in/google_sign_in
143+
# in_app_purchase/in_app_purchase
144+
# in_app_purchase_android
145+
# quick_actions
146+
# shared_preferences/shared_preferences
147+
# url_launcher/url_launcher
148+
# video_player/video_player
149+
# webview_flutter
150+
# Deprecated; no plan to backfill the missing files:
151+
# android_intent,connectivity/connectivity,device_info/device_info,sensors,share,wifi_info_flutter/wifi_info_flutter
152+
# No integration tests to run:
153+
# image_picker/image_picker - Native UI is the critical functionality
154+
# espresso - No Dart code, so no integration tests
155+
PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "camera/camera,google_sign_in/google_sign_in,in_app_purchase/in_app_purchase,in_app_purchase_android,quick_actions,shared_preferences/shared_preferences,url_launcher/url_launcher,video_player/video_player,webview_flutter,android_intent,connectivity/connectivity,device_info/device_info,sensors,share,wifi_info_flutter/wifi_info_flutter,image_picker/image_picker,espresso"
140156
build_script:
141157
# Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they
142158
# might include non-ASCII characters which makes Gradle crash.
@@ -159,7 +175,7 @@ task:
159175
- export CIRRUS_COMMIT_MESSAGE=""
160176
- if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then
161177
- echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json
162-
- ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26
178+
- ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS
163179
- else
164180
- echo "This user does not have permission to run Firebase Test Lab tests."
165181
- fi

script/tool/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
- Added an `xctest` flag to select specific test targets, to allow running only
44
unit tests or integration tests.
55
- Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command.
6+
- Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more
7+
than one plugin's tests in a single run.
8+
- **Breaking change**: If `firebase-test-lab` is run on a package that supports
9+
Android, but for which no tests are run, it now fails instead of skipping.
10+
This matches `drive-examples`, as this command is what is used for driving
11+
Android Flutter integration tests on CI.
612

713
## 0.4.1
814

script/tool/lib/src/firebase_test_lab_command.dart

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
7676

7777
static const String _gradleWrapper = 'gradlew';
7878

79-
Completer<void>? _firebaseProjectConfigured;
79+
bool _firebaseProjectConfigured = false;
8080

8181
Future<void> _configureFirebaseProject() async {
82-
if (_firebaseProjectConfigured != null) {
83-
return _firebaseProjectConfigured!.future;
82+
if (_firebaseProjectConfigured) {
83+
return;
8484
}
85-
_firebaseProjectConfigured = Completer<void>();
8685

8786
final String serviceKey = getStringArg('service-key');
8887
if (serviceKey.isEmpty) {
@@ -110,31 +109,34 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
110109
print('');
111110
if (exitCode == 0) {
112111
print('Firebase project configured.');
113-
return;
114112
} else {
115113
logWarning(
116114
'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.');
117115
}
118116
}
119-
_firebaseProjectConfigured!.complete(null);
117+
_firebaseProjectConfigured = true;
120118
}
121119

122120
@override
123121
Future<PackageResult> runForPackage(Directory package) async {
124-
if (!package
125-
.childDirectory('example')
126-
.childDirectory('android')
122+
final Directory exampleDirectory = package.childDirectory('example');
123+
final Directory androidDirectory =
124+
exampleDirectory.childDirectory('android');
125+
if (!androidDirectory.existsSync()) {
126+
return PackageResult.skip(
127+
'${getPackageDescription(exampleDirectory)} does not support Android.');
128+
}
129+
130+
if (!androidDirectory
127131
.childDirectory('app')
128132
.childDirectory('src')
129133
.childDirectory('androidTest')
130134
.existsSync()) {
131-
return PackageResult.skip('No example with androidTest directory');
135+
printError('No androidTest directory found.');
136+
return PackageResult.fail(
137+
<String>['No tests ran (use --exclude if this is intentional).']);
132138
}
133139

134-
final Directory exampleDirectory = package.childDirectory('example');
135-
final Directory androidDirectory =
136-
exampleDirectory.childDirectory('android');
137-
138140
// Ensures that gradle wrapper exists
139141
if (!await _ensureGradleWrapperExists(androidDirectory)) {
140142
return PackageResult.fail(<String>['Unable to build example apk']);
@@ -191,6 +193,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
191193
errors.add('$testName failed tests');
192194
}
193195
}
196+
197+
if (errors.isEmpty && resultsCounter == 0) {
198+
printError('No integration tests were run.');
199+
errors.add('No tests ran (use --exclude if this is intentional).');
200+
}
201+
194202
return errors.isEmpty
195203
? PackageResult.success()
196204
: PackageResult.fail(errors);

script/tool/test/firebase_test_lab_command_test.dart

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,85 @@ void main() {
8484
]));
8585
});
8686

87+
test('only runs gcloud configuration once', () async {
88+
createFakePlugin('plugin1', packagesDir, extraFiles: <String>[
89+
'test/plugin_test.dart',
90+
'example/integration_test/foo_test.dart',
91+
'example/android/gradlew',
92+
'example/android/app/src/androidTest/MainActivityTest.java',
93+
]);
94+
createFakePlugin('plugin2', packagesDir, extraFiles: <String>[
95+
'test/plugin_test.dart',
96+
'example/integration_test/bar_test.dart',
97+
'example/android/gradlew',
98+
'example/android/app/src/androidTest/MainActivityTest.java',
99+
]);
100+
101+
final List<String> output = await runCapturingPrint(runner, <String>[
102+
'firebase-test-lab',
103+
'--device',
104+
'model=flame,version=29',
105+
'--device',
106+
'model=seoul,version=26',
107+
'--test-run-id',
108+
'testRunId',
109+
'--build-id',
110+
'buildId',
111+
]);
112+
113+
expect(
114+
output,
115+
containsAllInOrder(<Matcher>[
116+
contains('Running for plugin1'),
117+
contains('Firebase project configured.'),
118+
contains('Testing example/integration_test/foo_test.dart...'),
119+
contains('Running for plugin2'),
120+
contains('Testing example/integration_test/bar_test.dart...'),
121+
]),
122+
);
123+
124+
expect(
125+
processRunner.recordedCalls,
126+
orderedEquals(<ProcessCall>[
127+
ProcessCall(
128+
'gcloud',
129+
'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json'
130+
.split(' '),
131+
null),
132+
ProcessCall(
133+
'gcloud', 'config set project flutter-infra'.split(' '), null),
134+
ProcessCall(
135+
'/packages/plugin1/example/android/gradlew',
136+
'app:assembleAndroidTest -Pverbose=true'.split(' '),
137+
'/packages/plugin1/example/android'),
138+
ProcessCall(
139+
'/packages/plugin1/example/android/gradlew',
140+
'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin1/example/integration_test/foo_test.dart'
141+
.split(' '),
142+
'/packages/plugin1/example/android'),
143+
ProcessCall(
144+
'gcloud',
145+
'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26'
146+
.split(' '),
147+
'/packages/plugin1/example'),
148+
ProcessCall(
149+
'/packages/plugin2/example/android/gradlew',
150+
'app:assembleAndroidTest -Pverbose=true'.split(' '),
151+
'/packages/plugin2/example/android'),
152+
ProcessCall(
153+
'/packages/plugin2/example/android/gradlew',
154+
'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin2/example/integration_test/bar_test.dart'
155+
.split(' '),
156+
'/packages/plugin2/example/android'),
157+
ProcessCall(
158+
'gcloud',
159+
'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26'
160+
.split(' '),
161+
'/packages/plugin2/example'),
162+
]),
163+
);
164+
});
165+
87166
test('runs integration tests', () async {
88167
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
89168
'test/plugin_test.dart',
@@ -203,12 +282,87 @@ void main() {
203282
);
204283
});
205284

206-
test('skips packages with no androidTest directory', () async {
285+
test('fails for packages with no androidTest directory', () async {
207286
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
208287
'example/integration_test/foo_test.dart',
209288
'example/android/gradlew',
210289
]);
211290

291+
Error? commandError;
292+
final List<String> output = await runCapturingPrint(
293+
runner,
294+
<String>[
295+
'firebase-test-lab',
296+
'--device',
297+
'model=flame,version=29',
298+
'--device',
299+
'model=seoul,version=26',
300+
'--test-run-id',
301+
'testRunId',
302+
'--build-id',
303+
'buildId',
304+
],
305+
errorHandler: (Error e) {
306+
commandError = e;
307+
},
308+
);
309+
310+
expect(commandError, isA<ToolExit>());
311+
expect(
312+
output,
313+
containsAllInOrder(<Matcher>[
314+
contains('Running for plugin'),
315+
contains('No androidTest directory found.'),
316+
contains('The following packages had errors:'),
317+
contains('plugin:\n'
318+
' No tests ran (use --exclude if this is intentional).'),
319+
]),
320+
);
321+
});
322+
323+
test('fails for packages with no integration test files', () async {
324+
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
325+
'example/android/gradlew',
326+
'example/android/app/src/androidTest/MainActivityTest.java',
327+
]);
328+
329+
Error? commandError;
330+
final List<String> output = await runCapturingPrint(
331+
runner,
332+
<String>[
333+
'firebase-test-lab',
334+
'--device',
335+
'model=flame,version=29',
336+
'--device',
337+
'model=seoul,version=26',
338+
'--test-run-id',
339+
'testRunId',
340+
'--build-id',
341+
'buildId',
342+
],
343+
errorHandler: (Error e) {
344+
commandError = e;
345+
},
346+
);
347+
348+
expect(commandError, isA<ToolExit>());
349+
expect(
350+
output,
351+
containsAllInOrder(<Matcher>[
352+
contains('Running for plugin'),
353+
contains('No integration tests were run'),
354+
contains('The following packages had errors:'),
355+
contains('plugin:\n'
356+
' No tests ran (use --exclude if this is intentional).'),
357+
]),
358+
);
359+
});
360+
361+
test('skips packages with no android directory', () async {
362+
createFakePackage('package', packagesDir, extraFiles: <String>[
363+
'example/integration_test/foo_test.dart',
364+
]);
365+
212366
final List<String> output = await runCapturingPrint(runner, <String>[
213367
'firebase-test-lab',
214368
'--device',
@@ -224,8 +378,8 @@ void main() {
224378
expect(
225379
output,
226380
containsAllInOrder(<Matcher>[
227-
contains('Running for plugin'),
228-
contains('No example with androidTest directory'),
381+
contains('Running for package'),
382+
contains('package/example does not support Android'),
229383
]),
230384
);
231385
expect(output,

0 commit comments

Comments
 (0)