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

View File

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

View File

@ -5,50 +5,74 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../convert.dart'; import '../convert.dart';
import '../web/compile.dart';
import 'test_compiler.dart'; import 'test_compiler.dart';
import 'test_config.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 /// The [`goldenFileComparator`](https://api.flutter.dev/flutter/flutter_test/goldenFileComparator.html)
/// file can contain arbitrary Dart code that depends on dart:ui. Thus it has to /// is configured using [`flutter_test_config.dart`](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html)
/// be executed in a `flutter_tester` environment. This helper class generates a /// and that file often contains arbitrary Dart code that depends on [`dart:ui`](https://api.flutter.dev/flutter/dart-ui/dart-ui-library.html).
/// Dart file configured with flutter_test_config.dart to perform the comparison ///
/// of golden files. /// This proxying comparator creates a minimal application that runs on a
class TestGoldenComparator { /// `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. /// Creates a [TestGoldenComparator] instance.
TestGoldenComparator(this.shellPath, this.compilerFactory, { TestGoldenComparator({
required String flutterTesterBinPath,
required TestCompiler Function() compilerFactory,
required Logger logger, required Logger logger,
required FileSystem fileSystem, required FileSystem fileSystem,
required ProcessManager processManager, required ProcessManager processManager,
required this.webRenderer, Map<String, String> environment = const <String, String>{},
}) : tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform.'), }) : _tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform.'),
_flutterTesterBinPath = flutterTesterBinPath,
_compilerFactory = compilerFactory,
_logger = logger, _logger = logger,
_fileSystem = fileSystem, _fileSystem = fileSystem,
_processManager = processManager; _processManager = processManager,
_environment = environment;
final String? shellPath; final String _flutterTesterBinPath;
final Directory tempDir; final Directory _tempDir;
final TestCompiler Function() compilerFactory;
final Logger _logger; final Logger _logger;
final FileSystem _fileSystem; final FileSystem _fileSystem;
final ProcessManager _processManager; final ProcessManager _processManager;
final WebRendererMode webRenderer; final Map<String, String> _environment;
final TestCompiler Function() _compilerFactory;
late final TestCompiler _compiler = _compilerFactory();
TestCompiler? _compiler;
TestGoldenComparatorProcess? _previousComparator; TestGoldenComparatorProcess? _previousComparator;
Uri? _previousTestUri; Uri? _previousTestUri;
/// Closes the comparator.
///
/// Any operation in process is terminated and the comparator can no longer be used.
Future<void> close() async { Future<void> close() async {
tempDir.deleteSync(recursive: true); _tempDir.deleteSync(recursive: true);
await _compiler?.dispose(); await _compiler.dispose();
await _previousComparator?.close(); await _previousComparator?.close();
} }
@ -73,33 +97,46 @@ class TestGoldenComparator {
Future<Process?> _startProcess(String testBootstrap) async { Future<Process?> _startProcess(String testBootstrap) async {
// Prepare the Dart file that will talk to us and start the test. // 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); await listenerFile.writeAsString(testBootstrap);
// Lazily create the compiler final String? output = await _compiler.compile(listenerFile.uri);
_compiler = _compiler ?? compilerFactory();
final String? output = await _compiler!.compile(listenerFile.uri);
if (output == null) { if (output == null) {
return null; return null;
} }
final List<String> command = <String>[ final List<String> command = <String>[
shellPath!, _flutterTesterBinPath,
'--disable-vm-service', '--disable-vm-service',
'--non-interactive', '--non-interactive',
'--packages=${_fileSystem.path.join('.dart_tool', 'package_config.json')}', '--packages=${_fileSystem.path.join('.dart_tool', 'package_config.json')}',
output, output,
]; ];
final Map<String, String> environment = <String, String>{ return _processManager.start(command, environment: _environment);
// Chrome is the only supported browser currently.
'FLUTTER_TEST_BROWSER': 'chrome',
'FLUTTER_WEB_RENDERER': webRenderer.name,
};
return _processManager.start(command, environment: environment);
} }
Future<String?> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool? updateGoldens) async { /// Compares the golden file designated by [goldenKey], relative to [testUri], to the provide [bytes].
final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(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); final TestGoldenComparatorProcess? process = await _processForTestFile(testUri);
if (process == null) { if (process == null) {
return 'process was 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 /// Represents a `flutter_tester` process started for golden comparison. Also
/// handles communication with the child process. /// handles communication with the child process.
class TestGoldenComparatorProcess { 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/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.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/test/flutter_web_platform.dart';
import 'package:flutter_tools/src/web/chrome.dart'; import 'package:flutter_tools/src/web/chrome.dart';
import 'package:flutter_tools/src/web/compile.dart'; import 'package:flutter_tools/src/web/compile.dart';
import 'package:flutter_tools/src/web/memory_fs.dart'; import 'package:flutter_tools/src/web/memory_fs.dart';
import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf.dart' as shelf;
import 'package:test/test.dart';
import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/fakes.dart'; import '../../src/fakes.dart';
@ -40,6 +41,7 @@ void main() {
late Artifacts artifacts; late Artifacts artifacts;
late ProcessManager processManager; late ProcessManager processManager;
late FakeOperatingSystemUtils operatingSystemUtils; late FakeOperatingSystemUtils operatingSystemUtils;
late Directory tempDir;
setUp(() { setUp(() {
fileSystem = MemoryFileSystem.test(); fileSystem = MemoryFileSystem.test();
@ -48,6 +50,7 @@ void main() {
artifacts = Artifacts.test(fileSystem: fileSystem); artifacts = Artifacts.test(fileSystem: fileSystem);
processManager = FakeProcessManager.empty(); processManager = FakeProcessManager.empty();
operatingSystemUtils = FakeOperatingSystemUtils(); operatingSystemUtils = FakeOperatingSystemUtils();
tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform_test.');
for (final HostArtifact artifact in <HostArtifact>[ for (final HostArtifact artifact in <HostArtifact>[
HostArtifact.webPrecompiledAmdCanvaskitAndHtmlSoundSdk, HostArtifact.webPrecompiledAmdCanvaskitAndHtmlSoundSdk,
@ -69,6 +72,10 @@ void main() {
} }
}); });
tearDown(() {
tryToDelete(tempDir);
});
testUsingContext( testUsingContext(
'FlutterWebPlatform serves the correct dart_sdk.js (amd module system) for the passed web renderer', 'FlutterWebPlatform serves the correct dart_sdk.js (amd module system) for the passed web renderer',
() async { () async {
@ -81,15 +88,16 @@ void main() {
logger: logger, logger: logger,
); );
final MockServer server = MockServer(); final MockServer server = MockServer();
fileSystem.directory('/test').createSync();
final FlutterWebPlatform webPlatform = await FlutterWebPlatform.start( final FlutterWebPlatform webPlatform = await FlutterWebPlatform.start(
'ProjectRoot', 'ProjectRoot',
flutterProject: FlutterProject.fromDirectoryTest(tempDir),
buildInfo: BuildInfo.debug, buildInfo: BuildInfo.debug,
webMemoryFS: WebMemoryFS(), webMemoryFS: WebMemoryFS(),
fileSystem: fileSystem, fileSystem: fileSystem,
buildDirectory: fileSystem.directory('build'), buildDirectory: fileSystem.directory('build'),
logger: logger, logger: logger,
chromiumLauncher: chromiumLauncher, chromiumLauncher: chromiumLauncher,
flutterTesterBinPath: artifacts.getArtifactPath(Artifact.flutterTester),
artifacts: artifacts, artifacts: artifacts,
processManager: processManager, processManager: processManager,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
@ -125,9 +133,9 @@ void main() {
logger: logger, logger: logger,
); );
final MockServer server = MockServer(); final MockServer server = MockServer();
fileSystem.directory('/test').createSync();
final FlutterWebPlatform webPlatform = await FlutterWebPlatform.start( final FlutterWebPlatform webPlatform = await FlutterWebPlatform.start(
'ProjectRoot', 'ProjectRoot',
flutterProject: FlutterProject.fromDirectoryTest(tempDir),
buildInfo: const BuildInfo( buildInfo: const BuildInfo(
BuildMode.debug, BuildMode.debug,
'', '',
@ -140,6 +148,7 @@ void main() {
buildDirectory: fileSystem.directory('build'), buildDirectory: fileSystem.directory('build'),
logger: logger, logger: logger,
chromiumLauncher: chromiumLauncher, chromiumLauncher: chromiumLauncher,
flutterTesterBinPath: artifacts.getArtifactPath(Artifact.flutterTester),
artifacts: artifacts, artifacts: artifacts,
processManager: processManager, processManager: processManager,
webRenderer: WebRendererMode.canvaskit, 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/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/globals.dart' as globals; 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/common.dart';
import '../../src/fakes.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 { }
}