flutter_flutter/dev/tools/examples_smoke_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

248 lines
8.0 KiB
Dart

// 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';
const FileSystem _kFilesystem = LocalFileSystem();
const ProcessManager _kProcessManager = LocalProcessManager();
const Platform _kPlatform = LocalPlatform();
FutureOr<dynamic> main() async {
if (!_kPlatform.isLinux && !_kPlatform.isWindows && !_kPlatform.isMacOS) {
stderr.writeln('Example smoke tests are only designed to run on desktop platforms');
exitCode = 4;
return;
}
final Directory flutterDir = _kFilesystem.directory(
path.absolute(path.dirname(path.dirname(path.dirname(_kPlatform.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(_kPlatform.isWindows ? 'flutter.bat' : 'flutter');
final cmd = <String>[
// If we're in a container with no X display, then use the virtual framebuffer.
if (_kPlatform.isLinux &&
(_kPlatform.environment['DISPLAY'] == null || _kPlatform.environment['DISPLAY']!.isEmpty))
'/usr/bin/xvfb-run',
flutterExe.absolute.path,
'test',
'--reporter=expanded',
'--device-id=${_kPlatform.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(File file, Directory examplesLibDir)
: importPath = _getImportPath(file, examplesLibDir),
importName = '' {
importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_');
}
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 _kFilesystem.file(path.join(examplesLibDir.absolute.path, examplePath));
});
// Collect the examples, and import them all as separate symbols.
final infoList = <ExampleInfo>[
for (final File example in examples) ExampleInfo(example, examplesLibDir),
];
infoList.sort((ExampleInfo a, ExampleInfo b) => a.importPath.compareTo(b.importPath));
final imports = <String>[
"import 'package:flutter/widgets.dart';",
"import 'package:flutter/scheduler.dart';",
"import 'package:flutter_test/flutter_test.dart';",
"import 'package:integration_test/integration_test.dart';",
for (final ExampleInfo info in infoList)
"import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};",
]..sort();
final 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 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;
timeDilation = 1.0;
}
},
);
''');
}
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 stdoutOutput = <int>[];
final combinedOutput = <int>[];
final stdoutComplete = Completer<void>();
final stderrComplete = Completer<void>();
late Process process;
Future<int> allComplete() async {
await stderrComplete.future;
await stdoutComplete.future;
return process.exitCode;
}
try {
process = await _kProcessManager.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);
}