Greg Spencer | ab2b085 | 2021-09-28 09:32:06 -0700 | [diff] [blame^] | 1 | // 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 | |
| 10 | import 'dart:async'; |
| 11 | import 'dart:convert'; |
| 12 | import 'dart:io' show stdout, stderr, exitCode, Process, ProcessException; |
| 13 | |
| 14 | import 'package:file/file.dart'; |
| 15 | import 'package:file/local.dart'; |
| 16 | import 'package:path/path.dart' as path; |
| 17 | import 'package:platform/platform.dart'; |
| 18 | import 'package:process/process.dart'; |
| 19 | |
| 20 | const bool kIsWeb = identical(0, 0.0); |
| 21 | FileSystem filesystem = const LocalFileSystem(); |
| 22 | ProcessManager processManager = const LocalProcessManager(); |
| 23 | Platform platform = const LocalPlatform(); |
| 24 | |
| 25 | FutureOr<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 | |
| 49 | Future<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. |
| 61 | Future<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. |
| 84 | class 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. |
| 104 | Future<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 | |
| 141 | import '../../../dev/manual_tests/test/mock_image_http.dart'; |
| 142 | |
| 143 | void 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. |
| 185 | Future<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 | } |