From b18ab587062a7541f2dc8370041fc9851e4da853 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Fri, 21 Feb 2025 15:07:56 +0100 Subject: [PATCH 1/2] Compare screenshots, generate report of differences. --- pkg/pub_integration/pubspec.yaml | 1 + .../tool/compare_screenshots.dart | 125 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 pkg/pub_integration/tool/compare_screenshots.dart diff --git a/pkg/pub_integration/pubspec.yaml b/pkg/pub_integration/pubspec.yaml index 93ee7e3514..90f9e57c7e 100644 --- a/pkg/pub_integration/pubspec.yaml +++ b/pkg/pub_integration/pubspec.yaml @@ -17,5 +17,6 @@ dependencies: dev_dependencies: coverage: any # test already depends on it + markdown: ^7.3.0 shelf: ^1.4.0 test: ^1.16.5 diff --git a/pkg/pub_integration/tool/compare_screenshots.dart b/pkg/pub_integration/tool/compare_screenshots.dart new file mode 100644 index 0000000000..5f00ae3c3c --- /dev/null +++ b/pkg/pub_integration/tool/compare_screenshots.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. 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:io'; + +import 'package:markdown/markdown.dart'; +import 'package:path/path.dart' as p; + +/// Compares the screenshots from the previous and current test runs. +/// Uses imagemagick for image processing. +/// +/// dart `` `` `` ``. +Future main(List args) async { + final beforeFiles = await _list(args[0]); + final afterFiles = await _list(args[1]); + + final reportDir = Directory(args[2]); + await reportDir.create(recursive: true); + await _CompareTool(beforeFiles, afterFiles, reportDir)._compare(); +} + +class _CompareTool { + final Directory _reportDir; + final Map _beforeFiles; + final Map _afterFiles; + final _report = StringBuffer(); + + _CompareTool( + this._beforeFiles, + this._afterFiles, + this._reportDir, + ); + + Future _compare() async { + _report.writeln( + 'Screenshot comparison report generated at ${DateTime.now().toIso8601String()}.'); + + final newFiles = _afterFiles.keys + .where((key) => !_beforeFiles.containsKey(key)) + .toList(); + if (newFiles.isNotEmpty) { + _report.writeln([ + '', + '# New files', + newFiles.map((e) => '- `$e`').join('\n'), + ].join('\n\n')); + } + + final missingFiles = _beforeFiles.keys + .where((key) => !_afterFiles.containsKey(key)) + .toList(); + if (missingFiles.isNotEmpty) { + _report.writeln([ + '', + '# Missing files', + missingFiles.map((e) => '- `$e`').join('\n'), + ].join('\n\n')); + } + + for (final path in _afterFiles.keys) { + final after = _afterFiles[path]!; + if (!_beforeFiles.containsKey(path)) continue; + final before = _beforeFiles[path]!; + + // quick byte-content check + final afterBytes = await after.readAsBytes(); + final beforeBytes = await before.readAsBytes(); + if (afterBytes.length == beforeBytes.length && + afterBytes.indexed.every((e) => beforeBytes[e.$1] == e.$2)) { + continue; + } + + final relativeDir = p.dirname(path); + final basename = p.basenameWithoutExtension(path); + final diffPath = + p.join(_reportDir.path, relativeDir, '$basename-diff.png'); + await File(diffPath).parent.create(recursive: true); + + final pr = await Process.run('compare', [ + before.path, + after.path, + diffPath, + ]); + if (pr.exitCode == 0) continue; + + final beforeFile = + File(p.join(_reportDir.path, relativeDir, '$basename-before.png')); + await beforeFile.writeAsBytes(beforeBytes); + final afterFile = + File(p.join(_reportDir.path, relativeDir, '$basename-after.png')); + await afterFile.writeAsBytes(afterBytes); + + _report.writeln('`$path`\n'); + _report.writeln( + '![before](${p.join(relativeDir, '$basename-before.png')})\n'); + _report + .writeln('![after](${p.join(relativeDir, '$basename-after.png')})\n'); + _report + .writeln('![diff](${p.join(relativeDir, '$basename-diff.png')})\n'); + _report.writeln(); + } + + await _writeIndexHtml(); + } + + Future _writeIndexHtml() async { + await File(p.join(_reportDir.path, 'index.html')).writeAsString([ + '', + markdownToHtml(_report.toString()), + '', + ].join('\n')); + } +} + +Future> _list(String path) async { + final map = {}; + await for (final file in Directory(path).list(recursive: true)) { + if (file is! File) continue; + final rp = p.relative(file.path, from: path); + map[rp] = file; + } + return Map.fromEntries( + map.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); +} From 87ac3f7aaf58527264cc5f8c415ab517c992f3ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Fri, 21 Feb 2025 15:11:00 +0100 Subject: [PATCH 2/2] Update pkg/pub_integration/tool/compare_screenshots.dart Co-authored-by: Sigurd Meldgaard --- pkg/pub_integration/tool/compare_screenshots.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/pub_integration/tool/compare_screenshots.dart b/pkg/pub_integration/tool/compare_screenshots.dart index 5f00ae3c3c..0cb917ebc7 100644 --- a/pkg/pub_integration/tool/compare_screenshots.dart +++ b/pkg/pub_integration/tool/compare_screenshots.dart @@ -10,7 +10,7 @@ import 'package:path/path.dart' as p; /// Compares the screenshots from the previous and current test runs. /// Uses imagemagick for image processing. /// -/// dart `` `` `` ``. +/// `dart ` Future main(List args) async { final beforeFiles = await _list(args[0]); final afterFiles = await _list(args[1]);