Skip to content

Commit

Permalink
Split GoldensClient out of flutter_goldens.dart (flutter#20102)
Browse files Browse the repository at this point in the history
This enables using GoldensClient from the flutter driver file for the
embedded Android views test.

flutter_goldens.dart has a transitive dependency on dart:ui so it cannot
be included in non Flutter apps. Splitting it out allows us to use it
from driver tests.

This also removes the visibleForTesting annotation from GoldensClient.
  • Loading branch information
amirh authored Aug 2, 2018
1 parent 506cf3c commit ed533e9
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 171 deletions.
178 changes: 178 additions & 0 deletions packages/flutter_goldens/lib/client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';

// If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter

const String _kFlutterRootKey = 'FLUTTER_ROOT';

/// 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.
class GoldensClient {
/// Create a handle to a local clone of the goldens repository.
GoldensClient({
this.fs = const LocalFileSystem(),
this.platform = const LocalPlatform(),
this.process = const LocalProcessManager(),
});

/// The file system to use for storing the local clone of the repository.
///
/// This is useful in tests, where a local file system (the default) can
/// be replaced by a memory file system.
final FileSystem fs;

/// A wrapper for the [dart:io.Platform] API.
///
/// This is useful in tests, where the system platform (the default) can
/// be replaced by a mock platform instance.
final Platform platform;

/// A controller for launching subprocesses.
///
/// This is useful in tests, where the real process manager (the default)
/// can be replaced by a mock process manager that doesn't really create
/// subprocesses.
final ProcessManager process;

RandomAccessFile _lock;

/// The local [Directory] where the Flutter repository is hosted.
///
/// Uses the [fs] file system.
Directory get flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]);

/// The local [Directory] where the goldens repository is hosted.
///
/// Uses the [fs] file system.
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',
'git remote set-url --push upstream git@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);
await _lock.lock(io.FileLock.blockingExclusive);
}

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 {
/// Create an exception that represents a non-zero exit code.
///
/// The first argument must be non-zero.
const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0);

/// The code that the process will signal to th eoperating system.
///
/// By definiton, this is not zero.
final int exitCode;

/// The message to show on standard error.
final String stderr;

@override
String toString() {
return 'Exit code $exitCode: $stderr';
}
}
173 changes: 2 additions & 171 deletions packages/flutter_goldens/lib/flutter_goldens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,16 @@
// 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';

// If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter

const String _kFlutterRootKey = 'FLUTTER_ROOT';
import 'client.dart';
export 'client.dart';

/// Main method that can be used in a `flutter_test_config.dart` file to set
/// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that
Expand Down Expand Up @@ -103,166 +97,3 @@ class FlutterGoldenFileComparator implements GoldenFileComparator {
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 {
/// Create a handle to a local clone of the goldens repository.
GoldensClient({
this.fs = const LocalFileSystem(),
this.platform = const LocalPlatform(),
this.process = const LocalProcessManager(),
});

/// The file system to use for storing the local clone of the repository.
///
/// This is useful in tests, where a local file system (the default) can
/// be replaced by a memory file system.
final FileSystem fs;

/// A wrapper for the [dart:io.Platform] API.
///
/// This is useful in tests, where the system platform (the default) can
/// be replaced by a mock platform instance.
final Platform platform;

/// A controller for launching subprocesses.
///
/// This is useful in tests, where the real process manager (the default)
/// can be replaced by a mock process manager that doesn't really create
/// subprocesses.
final ProcessManager process;

RandomAccessFile _lock;

/// The local [Directory] where the Flutter repository is hosted.
///
/// Uses the [fs] file system.
Directory get flutterRoot => fs.directory(platform.environment[_kFlutterRootKey]);

/// The local [Directory] where the goldens repository is hosted.
///
/// Uses the [fs] file system.
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',
'git remote set-url --push upstream git@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);
await _lock.lock(io.FileLock.blockingExclusive);
}

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 {
/// Create an exception that represents a non-zero exit code.
///
/// The first argument must be non-zero.
const NonZeroExitCode(this.exitCode, this.stderr) : assert(exitCode != 0);

/// The code that the process will signal to th eoperating system.
///
/// By definiton, this is not zero.
final int exitCode;

/// The message to show on standard error.
final String stderr;

@override
String toString() {
return 'Exit code $exitCode: $stderr';
}
}

0 comments on commit ed533e9

Please sign in to comment.