Skip to content

Commit 23ef073

Browse files
authored
Compare screenshots, generate report of differences. (#8579)
1 parent 84318ee commit 23ef073

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

pkg/pub_integration/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ dependencies:
1717

1818
dev_dependencies:
1919
coverage: any # test already depends on it
20+
markdown: ^7.3.0
2021
shelf: ^1.4.0
2122
test: ^1.16.5
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:markdown/markdown.dart';
8+
import 'package:path/path.dart' as p;
9+
10+
/// Compares the screenshots from the previous and current test runs.
11+
/// Uses imagemagick for image processing.
12+
///
13+
/// `dart <script.dart> <before-dir> <after-dir> <report-dir>`
14+
Future<void> main(List<String> args) async {
15+
final beforeFiles = await _list(args[0]);
16+
final afterFiles = await _list(args[1]);
17+
18+
final reportDir = Directory(args[2]);
19+
await reportDir.create(recursive: true);
20+
await _CompareTool(beforeFiles, afterFiles, reportDir)._compare();
21+
}
22+
23+
class _CompareTool {
24+
final Directory _reportDir;
25+
final Map<String, File> _beforeFiles;
26+
final Map<String, File> _afterFiles;
27+
final _report = StringBuffer();
28+
29+
_CompareTool(
30+
this._beforeFiles,
31+
this._afterFiles,
32+
this._reportDir,
33+
);
34+
35+
Future<void> _compare() async {
36+
_report.writeln(
37+
'Screenshot comparison report generated at ${DateTime.now().toIso8601String()}.');
38+
39+
final newFiles = _afterFiles.keys
40+
.where((key) => !_beforeFiles.containsKey(key))
41+
.toList();
42+
if (newFiles.isNotEmpty) {
43+
_report.writeln([
44+
'',
45+
'# New files',
46+
newFiles.map((e) => '- `$e`').join('\n'),
47+
].join('\n\n'));
48+
}
49+
50+
final missingFiles = _beforeFiles.keys
51+
.where((key) => !_afterFiles.containsKey(key))
52+
.toList();
53+
if (missingFiles.isNotEmpty) {
54+
_report.writeln([
55+
'',
56+
'# Missing files',
57+
missingFiles.map((e) => '- `$e`').join('\n'),
58+
].join('\n\n'));
59+
}
60+
61+
for (final path in _afterFiles.keys) {
62+
final after = _afterFiles[path]!;
63+
if (!_beforeFiles.containsKey(path)) continue;
64+
final before = _beforeFiles[path]!;
65+
66+
// quick byte-content check
67+
final afterBytes = await after.readAsBytes();
68+
final beforeBytes = await before.readAsBytes();
69+
if (afterBytes.length == beforeBytes.length &&
70+
afterBytes.indexed.every((e) => beforeBytes[e.$1] == e.$2)) {
71+
continue;
72+
}
73+
74+
final relativeDir = p.dirname(path);
75+
final basename = p.basenameWithoutExtension(path);
76+
final diffPath =
77+
p.join(_reportDir.path, relativeDir, '$basename-diff.png');
78+
await File(diffPath).parent.create(recursive: true);
79+
80+
final pr = await Process.run('compare', [
81+
before.path,
82+
after.path,
83+
diffPath,
84+
]);
85+
if (pr.exitCode == 0) continue;
86+
87+
final beforeFile =
88+
File(p.join(_reportDir.path, relativeDir, '$basename-before.png'));
89+
await beforeFile.writeAsBytes(beforeBytes);
90+
final afterFile =
91+
File(p.join(_reportDir.path, relativeDir, '$basename-after.png'));
92+
await afterFile.writeAsBytes(afterBytes);
93+
94+
_report.writeln('`$path`\n');
95+
_report.writeln(
96+
'![before](${p.join(relativeDir, '$basename-before.png')})\n');
97+
_report
98+
.writeln('![after](${p.join(relativeDir, '$basename-after.png')})\n');
99+
_report
100+
.writeln('![diff](${p.join(relativeDir, '$basename-diff.png')})\n');
101+
_report.writeln();
102+
}
103+
104+
await _writeIndexHtml();
105+
}
106+
107+
Future<void> _writeIndexHtml() async {
108+
await File(p.join(_reportDir.path, 'index.html')).writeAsString([
109+
'<html><body>',
110+
markdownToHtml(_report.toString()),
111+
'</body></html>',
112+
].join('\n'));
113+
}
114+
}
115+
116+
Future<Map<String, File>> _list(String path) async {
117+
final map = <String, File>{};
118+
await for (final file in Directory(path).list(recursive: true)) {
119+
if (file is! File) continue;
120+
final rp = p.relative(file.path, from: path);
121+
map[rp] = file;
122+
}
123+
return Map.fromEntries(
124+
map.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
125+
}

0 commit comments

Comments
 (0)