blob: 2b13ec65fbbc9591bdf9649f82f5ffc3b7be2707 [file] [log] [blame]
// Copyright 2013 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:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:path/path.dart' as path;
import 'package:skia_gold_client/skia_gold_client.dart';
import 'impeller_enabled.dart';
const String _kSkiaGoldWorkDirectoryKey = 'kSkiaGoldWorkDirectory';
/// A helper for doing image comparison (golden) tests.
///
/// Contains utilities for comparing two images in memory that are expected to
/// be identical, or for adding images to Skia gold for comparison.
class ImageComparer {
ImageComparer._({required SkiaGoldClient client}) : _client = client;
// Avoid talking to Skia gold for the force-multithreading variants.
static bool get _useSkiaGold => !Platform.executableArguments.contains('--force-multithreading');
/// Creates an image comparer and authorizes.
static Future<ImageComparer> create({bool verbose = false}) async {
const String workDirectoryPath = String.fromEnvironment(_kSkiaGoldWorkDirectoryKey);
if (workDirectoryPath.isEmpty) {
throw UnsupportedError('Using ImageComparer requries defining kSkiaGoldWorkDirectoryKey.');
}
final Directory workDirectory = Directory(
impellerEnabled ? '${workDirectoryPath}_iplr' : workDirectoryPath,
)..createSync();
final Map<String, String> dimensions = <String, String>{
'impeller_enabled': impellerEnabled.toString(),
};
final SkiaGoldClient client =
SkiaGoldClient.isAvailable() && _useSkiaGold
? SkiaGoldClient(workDirectory, dimensions: dimensions, verbose: verbose)
: _FakeSkiaGoldClient(workDirectory, dimensions, verbose: verbose);
await client.auth();
return ImageComparer._(client: client);
}
final SkiaGoldClient _client;
/// Adds an [Image] to Skia Gold for comparison.
///
/// The [fileName] must be unique.
Future<void> addGoldenImage(Image image, String fileName) async {
final ByteData data = (await image.toByteData(format: ImageByteFormat.png))!;
final File file = File(path.join(_client.workDirectory.path, fileName))
..writeAsBytesSync(data.buffer.asUint8List());
await _client.addImg(fileName, file, screenshotSize: image.width * image.height).catchError((
dynamic error,
) {
print('Skia gold comparison failed: $error');
throw Exception('Failed comparison: $fileName');
});
}
Future<bool> fuzzyCompareImages(Image golden, Image testImage) async {
if (golden.width != testImage.width || golden.height != testImage.height) {
return false;
}
int getPixel(ByteData data, int x, int y) => data.getUint32((x + y * golden.width) * 4);
final ByteData goldenData = (await golden.toByteData())!;
final ByteData testImageData = (await testImage.toByteData())!;
for (int y = 0; y < golden.height; y++) {
for (int x = 0; x < golden.width; x++) {
if (getPixel(goldenData, x, y) != getPixel(testImageData, x, y)) {
return false;
}
}
}
return true;
}
}
// TODO(dnfield): add local comparison against baseline,
// https://github.com/flutter/flutter/issues/136831
class _FakeSkiaGoldClient implements SkiaGoldClient {
_FakeSkiaGoldClient(this.workDirectory, this.dimensions, {this.verbose = false});
@override
final Directory workDirectory;
@override
final Map<String, String> dimensions;
@override
final bool verbose;
@override
Future<void> auth() async {}
@override
Future<void> addImg(
String testName,
File goldenFile, {
double differentPixelsRate = 0.01,
int pixelColorDelta = 0,
required int screenshotSize,
}) async {}
@override
dynamic noSuchMethod(Invocation invocation) {
throw UnimplementedError(invocation.memberName.toString().split('"')[1]);
}
}