blob: cfa2a5e0b07b0f457ce7bce115e0671b8ac7dedf [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This test builds an integration test from the list of samples in the
// examples/api/lib directory, and then runs it. The tests are just smoke tests,
// designed to start up each example and run it for a couple of frames to make
// sure it doesn't throw an exception or fail to compile.
import 'dart:async';
import 'dart:convert';
import 'dart:io' show Process, ProcessException, exitCode, stderr, stdout;
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'package:process/process.dart';
FileSystem filesystem = const LocalFileSystem();
ProcessManager processManager = const LocalProcessManager();
Platform platform = const LocalPlatform();
FutureOr<dynamic> main() async {
if (!platform.isLinux && !platform.isWindows && !platform.isMacOS) {
stderr.writeln('Example smoke tests are only designed to run on desktop platforms');
exitCode = 4;
return;
}
final Directory flutterDir = filesystem.directory(
path.absolute(
path.dirname(
path.dirname(
path.dirname(platform.script.toFilePath()),
),
),
),
);
final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api');
final File integrationTest = await generateTest(apiDir);
try {
await runSmokeTests(flutterDir: flutterDir, integrationTest: integrationTest, apiDir: apiDir);
} finally {
await cleanUp(integrationTest);
}
}
Future<void> cleanUp(File integrationTest) async {
try {
await integrationTest.delete();
// Delete the integration_test directory if it is empty.
await integrationTest.parent.delete();
} on FileSystemException {
// Ignore, there might be other files in there preventing it from
// being removed, or it might not exist.
}
}
// Executes the generated smoke test.
Future<void> runSmokeTests({
required Directory flutterDir,
required File integrationTest,
required Directory apiDir,
}) async {
final File flutterExe =
flutterDir.childDirectory('bin').childFile(platform.isWindows ? 'flutter.bat' : 'flutter');
final List<String> cmd = <String>[
// If we're in a container with no X display, then use the virtual framebuffer.
if (platform.isLinux &&
(platform.environment['DISPLAY'] == null ||
platform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run',
flutterExe.absolute.path,
'test',
'--reporter=expanded',
'--device-id=${platform.operatingSystem}',
integrationTest.absolute.path,
];
await runCommand(cmd, workingDirectory: apiDir);
}
// A class to hold information related to an example, used to generate names
// from for the tests.
class ExampleInfo {
ExampleInfo(this.file, Directory examplesLibDir)
: importPath = _getImportPath(file, examplesLibDir),
importName = '' {
importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_');
}
final File file;
final String importPath;
String importName;
static String _getImportPath(File example, Directory examplesLibDir) {
final String relativePath =
path.relative(example.absolute.path, from: examplesLibDir.absolute.path);
// So that Windows paths are proper URIs in the import statements.
return path.toUri(relativePath).toFilePath(windows: false);
}
}
// Generates the combined smoke test.
Future<File> generateTest(Directory apiDir) async {
final Directory examplesLibDir = apiDir.childDirectory('lib');
// Get files from git, to avoid any non-repo files that might be in someone's
// workspace.
final List<String> gitFiles = (await runCommand(
<String>['git', 'ls-files', '**/*.dart'],
workingDirectory: examplesLibDir,
quiet: true,
)).replaceAll(r'\', '/')
.trim()
.split('\n');
final Iterable<File> examples = gitFiles.map<File>((String examplePath) {
return filesystem.file(path.join(examplesLibDir.absolute.path, examplePath));
});
// Collect the examples, and import them all as separate symbols.
final List<String> imports = <String>[];
imports.add('''import 'package:flutter/widgets.dart';''');
imports.add('''import 'package:flutter_test/flutter_test.dart';''');
imports.add('''import 'package:integration_test/integration_test.dart';''');
final List<ExampleInfo> infoList = <ExampleInfo>[];
for (final File example in examples) {
final ExampleInfo info = ExampleInfo(example, examplesLibDir);
infoList.add(info);
imports.add('''import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};''');
}
imports.sort();
infoList.sort((ExampleInfo a, ExampleInfo b) => a.importPath.compareTo(b.importPath));
final StringBuffer buffer = StringBuffer();
buffer.writeln('// Temporary generated file. Do not commit.');
buffer.writeln("import 'dart:io';");
buffer.writeAll(imports, '\n');
buffer.writeln(r'''
import '../../../dev/manual_tests/test/mock_image_http.dart';
void main() {
IntegrationTestWidgetsFlutterBinding? binding;
try {
binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding;
} catch (e) {
stderr.writeln('Unable to initialize binding${binding == null ? '' : ' $binding'}: $e');
exitCode = 128;
return;
}
''');
for (final ExampleInfo info in infoList) {
buffer.writeln('''
testWidgets(
'Smoke test ${info.importPath}',
(WidgetTester tester) async {
final ErrorWidgetBuilder originalBuilder = ErrorWidget.builder;
try {
HttpOverrides.runZoned(() {
${info.importName}.main();
}, createHttpClient: (SecurityContext? context) => FakeHttpClient(context));
await tester.pump();
await tester.pump();
expect(find.byType(WidgetsApp), findsOneWidget);
} finally {
ErrorWidget.builder = originalBuilder;
}
},
);
''');
}
buffer.writeln('}');
final File integrationTest =
apiDir.childDirectory('integration_test').childFile('smoke_integration_test.dart');
integrationTest.createSync(recursive: true);
integrationTest.writeAsStringSync(buffer.toString());
return integrationTest;
}
// Run a command, and optionally stream the output as it runs, returning the
// stdout.
Future<String> runCommand(
List<String> cmd, {
required Directory workingDirectory,
bool quiet = false,
List<String>? output,
Map<String, String>? environment,
}) async {
final List<int> stdoutOutput = <int>[];
final List<int> combinedOutput = <int>[];
final Completer<void> stdoutComplete = Completer<void>();
final Completer<void> stderrComplete = Completer<void>();
late Process process;
Future<int> allComplete() async {
await stderrComplete.future;
await stdoutComplete.future;
return process.exitCode;
}
try {
process = await processManager.start(
cmd,
workingDirectory: workingDirectory.absolute.path,
environment: environment,
);
process.stdout.listen(
(List<int> event) {
stdoutOutput.addAll(event);
combinedOutput.addAll(event);
if (!quiet) {
stdout.add(event);
}
},
onDone: () async => stdoutComplete.complete(),
);
process.stderr.listen(
(List<int> event) {
combinedOutput.addAll(event);
if (!quiet) {
stderr.add(event);
}
},
onDone: () async => stderrComplete.complete(),
);
} on ProcessException catch (e) {
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
'failed with:\n$e');
exitCode = 2;
return utf8.decode(stdoutOutput);
} on ArgumentError catch (e) {
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
'failed with:\n$e');
exitCode = 3;
return utf8.decode(stdoutOutput);
}
final int processExitCode = await allComplete();
if (processExitCode != 0) {
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode');
exitCode = processExitCode;
}
if (output != null) {
output.addAll(utf8.decode(combinedOutput).split('\n'));
}
return utf8.decode(stdoutOutput);
}