blob: 84addf8e20d69b21c2113753758fb7bfcc89b1f6 [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]);
}
}