Refactor TestGoldenComparator to be useful for non-web (Android, iOS) integration tests (#160215)

Part of https://github.com/flutter/flutter/issues/160043, makes it
easier to add https://github.com/flutter/flutter/pull/160131.

This PR has no functional changes to any of the code, but does refactor
both the code and tests:

- Makes a number of always non-null but not migrated to non-null
properties, well, not-null
- Creates two concrete methods (`update` and `compare` versus a
positional nullable boolean)
- Uses type signatures instead of `String?` to explain the possible
results of the methods
- Renames the mysterious `shellPath` variable to `flutterTesterBinPath`
- Expands and rewrites internally-facing doc comments
- Moves `WebRenderer` environment variable setting to
`flutter_web_platform.dart`
- Makes the tests have less duplication, and check for update/compare
cases

After this PR, I can use it in the non-web branch of the Flutter tool
without any hacks or TODOS :)

/cc @eyebrowsoffire (trivial web refactoring), @camsim99 (changes being
made to tool).
This commit is contained in:
Matan Lurey 2024-12-16 14:08:54 -08:00 committed by GitHub
parent 8fee7cb832
commit 18f56e3224
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 500 additions and 275 deletions

View File

@ -33,8 +33,8 @@ import '../web/bootstrap.dart';
import '../web/chrome.dart';
import '../web/compile.dart';
import '../web/memory_fs.dart';
import 'flutter_web_goldens.dart';
import 'test_compiler.dart';
import 'test_golden_comparator.dart';
import 'test_time_recorder.dart';
shelf.Handler createDirectoryHandler(Directory directory, { required bool crossOriginIsolated} ) {
@ -73,12 +73,12 @@ shelf.Handler createDirectoryHandler(Directory directory, { required bool crossO
class FlutterWebPlatform extends PlatformPlugin {
FlutterWebPlatform._(this._server, this._config, this._root, {
FlutterProject? flutterProject,
String? shellPath,
this.updateGoldens,
this.nullAssertions,
required this.updateGoldens,
required this.buildInfo,
required this.webMemoryFS,
required FlutterProject flutterProject,
required String flutterTesterBinPath,
required FileSystem fileSystem,
required Directory buildDirectory,
required File testDartJs,
@ -115,12 +115,16 @@ class FlutterWebPlatform extends PlatformPlugin {
.add(_packageFilesHandler);
_server.mount(cascade.handler);
_testGoldenComparator = TestGoldenComparator(
shellPath,
() => TestCompiler(buildInfo, flutterProject, testTimeRecorder: testTimeRecorder),
compilerFactory: () => TestCompiler(buildInfo, flutterProject, testTimeRecorder: testTimeRecorder),
flutterTesterBinPath: flutterTesterBinPath,
fileSystem: _fileSystem,
logger: _logger,
processManager: processManager,
webRenderer: webRenderer,
environment: <String, String>{
// Chrome is the only supported browser currently.
'FLUTTER_TEST_BROWSER': 'chrome',
'FLUTTER_WEB_RENDERER': webRenderer.name,
},
);
}
@ -133,7 +137,7 @@ class FlutterWebPlatform extends PlatformPlugin {
final ChromiumLauncher _chromiumLauncher;
final Logger _logger;
final Artifacts? _artifacts;
final bool? updateGoldens;
final bool updateGoldens;
final bool? nullAssertions;
final OneOffHandler _webSocketHandler = OneOffHandler();
final AsyncMemoizer<void> _closeMemo = AsyncMemoizer<void>();
@ -154,11 +158,11 @@ class FlutterWebPlatform extends PlatformPlugin {
}
static Future<FlutterWebPlatform> start(String root, {
FlutterProject? flutterProject,
String? shellPath,
bool updateGoldens = false,
bool pauseAfterLoad = false,
bool nullAssertions = false,
required FlutterProject flutterProject,
required String flutterTesterBinPath,
required BuildInfo buildInfo,
required WebMemoryFS webMemoryFS,
required FileSystem fileSystem,
@ -195,7 +199,7 @@ class FlutterWebPlatform extends PlatformPlugin {
Configuration.current.change(pauseAfterLoad: pauseAfterLoad),
root,
flutterProject: flutterProject,
shellPath: shellPath,
flutterTesterBinPath: flutterTesterBinPath,
updateGoldens: updateGoldens,
buildInfo: buildInfo,
webMemoryFS: webMemoryFS,
@ -456,8 +460,17 @@ class FlutterWebPlatform extends PlatformPlugin {
return shelf.Response.ok('Caught exception: $ex');
}
}
final String? errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens);
return shelf.Response.ok(errorMessage ?? 'true');
if (updateGoldens) {
return switch (await _testGoldenComparator.update(testUri, bytes, goldenKey)) {
TestGoldenUpdateDone() => shelf.Response.ok('true'),
TestGoldenUpdateError(error: final String error) => shelf.Response.ok(error),
};
} else {
return switch (await _testGoldenComparator.compare(testUri, bytes, goldenKey)) {
TestGoldenComparisonDone(matched: final bool matched) => shelf.Response.ok('$matched'),
TestGoldenComparisonError(error: final String error) => shelf.Response.ok(error),
};
}
} else {
return shelf.Response.notFound('Not Found');
}

View File

@ -133,7 +133,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
BuildInfo? buildInfo,
}) async {
// Configure package:test to use the Flutter engine for child processes.
final String shellPath = globals.artifacts!.getArtifactPath(Artifact.flutterTester);
final String flutterTesterBinPath = globals.artifacts!.getArtifactPath(Artifact.flutterTester);
// Compute the command-line arguments for package:test.
final List<String> testArgs = <String>[
@ -203,7 +203,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
return FlutterWebPlatform.start(
flutterProject.directory.path,
updateGoldens: updateGoldens,
shellPath: shellPath,
flutterTesterBinPath: flutterTesterBinPath,
flutterProject: flutterProject,
pauseAfterLoad: debuggingOptions.startPaused,
nullAssertions: debuggingOptions.nullAssertions,
@ -241,7 +241,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
final loader.FlutterPlatform platform = loader.installHook(
testWrapper: testWrapper,
shellPath: shellPath,
shellPath: flutterTesterBinPath,
debuggingOptions: debuggingOptions,
watcher: watcher,
enableVmService: enableVmService,

View File

@ -5,50 +5,74 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../convert.dart';
import '../web/compile.dart';
import 'test_compiler.dart';
import 'test_config.dart';
/// Helper class to start golden file comparison in a separate process.
/// Runs a [GoldenFileComparator] (that may depend on `dart:ui`) in a `flutter_tester`.
///
/// The golden file comparator is configured using flutter_test_config.dart and that
/// file can contain arbitrary Dart code that depends on dart:ui. Thus it has to
/// be executed in a `flutter_tester` environment. This helper class generates a
/// Dart file configured with flutter_test_config.dart to perform the comparison
/// of golden files.
class TestGoldenComparator {
/// The [`goldenFileComparator`](https://api.flutter.dev/flutter/flutter_test/goldenFileComparator.html)
/// is configured using [`flutter_test_config.dart`](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html)
/// and that file often contains arbitrary Dart code that depends on [`dart:ui`](https://api.flutter.dev/flutter/dart-ui/dart-ui-library.html).
///
/// This proxying comparator creates a minimal application that runs on a
/// `flutter_tester` instance, runs a golden comparison, and then returns the
/// results through [compareGoldens].
///
/// ## Example
///
/// ```dart
/// final comparator = TestGoldenComparator(
/// flutterTesterBinPath: '/path/to/flutter_tester',
/// logger: ...,
/// fileSystem: ...,
/// processManager: ...,
/// )
///
/// final result = await comparator.compare(testUri, bytes, goldenKey);
/// ```
final class TestGoldenComparator {
/// Creates a [TestGoldenComparator] instance.
TestGoldenComparator(this.shellPath, this.compilerFactory, {
TestGoldenComparator({
required String flutterTesterBinPath,
required TestCompiler Function() compilerFactory,
required Logger logger,
required FileSystem fileSystem,
required ProcessManager processManager,
required this.webRenderer,
}) : tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform.'),
Map<String, String> environment = const <String, String>{},
}) : _tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform.'),
_flutterTesterBinPath = flutterTesterBinPath,
_compilerFactory = compilerFactory,
_logger = logger,
_fileSystem = fileSystem,
_processManager = processManager;
_processManager = processManager,
_environment = environment;
final String? shellPath;
final Directory tempDir;
final TestCompiler Function() compilerFactory;
final String _flutterTesterBinPath;
final Directory _tempDir;
final Logger _logger;
final FileSystem _fileSystem;
final ProcessManager _processManager;
final WebRendererMode webRenderer;
final Map<String, String> _environment;
final TestCompiler Function() _compilerFactory;
late final TestCompiler _compiler = _compilerFactory();
TestCompiler? _compiler;
TestGoldenComparatorProcess? _previousComparator;
Uri? _previousTestUri;
/// Closes the comparator.
///
/// Any operation in process is terminated and the comparator can no longer be used.
Future<void> close() async {
tempDir.deleteSync(recursive: true);
await _compiler?.dispose();
_tempDir.deleteSync(recursive: true);
await _compiler.dispose();
await _previousComparator?.close();
}
@ -73,33 +97,46 @@ class TestGoldenComparator {
Future<Process?> _startProcess(String testBootstrap) async {
// Prepare the Dart file that will talk to us and start the test.
final File listenerFile = (await tempDir.createTemp('listener')).childFile('listener.dart');
final File listenerFile = (await _tempDir.createTemp('listener')).childFile('listener.dart');
await listenerFile.writeAsString(testBootstrap);
// Lazily create the compiler
_compiler = _compiler ?? compilerFactory();
final String? output = await _compiler!.compile(listenerFile.uri);
final String? output = await _compiler.compile(listenerFile.uri);
if (output == null) {
return null;
}
final List<String> command = <String>[
shellPath!,
_flutterTesterBinPath,
'--disable-vm-service',
'--non-interactive',
'--packages=${_fileSystem.path.join('.dart_tool', 'package_config.json')}',
output,
];
final Map<String, String> environment = <String, String>{
// Chrome is the only supported browser currently.
'FLUTTER_TEST_BROWSER': 'chrome',
'FLUTTER_WEB_RENDERER': webRenderer.name,
};
return _processManager.start(command, environment: environment);
return _processManager.start(command, environment: _environment);
}
Future<String?> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool? updateGoldens) async {
final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
/// Compares the golden file designated by [goldenKey], relative to [testUri], to the provide [bytes].
Future<TestGoldenComparison> compare(Uri testUri, Uint8List bytes, Uri goldenKey) async {
final String? result = await _compareGoldens(testUri, bytes, goldenKey, false);
return switch (result) {
null => const TestGoldenComparisonDone(matched: true),
'does not match' => const TestGoldenComparisonDone(matched: false),
final String error => TestGoldenComparisonError(error: error),
};
}
/// Updates the golden file designated by [goldenKey], relative to [testUri], to the provide [bytes].
Future<TestGoldenUpdate> update(Uri testUri, Uint8List bytes, Uri goldenKey) async {
final String? result = await _compareGoldens(testUri, bytes, goldenKey, true);
return switch (result) {
null => const TestGoldenUpdateDone(),
final String error => TestGoldenUpdateError(error: error),
};
}
@useResult
Future<String?> _compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool? updateGoldens) async {
final File imageFile = await (await _tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
final TestGoldenComparatorProcess? process = await _processForTestFile(testUri);
if (process == null) {
return 'process was null';
@ -112,6 +149,105 @@ class TestGoldenComparator {
}
}
/// The result of [TestGoldenComparator.compare].
///
/// See also:
///
/// * [TestGoldenComparisonDone]
/// * [TestGoldenComparisonError]
@immutable
sealed class TestGoldenComparison {}
/// A successful comparison that resulted in [matched].
final class TestGoldenComparisonDone implements TestGoldenComparison {
const TestGoldenComparisonDone({required this.matched});
/// Whether the bytes matched the file specified.
///
/// A value of `true` is a match, and `false` is a "did not match".
final bool matched;
@override
bool operator ==(Object other) {
return other is TestGoldenComparisonDone && matched == other.matched;
}
@override
int get hashCode => matched.hashCode;
@override
String toString() {
return 'TestGoldenComparisonDone(matched: $matched)';
}
}
/// A failed comparison that could not be completed for a reason in [error].
final class TestGoldenComparisonError implements TestGoldenComparison {
const TestGoldenComparisonError({required this.error});
/// Why the comparison failed, which should be surfaced to the user as an error.
final String error;
@override
bool operator ==(Object other) {
return other is TestGoldenComparisonError && error == other.error;
}
@override
int get hashCode => error.hashCode;
@override
String toString() {
return 'TestGoldenComparisonError(error: $error)';
}
}
/// The result of [TestGoldenComparator.update].
///
/// See also:
///
/// * [TestGoldenUpdateDone]
/// * [TestGoldenUpdateError]
@immutable
sealed class TestGoldenUpdate {}
/// A successful update.
final class TestGoldenUpdateDone implements TestGoldenUpdate {
const TestGoldenUpdateDone();
@override
bool operator ==(Object other) => other is TestGoldenUpdateDone;
@override
int get hashCode => (TestGoldenUpdateDone).hashCode;
@override
String toString() {
return 'TestGoldenUpdateDone()';
}
}
/// A failed update that could not be completed for a reason in [error].
final class TestGoldenUpdateError implements TestGoldenUpdate {
const TestGoldenUpdateError({required this.error});
/// Why the comparison failed, which should be surfaced to the user as an error.
final String error;
@override
bool operator ==(Object other) {
return other is TestGoldenUpdateError && error == other.error;
}
@override
int get hashCode => error.hashCode;
@override
String toString() {
return 'TestGoldenUpdateError(error: $error)';
}
}
/// Represents a `flutter_tester` process started for golden comparison. Also
/// handles communication with the child process.
class TestGoldenComparatorProcess {

View File

@ -8,13 +8,14 @@ import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/test/flutter_web_platform.dart';
import 'package:flutter_tools/src/web/chrome.dart';
import 'package:flutter_tools/src/web/compile.dart';
import 'package:flutter_tools/src/web/memory_fs.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:test/test.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
@ -40,6 +41,7 @@ void main() {
late Artifacts artifacts;
late ProcessManager processManager;
late FakeOperatingSystemUtils operatingSystemUtils;
late Directory tempDir;
setUp(() {
fileSystem = MemoryFileSystem.test();
@ -48,6 +50,7 @@ void main() {
artifacts = Artifacts.test(fileSystem: fileSystem);
processManager = FakeProcessManager.empty();
operatingSystemUtils = FakeOperatingSystemUtils();
tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform_test.');
for (final HostArtifact artifact in <HostArtifact>[
HostArtifact.webPrecompiledAmdCanvaskitAndHtmlSoundSdk,
@ -69,6 +72,10 @@ void main() {
}
});
tearDown(() {
tryToDelete(tempDir);
});
testUsingContext(
'FlutterWebPlatform serves the correct dart_sdk.js (amd module system) for the passed web renderer',
() async {
@ -81,15 +88,16 @@ void main() {
logger: logger,
);
final MockServer server = MockServer();
fileSystem.directory('/test').createSync();
final FlutterWebPlatform webPlatform = await FlutterWebPlatform.start(
'ProjectRoot',
flutterProject: FlutterProject.fromDirectoryTest(tempDir),
buildInfo: BuildInfo.debug,
webMemoryFS: WebMemoryFS(),
fileSystem: fileSystem,
buildDirectory: fileSystem.directory('build'),
logger: logger,
chromiumLauncher: chromiumLauncher,
flutterTesterBinPath: artifacts.getArtifactPath(Artifact.flutterTester),
artifacts: artifacts,
processManager: processManager,
webRenderer: WebRendererMode.canvaskit,
@ -125,9 +133,9 @@ void main() {
logger: logger,
);
final MockServer server = MockServer();
fileSystem.directory('/test').createSync();
final FlutterWebPlatform webPlatform = await FlutterWebPlatform.start(
'ProjectRoot',
flutterProject: FlutterProject.fromDirectoryTest(tempDir),
buildInfo: const BuildInfo(
BuildMode.debug,
'',
@ -140,6 +148,7 @@ void main() {
buildDirectory: fileSystem.directory('build'),
logger: logger,
chromiumLauncher: chromiumLauncher,
flutterTesterBinPath: artifacts.getArtifactPath(Artifact.flutterTester),
artifacts: artifacts,
processManager: processManager,
webRenderer: WebRendererMode.canvaskit,

View File

@ -7,7 +7,7 @@ import 'dart:convert';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/test/flutter_web_goldens.dart';
import 'package:flutter_tools/src/test/test_golden_comparator.dart';
import '../../src/common.dart';
import '../../src/fakes.dart';

View File

@ -0,0 +1,290 @@
// 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.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/test/test_compiler.dart';
import 'package:flutter_tools/src/test/test_golden_comparator.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
final Uri testUri1 = Uri(scheme: 'file', path: 'test_file_1');
final Uri testUri2 = Uri(scheme: 'file', path: 'test_file_2');
final Uri goldenKey1 = Uri(path: 'golden_key_1');
final Uri goldenKey2 = Uri(path: 'golden_key_2');
final Uint8List imageBytes = Uint8List.fromList(<int>[1, 2, 3, 4, 5]);
late FileSystem fileSystem;
late BufferLogger logger;
setUp(() {
fileSystem = MemoryFileSystem.test();
logger = BufferLogger.test();
});
testWithoutContext('should succeed when a golden-file comparison matched', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: true),
)
]),
fileSystem: fileSystem,
logger: logger,
);
final TestGoldenComparison result = await comparator.compare(
testUri1,
imageBytes,
goldenKey1,
);
expect(result, const TestGoldenComparisonDone(matched: true));
});
testWithoutContext('should succeed when a golden-file comparison does not match', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: false),
)
]),
fileSystem: fileSystem,
logger: logger,
);
final TestGoldenComparison result = await comparator.compare(
testUri1,
imageBytes,
goldenKey1,
);
expect(result, const TestGoldenComparisonDone(matched: false));
});
testWithoutContext('should return an error when a golden-file comparison errors', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: false, message: 'Did a bad'),
)
]),
fileSystem: fileSystem,
logger: logger,
);
final TestGoldenComparison result = await comparator.compare(
testUri1,
imageBytes,
goldenKey1,
);
expect(result, const TestGoldenComparisonError(error: 'Did a bad'));
});
testWithoutContext('should succeed when a golden-file update completes', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: true),
)
]),
fileSystem: fileSystem,
logger: logger,
);
final TestGoldenUpdate result = await comparator.update(
testUri1,
imageBytes,
goldenKey1,
);
expect(result, const TestGoldenUpdateDone());
});
testWithoutContext('should error when a golden-file update errors', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: false, message: 'Did a bad'),
)
]),
fileSystem: fileSystem,
logger: logger,
);
final TestGoldenUpdate result = await comparator.update(
testUri1,
imageBytes,
goldenKey1,
);
expect(result, const TestGoldenUpdateError(error: 'Did a bad'));
});
testWithoutContext('provides environment variables to the process', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: true),
environment: <String, String>{
'THE_ANSWER': '42',
}
)
]),
fileSystem: fileSystem,
logger: logger,
environment: <String, String>{
'THE_ANSWER': '42',
},
);
final TestGoldenUpdate result = await comparator.update(
testUri1,
imageBytes,
goldenKey1,
);
expect(result, const TestGoldenUpdateDone());
});
testWithoutContext('reuses the process for the same test file', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: <String>[
_encodeStdout(success: false, message: '1 Did a bad'),
_encodeStdout(success: false, message: '2 Did a bad'),
].join('\n'),
)
]),
fileSystem: fileSystem,
logger: logger,
);
final TestGoldenComparison result1 = await comparator.compare(testUri1, imageBytes, goldenKey1);
expect(result1, const TestGoldenComparisonError(error: '1 Did a bad'));
final TestGoldenComparison result2 = await comparator.compare(testUri1, imageBytes, goldenKey2);
expect(result2, const TestGoldenComparisonError(error: '2 Did a bad'));
});
testWithoutContext('does not reuse the process for different test file', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.list(<FakeCommand>[
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: false, message: '1 Did a bad'),
),
_fakeFluterTester(
'flutter_tester',
stdout: _encodeStdout(success: false, message: '2 Did a bad'),
)
]),
fileSystem: fileSystem,
logger: logger,
);
final TestGoldenComparison result1 = await comparator.compare(testUri1, imageBytes, goldenKey1);
expect(result1, const TestGoldenComparisonError(error: '1 Did a bad'));
final TestGoldenComparison result2 = await comparator.compare(testUri2, imageBytes, goldenKey2);
expect(result2, const TestGoldenComparisonError(error: '2 Did a bad'));
});
testWithoutContext('deletes the temporary directory when closed', () async {
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: _FakeTestCompiler.new,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.empty(),
fileSystem: fileSystem,
logger: logger,
);
expect(fileSystem.systemTempDirectory.listSync(recursive: true), isNotEmpty);
await comparator.close();
expect(fileSystem.systemTempDirectory.listSync(recursive: true), isEmpty);
});
testWithoutContext('disposes the test compiler when closed', () async {
final _FakeTestCompiler testCompiler = _FakeTestCompiler();
final TestGoldenComparator comparator = TestGoldenComparator(
compilerFactory: () => testCompiler,
flutterTesterBinPath: 'flutter_tester',
processManager: FakeProcessManager.empty(),
fileSystem: fileSystem,
logger: logger,
);
expect(testCompiler.disposed, false);
await comparator.close();
expect(testCompiler.disposed, true);
});
}
FakeCommand _fakeFluterTester(String pathToBinTool, {
required String stdout,
Map<String, String>? environment,
Completer<void>? waitUntil,
}) {
return FakeCommand(
command: <String>[
pathToBinTool,
'--disable-vm-service',
'--non-interactive',
'--packages=.dart_tool/package_config.json',
'compiler_output',
],
stdout: stdout,
environment: environment,
completer: waitUntil,
);
}
String _encodeStdout({required bool success, String? message}) {
return jsonEncode(<String, Object?>{
'success': success,
if (message != null)
'message': message,
});
}
final class _FakeTestCompiler extends Fake implements TestCompiler {
bool disposed = false;
@override
Future<String> compile(Uri mainDart) {
return Future<String>.value('compiler_output');
}
@override
Future<void> dispose() async {
disposed = true;
}
}

View File

@ -1,223 +0,0 @@
// 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.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/test/flutter_web_goldens.dart';
import 'package:flutter_tools/src/test/test_compiler.dart';
import 'package:flutter_tools/src/web/compile.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart';
final Uri goldenKey = Uri.parse('file://golden_key');
final Uri goldenKey2 = Uri.parse('file://second_golden_key');
final Uri testUri = Uri.parse('file://test_uri');
final Uri testUri2 = Uri.parse('file://second_test_uri');
final Uint8List imageBytes = Uint8List.fromList(<int>[1, 2, 3, 4, 5]);
void main() {
group('Test that TestGoldenComparator', () {
late FakeProcessManager processManager;
setUp(() {
processManager = FakeProcessManager.empty();
});
testWithoutContext('succeed when golden comparison succeed', () async {
final Map<String, dynamic> expectedResponse = <String, dynamic>{
'success': true,
'message': 'some message',
};
processManager.addCommand(FakeCommand(
command: const <String>[
'shell',
'--disable-vm-service',
'--non-interactive',
'--packages=.dart_tool/package_config.json',
'compiler_output',
],
stdout: '${jsonEncode(expectedResponse)}\n',
environment: const <String, String>{
'FLUTTER_TEST_BROWSER': 'chrome',
'FLUTTER_WEB_RENDERER': 'html',
},
));
final TestGoldenComparator comparator = TestGoldenComparator(
'shell',
() => FakeTestCompiler(),
processManager: processManager,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
webRenderer: WebRendererMode.html,
);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result, null);
});
testWithoutContext('fail with error message when golden comparison failed', () async {
final Map<String, dynamic> expectedResponse = <String, dynamic>{
'success': false,
'message': 'some message',
};
processManager.addCommand(FakeCommand(
command: const <String>[
'shell',
'--disable-vm-service',
'--non-interactive',
'--packages=.dart_tool/package_config.json',
'compiler_output',
], stdout: '${jsonEncode(expectedResponse)}\n',
));
final TestGoldenComparator comparator = TestGoldenComparator(
'shell',
() => FakeTestCompiler(),
processManager: processManager,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
webRenderer: WebRendererMode.canvaskit,
);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result, 'some message');
});
testWithoutContext('reuse the process for the same test file', () async {
final Map<String, dynamic> expectedResponse1 = <String, dynamic>{
'success': false,
'message': 'some message',
};
final Map<String, dynamic> expectedResponse2 = <String, dynamic>{
'success': false,
'message': 'some other message',
};
processManager.addCommand(FakeCommand(
command: const <String>[
'shell',
'--disable-vm-service',
'--non-interactive',
'--packages=.dart_tool/package_config.json',
'compiler_output',
], stdout: '${jsonEncode(expectedResponse1)}\n${jsonEncode(expectedResponse2)}\n',
));
final TestGoldenComparator comparator = TestGoldenComparator(
'shell',
() => FakeTestCompiler(),
processManager: processManager,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
webRenderer: WebRendererMode.html,
);
final String? result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result1, 'some message');
final String? result2 = await comparator.compareGoldens(testUri, imageBytes, goldenKey2, false);
expect(result2, 'some other message');
});
testWithoutContext('does not reuse the process for different test file', () async {
final Map<String, dynamic> expectedResponse1 = <String, dynamic>{
'success': false,
'message': 'some message',
};
final Map<String, dynamic> expectedResponse2 = <String, dynamic>{
'success': false,
'message': 'some other message',
};
processManager.addCommand(FakeCommand(
command: const <String>[
'shell',
'--disable-vm-service',
'--non-interactive',
'--packages=.dart_tool/package_config.json',
'compiler_output',
], stdout: '${jsonEncode(expectedResponse1)}\n',
));
processManager.addCommand(FakeCommand(
command: const <String>[
'shell',
'--disable-vm-service',
'--non-interactive',
'--packages=.dart_tool/package_config.json',
'compiler_output',
], stdout: '${jsonEncode(expectedResponse2)}\n',
));
final TestGoldenComparator comparator = TestGoldenComparator(
'shell',
() => FakeTestCompiler(),
processManager: processManager,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
webRenderer: WebRendererMode.canvaskit,
);
final String? result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result1, 'some message');
final String? result2 = await comparator.compareGoldens(testUri2, imageBytes, goldenKey2, false);
expect(result2, 'some other message');
});
testWithoutContext('removes all temporary files when closed', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final Map<String, dynamic> expectedResponse = <String, dynamic>{
'success': true,
'message': 'some message',
};
final StreamController<List<int>> controller = StreamController<List<int>>();
final IOSink stdin = IOSink(controller.sink);
processManager.addCommand(FakeCommand(
command: const <String>[
'shell',
'--disable-vm-service',
'--non-interactive',
'--packages=.dart_tool/package_config.json',
'compiler_output',
], stdout: '${jsonEncode(expectedResponse)}\n',
stdin: stdin,
));
final TestGoldenComparator comparator = TestGoldenComparator(
'shell',
() => FakeTestCompiler(),
processManager: processManager,
fileSystem: fileSystem,
logger: BufferLogger.test(),
webRenderer: WebRendererMode.html,
);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result, null);
await comparator.close();
expect(fileSystem.systemTempDirectory.listSync(recursive: true), isEmpty);
});
});
}
class FakeTestCompiler extends Fake implements TestCompiler {
@override
Future<String> compile(Uri mainDart) {
return Future<String>.value('compiler_output');
}
@override
Future<void> dispose() async { }
}