blob: cf36762f9d81d7717330597b9edf4b39f51acf6b [file] [log] [blame]
Greg Spencerab2b0852021-09-28 09:32:06 -07001// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// This test builds an integration test from the list of samples in the
6// examples/api/lib directory, and then runs it. The tests are just smoke tests,
7// designed to start up each example and run it for a couple of frames to make
8// sure it doesn't throw an exception or fail to compile.
9
10import 'dart:async';
11import 'dart:convert';
12import 'dart:io' show stdout, stderr, exitCode, Process, ProcessException;
13
14import 'package:file/file.dart';
15import 'package:file/local.dart';
16import 'package:path/path.dart' as path;
17import 'package:platform/platform.dart';
18import 'package:process/process.dart';
19
20const bool kIsWeb = identical(0, 0.0);
21FileSystem filesystem = const LocalFileSystem();
22ProcessManager processManager = const LocalProcessManager();
23Platform platform = const LocalPlatform();
24
25FutureOr<dynamic> main() async {
26 if (!platform.isLinux && !platform.isWindows && !platform.isMacOS) {
27 stderr.writeln('Example smoke tests are only designed to run on desktop platforms');
28 exitCode = 4;
29 return;
30 }
31 final Directory flutterDir = filesystem.directory(
32 path.absolute(
33 path.dirname(
34 path.dirname(
35 path.dirname(platform.script.toFilePath()),
36 ),
37 ),
38 ),
39 );
40 final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api');
41 final File integrationTest = await generateTest(apiDir);
42 try {
43 await runSmokeTests(flutterDir: flutterDir, integrationTest: integrationTest, apiDir: apiDir);
44 } finally {
45 await cleanUp(integrationTest);
46 }
47}
48
49Future<void> cleanUp(File integrationTest) async {
50 try {
51 await integrationTest.delete();
52 // Delete the integration_test directory if it is empty.
53 await integrationTest.parent.delete(recursive: false);
54 } on FileSystemException {
55 // Ignore, there might be other files in there preventing it from
56 // being removed, or it might not exist.
57 }
58}
59
60// Executes the generated smoke test.
61Future<void> runSmokeTests({
62 required Directory flutterDir,
63 required File integrationTest,
64 required Directory apiDir,
65}) async {
66 final File flutterExe =
67 flutterDir.childDirectory('bin').childFile(platform.isWindows ? 'flutter.bat' : 'flutter');
68 final List<String> cmd = <String>[
69 // If we're in a container with no X display, then use the virtual framebuffer.
70 if (platform.isLinux &&
71 (platform.environment['DISPLAY'] == null ||
72 platform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run',
73 flutterExe.absolute.path,
74 'test',
75 '--reporter=expanded',
76 '--device-id=${platform.operatingSystem}',
77 integrationTest.absolute.path,
78 ];
79 await runCommand(cmd, workingDirectory: apiDir);
80}
81
82// A class to hold information related to an example, used to generate names
83// from for the tests.
84class ExampleInfo {
85 ExampleInfo(this.file, Directory examplesLibDir)
86 : importPath = _getImportPath(file, examplesLibDir),
87 importName = '' {
88 importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_');
89 }
90
91 final File file;
92 final String importPath;
93 String importName;
94
95 static String _getImportPath(File example, Directory examplesLibDir) {
96 final String relativePath =
97 path.relative(example.absolute.path, from: examplesLibDir.absolute.path);
98 // So that Windows paths are proper URIs in the import statements.
99 return path.toUri(relativePath).toFilePath(windows: false);
100 }
101}
102
103// Generates the combined smoke test.
104Future<File> generateTest(Directory apiDir) async {
105 final Directory examplesLibDir = apiDir.childDirectory('lib');
106
107 // Get files from git, to avoid any non-repo files that might be in someone's
108 // workspace.
109 final List<String> gitFiles = (await runCommand(
110 <String>['git', 'ls-files', '**/*.dart'],
111 workingDirectory: examplesLibDir,
112 quiet: true,
113 )).replaceAll(r'\', '/')
114 .trim()
115 .split('\n');
116 final Iterable<File> examples = gitFiles.map<File>((String examplePath) {
117 return filesystem.file(path.join(examplesLibDir.absolute.path, examplePath));
118 });
119
120 // Collect the examples, and import them all as separate symbols.
121 final List<String> imports = <String>[];
122 imports.add('''import 'package:flutter/widgets.dart';''');
123 imports.add('''import 'package:flutter_test/flutter_test.dart';''');
124 imports.add('''import 'package:integration_test/integration_test.dart';''');
125 final List<ExampleInfo> infoList = <ExampleInfo>[];
126 for (final File example in examples) {
127 final ExampleInfo info = ExampleInfo(example, examplesLibDir);
128 infoList.add(info);
129 imports.add('''import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};''');
130 }
131 imports.sort();
132 infoList.sort((ExampleInfo a, ExampleInfo b) => a.importPath.compareTo(b.importPath));
133
134 final StringBuffer buffer = StringBuffer();
135 buffer.writeln('// Temporary generated file. Do not commit.');
136 buffer.writeln("import 'dart:io';");
137 buffer.writeAll(imports, '\n');
138 buffer.writeln(r'''
139
140
141import '../../../dev/manual_tests/test/mock_image_http.dart';
142
143void main() {
144 IntegrationTestWidgetsFlutterBinding? binding;
145 try {
146 binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding;
147 } catch (e) {
148 stderr.writeln('Unable to initialize binding${binding == null ? '' : ' $binding'}: $e');
149 exitCode = 128;
150 return;
151 }
152
153''');
154 for (final ExampleInfo info in infoList) {
155 buffer.writeln('''
156 testWidgets(
157 'Smoke test ${info.importPath}',
158 (WidgetTester tester) async {
159 final ErrorWidgetBuilder originalBuilder = ErrorWidget.builder;
160 try {
161 HttpOverrides.runZoned(() {
162 ${info.importName}.main();
163 }, createHttpClient: (SecurityContext? context) => FakeHttpClient(context));
164 await tester.pump();
165 await tester.pump();
166 expect(find.byType(WidgetsApp), findsOneWidget);
167 } finally {
168 ErrorWidget.builder = originalBuilder;
169 }
170 },
171 );
172''');
173 }
174 buffer.writeln('}');
175
176 final File integrationTest =
177 apiDir.childDirectory('integration_test').childFile('smoke_integration_test.dart');
178 integrationTest.createSync(recursive: true);
179 integrationTest.writeAsStringSync(buffer.toString());
180 return integrationTest;
181}
182
183// Run a command, and optionally stream the output as it runs, returning the
184// stdout.
185Future<String> runCommand(
186 List<String> cmd, {
187 required Directory workingDirectory,
188 bool quiet = false,
189 List<String>? output,
190 Map<String, String>? environment,
191}) async {
192 final List<int> stdoutOutput = <int>[];
193 final List<int> combinedOutput = <int>[];
194 final Completer<void> stdoutComplete = Completer<void>();
195 final Completer<void> stderrComplete = Completer<void>();
196
197 late Process process;
198 Future<int> allComplete() async {
199 await stderrComplete.future;
200 await stdoutComplete.future;
201 return process.exitCode;
202 }
203
204 try {
205 process = await processManager.start(
206 cmd,
207 workingDirectory: workingDirectory.absolute.path,
208 includeParentEnvironment: true,
209 environment: environment,
210 );
211 process.stdout.listen(
212 (List<int> event) {
213 stdoutOutput.addAll(event);
214 combinedOutput.addAll(event);
215 if (!quiet) {
216 stdout.add(event);
217 }
218 },
219 onDone: () async => stdoutComplete.complete(),
220 );
221 process.stderr.listen(
222 (List<int> event) {
223 combinedOutput.addAll(event);
224 if (!quiet) {
225 stderr.add(event);
226 }
227 },
228 onDone: () async => stderrComplete.complete(),
229 );
230 } on ProcessException catch (e) {
231 stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
232 'failed with:\n${e.toString()}');
233 exitCode = 2;
234 return utf8.decode(stdoutOutput);
235 } on ArgumentError catch (e) {
236 stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
237 'failed with:\n${e.toString()}');
238 exitCode = 3;
239 return utf8.decode(stdoutOutput);
240 }
241
242 final int processExitCode = await allComplete();
243 if (processExitCode != 0) {
244 stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode');
245 exitCode = processExitCode;
246 }
247
248 if (output != null) {
249 output.addAll(utf8.decode(combinedOutput).split('\n'));
250 }
251
252 return utf8.decode(stdoutOutput);
253}