diff --git a/apps/sandbox/test/flutter_test_config.dart b/apps/sandbox/test/flutter_test_config.dart index 20daee9..04ffe4c 100644 --- a/apps/sandbox/test/flutter_test_config.dart +++ b/apps/sandbox/test/flutter_test_config.dart @@ -2,10 +2,14 @@ import 'dart:async'; import 'package:alchemist/alchemist.dart'; import 'package:alchemist_test_reporter/alchemist_test_reporter.dart'; +import 'package:allure_report/allure_report.dart'; Future<void> testExecutable(FutureOr<void> Function() testMain) { const isRunningInCi = bool.fromEnvironment('CI', defaultValue: false); - goldenTestRunner = GoldenTestRunnerWithReports(inner: goldenTestRunner); + goldenTestRunner = GoldenTestRunnerWithReports( + inner: goldenTestRunner, + onAttachmentCreated: Allure.diff, + ); return AlchemistConfig.runWithConfig( config: AlchemistConfig( diff --git a/packages/alchemist_test_reporter/CHANGELOG.md b/packages/alchemist_test_reporter/CHANGELOG.md index 5df4c7c..7fcde1f 100644 --- a/packages/alchemist_test_reporter/CHANGELOG.md +++ b/packages/alchemist_test_reporter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +- **feat:** add diff file generation + ## 1.0.1 - lower dart sdk version bound diff --git a/packages/alchemist_test_reporter/lib/src/golden_test_runner_with_reports.dart b/packages/alchemist_test_reporter/lib/src/golden_test_runner_with_reports.dart index a322cd0..93f8ba8 100644 --- a/packages/alchemist_test_reporter/lib/src/golden_test_runner_with_reports.dart +++ b/packages/alchemist_test_reporter/lib/src/golden_test_runner_with_reports.dart @@ -1,18 +1,30 @@ // ignore_for_file: implementation_imports import 'dart:io'; +import 'dart:math'; import 'package:path/path.dart' as p; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:alchemist/alchemist.dart'; import 'package:alchemist/src/golden_test_runner.dart'; +import 'package:image/image.dart' as img; +import 'package:diff_image2/diff_image2.dart'; + +typedef AttachmentCallback = void Function( + String expected, + String actual, + String diff, +); class GoldenTestRunnerWithReports extends GoldenTestRunner { final GoldenTestRunner _inner; + final AttachmentCallback? onAttachmentCreated; - GoldenTestRunnerWithReports({required GoldenTestRunner inner}) - : _inner = inner; + GoldenTestRunnerWithReports({ + required GoldenTestRunner inner, + this.onAttachmentCreated, + }) : _inner = inner; @override Future<void> run({ @@ -61,8 +73,15 @@ class GoldenTestRunnerWithReports extends GoldenTestRunner { 'failures', '${name}_testImage$extension', )); + final diffFile = File(p.join( + Directory.current.path, + 'test', + 'failures', + '${name}_maskedDiff$extension', + )); if (masterFile.existsSync() && testFile.existsSync()) { + final time = DateTime.now(); final subfolder = p.dirname(goldenPath.toString()).replaceAll('goldens/', ''); @@ -77,13 +96,69 @@ class GoldenTestRunnerWithReports extends GoldenTestRunner { subfolder, p.basename(masterFile.path)); final test = p.join(Directory.current.path, 'reports', 'failures', subfolder, p.basename(testFile.path)); + final diff = p.join(Directory.current.path, 'reports', 'failures', + subfolder, p.basename(diffFile.path)); masterFile.copySync(base); testFile.copySync(test); - print('event:attachment:$base'); - print('event:attachment:$test'); + try { + if (diffFile.existsSync()) { + diffFile.copySync(diff); + } else { + _fitImages(base, test); + _writeDiff(base, test, diff); + } + + print('Elapsed - ${DateTime.now().difference(time)}'); + + if (onAttachmentCreated case AttachmentCallback onAttachmentCreated) { + onAttachmentCreated(base, test, diff); + } else { + print('event:attachment:$base'); + print('event:attachment:$test'); + print('event:attachment:$diff'); + } + } catch (e) { + print(e.toString()); + } } } } } + +Future<void> _fitImages(String left, String right) async { + var (leftImage, rightImage) = ( + img.decodePng(File(left).readAsBytesSync()), + img.decodePng(File(right).readAsBytesSync()), + ); + + if (leftImage == null || rightImage == null) return; + + final newLeftImage = img.copyExpandCanvas( + leftImage, + newWidth: max(leftImage.width, rightImage.width), + newHeight: max(leftImage.height, rightImage.height), + position: img.ExpandCanvasPosition.topLeft, + backgroundColor: img.ColorRgba8(255, 255, 255, 255), + ); + final newRightImage = img.copyExpandCanvas( + rightImage, + newWidth: max(leftImage.width, rightImage.width), + newHeight: max(leftImage.height, rightImage.height), + position: img.ExpandCanvasPosition.topLeft, + backgroundColor: img.ColorRgba8(255, 255, 255, 255), + ); + + File(left).writeAsBytesSync(img.encodePng(newLeftImage)); + File(right).writeAsBytesSync(img.encodePng(newRightImage)); +} + +void _writeDiff(String left, String right, String diff) { + final leftFile = img.decodePng(File(left).readAsBytesSync()); + final rightFile = img.decodePng(File(right).readAsBytesSync()); + + var diffResult = DiffImage.compareFromMemory(leftFile!, rightFile!); + final png = img.encodePng(diffResult.diffImage); + File(diff).writeAsBytesSync(png); +} diff --git a/packages/alchemist_test_reporter/pubspec.yaml b/packages/alchemist_test_reporter/pubspec.yaml index 405e814..39ecb4e 100644 --- a/packages/alchemist_test_reporter/pubspec.yaml +++ b/packages/alchemist_test_reporter/pubspec.yaml @@ -1,6 +1,6 @@ name: alchemist_test_reporter description: Alchemist Test Reporter Addon -version: 1.0.1 +version: 1.1.0 homepage: https://github.com/rIIh/dart_test_reporter issue_tracker: https://github.com/rIIh/dart_test_reporter/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+%5Balchemist_test_reporter%5D @@ -17,6 +17,8 @@ dependencies: alchemist: ^0.7.0 test: ^1.24.0 path: ^1.0.0 + image: ^4.2.0 + diff_image2: ^1.2.1 dev_dependencies: diff --git a/packages/allure_report/CHANGELOG.md b/packages/allure_report/CHANGELOG.md index 9c51f4d..e3f42c3 100644 --- a/packages/allure_report/CHANGELOG.md +++ b/packages/allure_report/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.2.0 + +- **feat:** add diff comparison support + + ## 1.1.1 - lower dart sdk version bound diff --git a/packages/allure_report/lib/src/allure.dart b/packages/allure_report/lib/src/allure.dart index fa8849d..14f34cf 100644 --- a/packages/allure_report/lib/src/allure.dart +++ b/packages/allure_report/lib/src/allure.dart @@ -23,6 +23,9 @@ abstract final class Allure { // #endregion + static void diff(String expected, String actual, String diff) => + print('allure:event:diff:$expected:$actual:$diff'); + static void severity(Severity severity) => print('allure:event:severity:${severity.name}'); diff --git a/packages/allure_report/lib/src/allure_reporter.dart b/packages/allure_report/lib/src/allure_reporter.dart index 3b60f37..3ee89d4 100644 --- a/packages/allure_report/lib/src/allure_reporter.dart +++ b/packages/allure_report/lib/src/allure_reporter.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:convert'; +import 'package:allure_report/src/events/diff_event.dart'; import 'package:allure_report/src/models/allure_link.dart'; import 'package:allure_report/src/models/severity.dart'; import 'package:allure_report/src/test_report.dart'; +import 'package:mime/mime.dart'; import 'package:test_reporter/test_reporter.dart'; import 'package:universal_io/io.dart'; import 'package:uuid/data.dart'; @@ -36,7 +38,7 @@ class AllureReporter implements TestReporter { } @override - FutureOr<void> onEvent(TestEvent event) { + Future<void> onEvent(TestEvent event) async { switch (event) { case TestSuiteEvent event: suites[event.suite.id] = event.suite; @@ -59,6 +61,33 @@ class AllureReporter implements TestReporter { ], ); + case TestMessageEvent event + when event.messageType == 'print' && + DiffEvent.isEligible(event.message): + try { + final diffEvent = DiffEvent.fromMessage(event.message); + final expectedBytes = await File(diffEvent.expected).readAsBytes(); + final actualBytes = await File(diffEvent.actual).readAsBytes(); + final diffBytes = await File(diffEvent.diff).readAsBytes(); + + final id = Uuid().v4(); + final allureDiff = p.join(Directory.systemTemp.path, '$id.imagediff'); + File(allureDiff).writeAsString(jsonEncode({ + 'expected': 'data:image/png;base64,${base64.encode(expectedBytes)}', + 'actual': 'data:image/png;base64,${base64.encode(actualBytes)}', + 'diff': 'data:image/png;base64,${base64.encode(diffBytes)}', + })); + + tests[event.testID] = tests[event.testID]!.copyWith( + attachments: [ + ...tests[event.testID]!.attachments, + allureDiff, + ], + ); + } catch (e) { + print("[E]: Failed to create diff: $e"); + } + case TestMessageEvent event when event.messageType == 'print' && event.message.startsWith('allure:event:') && @@ -141,6 +170,11 @@ class AllureReporter implements TestReporter { attachments.add({ 'name': p.basenameWithoutExtension(path), 'source': p.basename(path), + 'type': switch (p.basename(path)) { + String filename when filename.endsWith('.imagediff') => + 'application/vnd.allure.image.diff', + String filename => lookupMimeType(filename) + }, }); } diff --git a/packages/allure_report/lib/src/events/diff_event.dart b/packages/allure_report/lib/src/events/diff_event.dart new file mode 100644 index 0000000..656e181 --- /dev/null +++ b/packages/allure_report/lib/src/events/diff_event.dart @@ -0,0 +1,22 @@ +class DiffEvent { + static const kTag = 'allure:event:diff'; + + static bool isEligible(String message) { + return message.startsWith('$kTag:') && + ':'.allMatches(message.replaceAll('$kTag:', '')).length == 2; + } + + DiffEvent(this.expected, this.actual, this.diff); + + factory DiffEvent.fromMessage(String message) { + final [expected, actual, diff] = message // + .replaceAll('$kTag:', '') + .split(':'); + + return DiffEvent(expected, actual, diff); + } + + final String expected; + final String actual; + final String diff; +} diff --git a/packages/allure_report/pubspec.yaml b/packages/allure_report/pubspec.yaml index a4f8259..4186cd6 100644 --- a/packages/allure_report/pubspec.yaml +++ b/packages/allure_report/pubspec.yaml @@ -1,6 +1,6 @@ name: allure_report description: Allure Report Adapter. Use it with [test_reporter](https://pub.dev/packages/test_reporter) package -version: 1.1.1 +version: 1.2.0 repository: https://github.com/rIIh/dart_test_reporter issue_tracker: https://github.com/rIIh/dart_test_reporter/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+%5Ballure_report%5D @@ -13,6 +13,7 @@ dependencies: test_reporter: ^1.0.0 universal_io: ^2.2.2 uuid: ^4.4.0 + mime: ^1.0.5 dev_dependencies: build_runner: ^2.4.11