1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// 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:typed_data';
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.
///
/// 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 {
/// Creates a [TestGoldenComparator] instance.
TestGoldenComparator(this.shellPath, this.compilerFactory, {
required Logger logger,
required FileSystem fileSystem,
required ProcessManager processManager,
required this.webRenderer,
}) : tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform.'),
_logger = logger,
_fileSystem = fileSystem,
_processManager = processManager;
final String? shellPath;
final Directory tempDir;
final TestCompiler Function() compilerFactory;
final Logger _logger;
final FileSystem _fileSystem;
final ProcessManager _processManager;
final WebRendererMode webRenderer;
TestCompiler? _compiler;
TestGoldenComparatorProcess? _previousComparator;
Uri? _previousTestUri;
Future<void> close() async {
tempDir.deleteSync(recursive: true);
await _compiler?.dispose();
await _previousComparator?.close();
}
/// Start golden comparator in a separate process. Start one file per test file
/// to reduce the overhead of starting `flutter_tester`.
Future<TestGoldenComparatorProcess?> _processForTestFile(Uri testUri) async {
if (testUri == _previousTestUri) {
return _previousComparator!;
}
final String bootstrap = TestGoldenComparatorProcess.generateBootstrap(_fileSystem.file(testUri), testUri, logger: _logger);
final Process? process = await _startProcess(bootstrap);
if (process == null) {
return null;
}
unawaited(_previousComparator?.close());
_previousComparator = TestGoldenComparatorProcess(process, logger: _logger);
_previousTestUri = testUri;
return _previousComparator!;
}
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');
await listenerFile.writeAsString(testBootstrap);
// Lazily create the compiler
_compiler = _compiler ?? compilerFactory();
final String? output = await _compiler!.compile(listenerFile.uri);
if (output == null) {
return null;
}
final List<String> command = <String>[
shellPath!,
'--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 == WebRendererMode.html ? 'html' : 'canvaskit',
};
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);
final TestGoldenComparatorProcess? process = await _processForTestFile(testUri);
if (process == null) {
return 'process was null';
}
process.sendCommand(imageFile, goldenKey, updateGoldens);
final Map<String, dynamic> result = await process.getResponse();
return (result['success'] as bool) ? null : ((result['message'] as String?) ?? 'does not match');
}
}
/// Represents a `flutter_tester` process started for golden comparison. Also
/// handles communication with the child process.
class TestGoldenComparatorProcess {
/// Creates a [TestGoldenComparatorProcess] backed by [process].
TestGoldenComparatorProcess(this.process, {required Logger logger}) : _logger = logger {
// Pipe stdout and stderr to printTrace and printError.
// Also parse stdout as a stream of JSON objects.
streamIterator = StreamIterator<Map<String, dynamic>>(
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.where((String line) {
logger.printTrace('<<< $line');
return line.isNotEmpty && line[0] == '{';
})
.map<dynamic>(jsonDecode)
.cast<Map<String, dynamic>>());
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.forEach((String line) {
logger.printError('<<< $line');
});
}
final Logger _logger;
final Process process;
late StreamIterator<Map<String, dynamic>> streamIterator;
Future<void> close() async {
process.kill();
await process.exitCode;
}
void sendCommand(File imageFile, Uri? goldenKey, bool? updateGoldens) {
final Object command = jsonEncode(<String, dynamic>{
'imageFile': imageFile.path,
'key': goldenKey.toString(),
'update': updateGoldens,
});
_logger.printTrace('Preparing to send command: $command');
process.stdin.writeln(command);
}
Future<Map<String, dynamic>> getResponse() async {
final bool available = await streamIterator.moveNext();
assert(available);
return streamIterator.current;
}
static String generateBootstrap(File testFile, Uri testUri, {required Logger logger}) {
final File? testConfigFile = findTestConfigFile(testFile, logger);
// Generate comparator process for the file.
return '''
import 'dart:convert'; // flutter_ignore: dart_convert_import
import 'dart:io'; // flutter_ignore: dart_io_import
import 'package:flutter_test/flutter_test.dart';
${testConfigFile != null ? "import '${Uri.file(testConfigFile.path)}' as test_config;" : ""}
void main() async {
LocalFileComparator comparator = LocalFileComparator(Uri.parse('$testUri'));
goldenFileComparator = comparator;
${testConfigFile != null ? 'test_config.testExecutable(() async {' : ''}
final commands = stdin
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.map<dynamic>(jsonDecode);
await for (final dynamic command in commands) {
if (command is Map<String, dynamic>) {
File imageFile = File(command['imageFile'] as String);
Uri goldenKey = Uri.parse(command['key'] as String);
bool update = command['update'] as bool;
final bytes = await File(imageFile.path).readAsBytes();
if (update) {
await goldenFileComparator.update(goldenKey, bytes);
print(jsonEncode({'success': true}));
} else {
try {
bool success = await goldenFileComparator.compare(bytes, goldenKey);
print(jsonEncode({'success': success}));
} on Exception catch (ex) {
print(jsonEncode({'success': false, 'message': '\$ex'}));
}
}
} else {
print('object type is not right');
}
}
${testConfigFile != null ? '});' : ''}
}
''';
}
}