| // Copyright 2019 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:convert'; |
| import 'dart:io' as io; |
| |
| import 'package:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:platform/platform.dart'; |
| import 'package:process/process.dart'; |
| |
| import 'package:flutter_goldens_client/client.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 |
| |
| // TODO(Piinks): This file will replace ./client.dart when transition to Skia |
| // Gold testing is complete |
| |
| const String _kGoldctlKey = 'GOLDCTL'; |
| const String _kServiceAccountKey = 'GOLD_SERVICE_ACCOUNT'; |
| |
| /// An extension of the [GoldensClient] class that interfaces with Skia Gold |
| /// for golden file testing. |
| class SkiaGoldClient extends GoldensClient { |
| SkiaGoldClient({ |
| FileSystem fs = const LocalFileSystem(), |
| ProcessManager process = const LocalProcessManager(), |
| Platform platform = const LocalPlatform(), |
| }) : super( |
| fs: fs, |
| process: process, |
| platform: platform, |
| ); |
| |
| /// The local [Directory] within the [comparisonRoot] for the current test |
| /// context. In this directory, the client will create image and json files |
| /// for the goldctl tool to use. |
| /// |
| /// This is informed by the [FlutterGoldenFileComparator] [basedir]. It cannot |
| /// be null. |
| Directory _workDirectory; |
| |
| /// The path to the local [Directory] where the goldctl tool is hosted. |
| /// |
| /// Uses the [platform] environment in this implementation. |
| String get _goldctl => platform.environment[_kGoldctlKey]; |
| |
| /// The path to the local [Directory] where the service account key is |
| /// hosted. |
| /// |
| /// Uses the [platform] environment in this implementation. |
| String get _serviceAccount => platform.environment[_kServiceAccountKey]; |
| |
| /// Prepares the local work space for golden file testing and calls the |
| /// goldctl `auth` command. |
| /// |
| /// This ensures that the goldctl tool is authorized and ready for testing. It |
| /// will only be called once for each instance of |
| /// [FlutterSkiaGoldFileComparator]. |
| /// |
| /// The [workDirectory] parameter specifies the current directory that golden |
| /// tests are executing in, relative to the library of the given test. It is |
| /// informed by the basedir of the [FlutterSkiaGoldFileComparator]. |
| Future<void> auth(Directory workDirectory) async { |
| assert(workDirectory != null); |
| _workDirectory = workDirectory; |
| if (_clientIsAuthorized()) |
| return; |
| |
| final File authorization = _workDirectory.childFile('serviceAccount.json'); |
| await authorization.writeAsString(_serviceAccount); |
| |
| final List<String> authArguments = <String>[ |
| 'auth', |
| '--service-account', authorization.path, |
| '--work-dir', _workDirectory.childDirectory('temp').path, |
| ]; |
| |
| final io.ProcessResult authResults = await io.Process.run( |
| _goldctl, |
| authArguments, |
| ); |
| |
| if (authResults.exitCode != 0) { |
| final StringBuffer buf = StringBuffer() |
| ..writeln('Flutter + Skia Gold auth failed.') |
| ..writeln('stdout: ${authResults.stdout}') |
| ..writeln('stderr: ${authResults.stderr}'); |
| throw NonZeroExitCode(authResults.exitCode, buf.toString()); |
| } |
| } |
| |
| /// Executes the `imgtest init` command in the goldctl tool. |
| /// |
| /// The `imgtest` command collects and uploads test results to the Skia Gold |
| /// backend, the `init` argument initializes the testing environment. |
| Future<void> imgtestInit() async { |
| final String commitHash = await _getCurrentCommit(); |
| final File keys = _workDirectory.childFile('keys.json'); |
| final File failures = _workDirectory.childFile('failures.json'); |
| |
| await keys.writeAsString(_getKeysJSON()); |
| await failures.create(); |
| |
| final List<String> imgtestInitArguments = <String>[ |
| 'imgtest', 'init', |
| '--instance', 'flutter', |
| '--work-dir', _workDirectory.childDirectory('temp').path, |
| '--commit', commitHash, |
| '--keys-file', keys.path, |
| '--failure-file', failures.path, |
| '--passfail', |
| ]; |
| |
| if (imgtestInitArguments.contains(null)) { |
| final StringBuffer buf = StringBuffer(); |
| buf.writeln('Null argument for Skia Gold imgtest init:'); |
| imgtestInitArguments.forEach(buf.writeln); |
| throw NonZeroExitCode(1, buf.toString()); |
| } |
| |
| final io.ProcessResult imgtestInitResult = await io.Process.run( |
| _goldctl, |
| imgtestInitArguments, |
| ); |
| |
| if (imgtestInitResult.exitCode != 0) { |
| final StringBuffer buf = StringBuffer() |
| ..writeln('Flutter + Skia Gold imgtest init failed.') |
| ..writeln('stdout: ${imgtestInitResult.stdout}') |
| ..writeln('stderr: ${imgtestInitResult.stderr}'); |
| throw NonZeroExitCode(imgtestInitResult.exitCode, buf.toString()); |
| } |
| } |
| |
| /// Executes the `imgtest add` command in the goldctl tool. |
| /// |
| /// The `imgtest` command collects and uploads test results to the Skia Gold |
| /// backend, the `add` argument uploads the current image test. A response is |
| /// returned from the invocation of this command that indicates a pass or fail |
| /// result. |
| /// |
| /// The testName and goldenFile parameters reference the current comparison |
| /// being evaluated by the [FlutterSkiaGoldFileComparator]. |
| Future<bool> imgtestAdd(String testName, File goldenFile) async { |
| assert(testName != null); |
| assert(goldenFile != null); |
| |
| final List<String> imgtestArguments = <String>[ |
| 'imgtest', 'add', |
| '--work-dir', _workDirectory.childDirectory('temp').path, |
| '--test-name', testName.split(path.extension(testName.toString()))[0], |
| '--png-file', goldenFile.path, |
| ]; |
| |
| await io.Process.run( |
| _goldctl, |
| imgtestArguments, |
| ); |
| |
| // TODO(Piinks): Comment on PR if triage is needed, https://github.com/flutter/flutter/issues/34673 |
| // So as not to turn the tree red in this initial implementation, this will |
| // return true for now. |
| // The ProcessResult that returns from line 157 contains the pass/fail |
| // result of the test & links to the dashboard and diffs. |
| return true; |
| } |
| |
| /// Returns the current commit hash of the Flutter repository. |
| Future<String> _getCurrentCommit() async { |
| if (!flutterRoot.existsSync()) { |
| final StringBuffer buf = StringBuffer() |
| ..writeln('Flutter root could not be found: $flutterRoot'); |
| throw NonZeroExitCode(1, buf.toString()); |
| } else { |
| final io.ProcessResult revParse = await process.run( |
| <String>['git', 'rev-parse', 'HEAD'], |
| workingDirectory: flutterRoot.path, |
| ); |
| return revParse.exitCode == 0 ? revParse.stdout.trim() : null; |
| } |
| } |
| |
| /// Returns a JSON String with keys value pairs used to uniquely identify the |
| /// configuration that generated the given golden file. |
| /// |
| /// Currently, the only key value pair being tracked is the platform the image |
| /// was rendered on. |
| String _getKeysJSON() { |
| return json.encode( |
| <String, dynamic>{ |
| 'Platform' : platform.operatingSystem, |
| } |
| ); |
| } |
| |
| /// Returns a boolean value to prevent the client from re-authorizing itself |
| /// for multiple tests. |
| bool _clientIsAuthorized() { |
| final File authFile = _workDirectory.childFile(super.fs.path.join( |
| 'temp', |
| 'auth_opt.json', |
| )); |
| return authFile.existsSync(); |
| } |
| } |