| // Copyright 2018 The Chromium 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:async'; |
| import 'dart:io' as io; |
| import 'dart:typed_data'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:platform/platform.dart'; |
| import 'package:process/process.dart'; |
| |
| const String _kFlutterRootKey = 'FLUTTER_ROOT'; |
| |
| /// Main method that can be used in a `flutter_test_config.dart` file to set |
| /// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that |
| /// works for the current test. |
| Future<void> main(FutureOr<void> testMain()) async { |
| goldenFileComparator = await FlutterGoldenFileComparator.fromDefaultComparator(); |
| await testMain(); |
| } |
| |
| /// A golden file comparator specific to the `flutter/flutter` repository. |
| /// |
| /// Within the https://github.com/flutter/flutter repository, it's important |
| /// not to check-in binaries in order to keep the size of the repository to a |
| /// minimum. To satisfy this requirement, this comparator retrieves the golden |
| /// files from a sibling repository, `flutter/goldens`. |
| /// |
| /// This comparator will locally clone the `flutter/goldens` repository into |
| /// the `$FLUTTER_ROOT/bin/cache/pkg/goldens` folder, then perform the comparison against |
| /// the files therein. |
| class FlutterGoldenFileComparator implements GoldenFileComparator { |
| /// Creates a [FlutterGoldenFileComparator] that will resolve golden file |
| /// URIs relative to the specified [basedir]. |
| /// |
| /// The [fs] parameter exists for testing purposes only. |
| @visibleForTesting |
| FlutterGoldenFileComparator( |
| this.basedir, { |
| this.fs: const LocalFileSystem(), |
| }); |
| |
| /// The directory to which golden file URIs will be resolved in [compare] and [update]. |
| final Uri basedir; |
| |
| /// The file system used to perform file access. |
| @visibleForTesting |
| final FileSystem fs; |
| |
| /// Creates a new [FlutterGoldenFileComparator] that mirrors the relative |
| /// path resolution of the default [goldenFileComparator]. |
| /// |
| /// By the time the future completes, the clone of the `flutter/goldens` |
| /// repository is guaranteed to be ready use. |
| /// |
| /// The [goldens] and [defaultComparator] parameters are visible for testing |
| /// purposes only. |
| static Future<FlutterGoldenFileComparator> fromDefaultComparator({ |
| GoldensClient goldens, |
| LocalFileComparator defaultComparator, |
| }) async { |
| defaultComparator ??= goldenFileComparator; |
| |
| // Prepare the goldens repo. |
| goldens ??= new GoldensClient(); |
| await goldens.prepare(); |
| |
| // Calculate the appropriate basedir for the current test context. |
| final FileSystem fs = goldens.fs; |
| final Directory testDirectory = fs.directory(defaultComparator.basedir); |
| final String testDirectoryRelativePath = fs.path.relative(testDirectory.path, from: goldens.flutterRoot.path); |
| return new FlutterGoldenFileComparator(goldens.repositoryRoot.childDirectory(testDirectoryRelativePath).uri); |
| } |
| |
| @override |
| Future<bool> compare(Uint8List imageBytes, Uri golden) async { |
| final File goldenFile = _getGoldenFile(golden); |
| if (!goldenFile.existsSync()) { |
| throw new TestFailure('Could not be compared against non-existent file: "$golden"'); |
| } |
| final List<int> goldenBytes = await goldenFile.readAsBytes(); |
| // TODO(tvolkert): Improve the intelligence of this comparison. |
| return const ListEquality<int>().equals(goldenBytes, imageBytes); |
| } |
| |
| @override |
| Future<void> update(Uri golden, Uint8List imageBytes) async { |
| final File goldenFile = _getGoldenFile(golden); |
| await goldenFile.parent.create(recursive: true); |
| await goldenFile.writeAsBytes(imageBytes, flush: true); |
| } |
| |
| File _getGoldenFile(Uri uri) { |
| return fs.directory(basedir).childFile(fs.file(uri).path); |
| } |
| } |
| |
| /// A class that represents a clone of the https://github.com/flutter/goldens |
| /// repository, nested within the `bin/cache` directory of the caller's Flutter |
| /// repository. |
| @visibleForTesting |
| class GoldensClient { |
| GoldensClient({ |
| this.fs: const LocalFileSystem(), |
| this.platform: const LocalPlatform(), |
| this.process: const LocalProcessManager(), |
| }); |
| |
| final FileSystem fs; |
| final Platform platform; |
| final ProcessManager process; |
| |
| RandomAccessFile _lock; |
| |
| Directory get flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]); |
| |
| Directory get repositoryRoot => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'goldens')); |
| |
| /// Prepares the local clone of the `flutter/goldens` repository for golden |
| /// file testing. |
| /// |
| /// This ensures that the goldens repository has been cloned into its |
| /// expected location within `bin/cache` and that it is synced to the Git |
| /// revision specified in `bin/internal/goldens.version`. |
| /// |
| /// While this is preparing the repository, it obtains a file lock such that |
| /// [GoldensClient] instances in other processes or isolates will not |
| /// duplicate the work that this is doing. |
| Future<void> prepare() async { |
| final String goldensCommit = await _getGoldensCommit(); |
| String currentCommit = await _getCurrentCommit(); |
| if (currentCommit != goldensCommit) { |
| await _obtainLock(); |
| try { |
| // Check the current commit again now that we have the lock. |
| currentCommit = await _getCurrentCommit(); |
| if (currentCommit != goldensCommit) { |
| if (currentCommit == null) { |
| await _initRepository(); |
| } |
| await _syncTo(goldensCommit); |
| } |
| } finally { |
| await _releaseLock(); |
| } |
| } |
| } |
| |
| Future<String> _getGoldensCommit() async { |
| final File versionFile = flutterRoot.childFile(fs.path.join('bin', 'internal', 'goldens.version')); |
| return (await versionFile.readAsString()).trim(); |
| } |
| |
| Future<String> _getCurrentCommit() async { |
| if (!repositoryRoot.existsSync()) { |
| return null; |
| } else { |
| final io.ProcessResult revParse = await process.run( |
| <String>['git', 'rev-parse', 'HEAD'], |
| workingDirectory: repositoryRoot.path, |
| ); |
| return revParse.exitCode == 0 ? revParse.stdout.trim() : null; |
| } |
| } |
| |
| Future<void> _initRepository() async { |
| await repositoryRoot.create(recursive: true); |
| await _runCommands( |
| <String>[ |
| 'git init', |
| 'git remote add upstream https://github.com/flutter/goldens.git', |
| ], |
| workingDirectory: repositoryRoot, |
| ); |
| } |
| |
| Future<void> _syncTo(String commit) async { |
| await _runCommands( |
| <String>[ |
| 'git pull upstream master', |
| 'git fetch upstream $commit', |
| 'git reset --hard FETCH_HEAD', |
| ], |
| workingDirectory: repositoryRoot, |
| ); |
| } |
| |
| Future<void> _runCommands( |
| List<String> commands, { |
| Directory workingDirectory, |
| }) async { |
| for (String command in commands) { |
| final List<String> parts = command.split(' '); |
| final io.ProcessResult result = await process.run( |
| parts, |
| workingDirectory: workingDirectory?.path, |
| ); |
| if (result.exitCode != 0) { |
| throw new NonZeroExitCode(result.exitCode, result.stderr); |
| } |
| } |
| } |
| |
| Future<void> _obtainLock() async { |
| final File lockFile = flutterRoot.childFile(fs.path.join('bin', 'cache', 'goldens.lockfile')); |
| await lockFile.create(recursive: true); |
| _lock = await lockFile.open(mode: io.FileMode.WRITE); // ignore: deprecated_member_use |
| await _lock.lock(io.FileLock.BLOCKING_EXCLUSIVE); // ignore: deprecated_member_use |
| } |
| |
| Future<void> _releaseLock() async { |
| await _lock.close(); |
| _lock = null; |
| } |
| } |
| |
| /// Exception that signals a process' exit with a non-zero exit code. |
| class NonZeroExitCode implements Exception { |
| const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0); |
| |
| final int exitCode; |
| final String stderr; |
| |
| @override |
| String toString() { |
| return 'Exit code $exitCode: $stderr'; |
| } |
| } |