flutter_web_goldens.dart 7.53 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2014 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:async';
import 'dart:typed_data';

8 9
import 'package:process/process.dart';

10 11
import '../base/file_system.dart';
import '../base/io.dart';
12
import '../base/logger.dart';
13
import '../convert.dart';
14
import '../web/compile.dart';
15 16 17 18 19
import 'test_compiler.dart';
import 'test_config.dart';

/// Helper class to start golden file comparison in a separate process.
///
20
/// The golden file comparator is configured using flutter_test_config.dart and that
21 22 23 24 25 26
/// file can contain arbitrary Dart code that depends on dart:ui. Thus it has to
/// be executed in a `flutter_tester` environment. This helper class generates a
/// Dart file configured with flutter_test_config.dart to perform the comparison
/// of golden files.
class TestGoldenComparator {
  /// Creates a [TestGoldenComparator] instance.
27
  TestGoldenComparator(this.shellPath, this.compilerFactory, {
28 29 30 31
    required Logger logger,
    required FileSystem fileSystem,
    required ProcessManager? processManager,
    required this.webRenderer,
32 33 34 35
  }) : tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform.'),
       _logger = logger,
       _fileSystem = fileSystem,
       _processManager = processManager;
36

37
  final String? shellPath;
38 39
  final Directory tempDir;
  final TestCompiler Function() compilerFactory;
40 41
  final Logger _logger;
  final FileSystem _fileSystem;
42
  final ProcessManager? _processManager;
43
  final WebRendererMode webRenderer;
44

45 46 47
  TestCompiler? _compiler;
  TestGoldenComparatorProcess? _previousComparator;
  Uri? _previousTestUri;
48 49 50 51 52 53 54 55 56

  Future<void> close() async {
    tempDir.deleteSync(recursive: true);
    await _compiler?.dispose();
    await _previousComparator?.close();
  }

  /// Start golden comparator in a separate process. Start one file per test file
  /// to reduce the overhead of starting `flutter_tester`.
57
  Future<TestGoldenComparatorProcess?> _processForTestFile(Uri testUri) async {
58 59 60 61
    if (testUri == _previousTestUri) {
      return _previousComparator;
    }

62
    final String bootstrap = TestGoldenComparatorProcess.generateBootstrap(_fileSystem.file(testUri), testUri, logger: _logger);
63 64 65 66
    final Process? process = await _startProcess(bootstrap);
    if (process == null) {
      return null;
    }
67
    unawaited(_previousComparator?.close());
68
    _previousComparator = TestGoldenComparatorProcess(process, logger: _logger);
69 70 71 72 73
    _previousTestUri = testUri;

    return _previousComparator;
  }

74
  Future<Process?> _startProcess(String testBootstrap) async {
75 76 77 78 79 80
    // Prepare the Dart file that will talk to us and start the test.
    final File listenerFile = (await tempDir.createTemp('listener')).childFile('listener.dart');
    await listenerFile.writeAsString(testBootstrap);

    // Lazily create the compiler
    _compiler = _compiler ?? compilerFactory();
81 82 83 84
    final String? output = await _compiler!.compile(listenerFile.uri);
    if (output == null) {
      return null;
    }
85
    final List<String> command = <String>[
86
      shellPath!,
87 88
      '--disable-observatory',
      '--non-interactive',
89
      '--packages=${_fileSystem.path.join('.dart_tool', 'package_config.json')}',
90 91 92 93 94 95
      output,
    ];

    final Map<String, String> environment = <String, String>{
      // Chrome is the only supported browser currently.
      'FLUTTER_TEST_BROWSER': 'chrome',
96
      'FLUTTER_WEB_RENDERER': webRenderer == WebRendererMode.html ? 'html' : 'canvaskit',
97
    };
98
    return _processManager!.start(command, environment: environment);
99 100
  }

101
  Future<String?> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool? updateGoldens) async {
102
    final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
103
    final TestGoldenComparatorProcess process = await (_processForTestFile(testUri) as FutureOr<TestGoldenComparatorProcess>);
104 105 106 107 108 109 110
    process.sendCommand(imageFile, goldenKey, updateGoldens);

    final Map<String, dynamic> result = await process.getResponse();

    if (result == null) {
      return 'unknown error';
    } else {
111
      return (result['success'] as bool) ? null : ((result['message'] as String?) ?? 'does not match');
112 113 114 115 116 117 118 119
    }
  }
}

/// Represents a `flutter_tester` process started for golden comparison. Also
/// handles communication with the child process.
class TestGoldenComparatorProcess {
  /// Creates a [TestGoldenComparatorProcess] backed by [process].
120
  TestGoldenComparatorProcess(this.process, {required Logger logger}) : _logger = logger {
121 122 123 124 125 126 127
    // Pipe stdout and stderr to printTrace and printError.
    // Also parse stdout as a stream of JSON objects.
    streamIterator = StreamIterator<Map<String, dynamic>>(
      process.stdout
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter())
        .where((String line) {
128
          logger.printTrace('<<< $line');
129 130 131 132 133 134 135 136 137
          return line.isNotEmpty && line[0] == '{';
        })
        .map<dynamic>(jsonDecode)
        .cast<Map<String, dynamic>>());

    process.stderr
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter())
        .forEach((String line) {
138
          logger.printError('<<< $line');
139 140 141
        });
  }

142
  final Logger _logger;
143
  final Process process;
144
  late StreamIterator<Map<String, dynamic>> streamIterator;
145 146 147

  Future<void> close() async {
    process.kill();
148
    await process.exitCode;
149 150
  }

151
  void sendCommand(File imageFile, Uri? goldenKey, bool? updateGoldens) {
152 153 154 155 156
    final Object command = jsonEncode(<String, dynamic>{
      'imageFile': imageFile.path,
      'key': goldenKey.toString(),
      'update': updateGoldens,
    });
157
    _logger.printTrace('Preparing to send command: $command');
158 159 160 161 162 163 164 165 166
    process.stdin.writeln(command);
  }

  Future<Map<String, dynamic>> getResponse() async {
    final bool available = await streamIterator.moveNext();
    assert(available);
    return streamIterator.current;
  }

167 168
  static String generateBootstrap(File testFile, Uri testUri, {required Logger logger}) {
    final File? testConfigFile = findTestConfigFile(testFile, logger);
169 170
    // Generate comparator process for the file.
    return '''
171 172
import 'dart:convert'; // flutter_ignore: dart_convert_import
import 'dart:io'; // flutter_ignore: dart_io_import
173 174 175 176 177 178 179 180 181 182 183 184 185

import 'package:flutter_test/flutter_test.dart';

${testConfigFile != null ? "import '${Uri.file(testConfigFile.path)}' as test_config;" : ""}

void main() async {
  LocalFileComparator comparator = LocalFileComparator(Uri.parse('$testUri'));
  goldenFileComparator = comparator;

  ${testConfigFile != null ? 'test_config.testExecutable(() async {' : ''}
  final commands = stdin
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
186 187
    .map<dynamic>(jsonDecode);
  await for (final dynamic command in commands) {
188
    if (command is Map<String, dynamic>) {
189 190 191
      File imageFile = File(command['imageFile'] as String);
      Uri goldenKey = Uri.parse(command['key'] as String);
      bool update = command['update'] as bool;
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

      final bytes = await File(imageFile.path).readAsBytes();
      if (update) {
        await goldenFileComparator.update(goldenKey, bytes);
        print(jsonEncode({'success': true}));
      } else {
        try {
          bool success = await goldenFileComparator.compare(bytes, goldenKey);
          print(jsonEncode({'success': success}));
        } on Exception catch (ex) {
          print(jsonEncode({'success': false, 'message': '\$ex'}));
        }
      }
    } else {
      print('object type is not right');
    }
  }
  ${testConfigFile != null ? '});' : ''}
}
    ''';
  }
}