blob: 79277762ac1c70fb81ea05b9a5f5586424228766 [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. 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';
import 'package:path/path.dart' as p;
import 'package:pub/src/ascii_tree.dart' as ascii_tree;
import 'package:pub/src/io.dart';
import 'package:stack_trace/stack_trace.dart' show Trace;
import 'package:test/test.dart';
import 'ascii_tree_test.dart';
import 'descriptor.dart' as d;
import 'test_pub.dart';
final _isCI = () {
final p = RegExp(r'^1|(?:true)$', caseSensitive: false);
final ci = Platform.environment['CI'];
return ci != null && ci.isNotEmpty && p.hasMatch(ci);
}();
/// Find the current `_test.dart` filename invoked from stack-trace.
String _findCurrentTestFilename() => Trace.current()
.frames
.lastWhere(
(frame) =>
frame.uri.isScheme('file') &&
p.basename(frame.uri.toFilePath()).endsWith('_test.dart'),
)
.uri
.toFilePath();
class GoldenTestContext {
static const _endOfSection = ''
'--------------------------------'
' END OF OUTPUT '
'---------------------------------\n\n';
late final String _currentTestFile;
late final String _testName;
late String _goldenFilePath;
late File _goldenFile;
late String _header;
final _results = <String>[];
late bool _goldenFileExists;
bool _generatedNewData = false; // track if new data is generated
int _nextSectionIndex = 0;
GoldenTestContext._(this._currentTestFile, this._testName) {
final rel = p.relative(
_currentTestFile.replaceAll(RegExp(r'\.dart$'), ''),
from: p.join(p.current, 'test'),
);
_goldenFilePath = p.join(
'test',
'testdata',
'goldens',
rel,
// Sanitize the name, and add .txt
_testName.replaceAll(RegExp(r'[<>:"/\|?*%#]'), '~') + '.txt',
);
_goldenFile = File(_goldenFilePath);
_header = '# GENERATED BY: ${p.relative(_currentTestFile)}\n\n';
}
void _readGoldenFile() {
_goldenFileExists = _goldenFile.existsSync();
// Read the golden file for this test
if (_goldenFileExists) {
var text = _goldenFile.readAsStringSync().replaceAll('\r\n', '\n');
// Strip header line
if (text.startsWith('#') && text.contains('\n\n')) {
text = text.substring(text.indexOf('\n\n') + 2);
}
_results.addAll(text.split(_endOfSection));
}
}
/// Expect section [sectionIndex] to match [actual].
void _expectSection(int sectionIndex, String actual) {
if (_goldenFileExists &&
_results.length > sectionIndex &&
_results[sectionIndex].isNotEmpty) {
expect(
actual,
equals(_results[sectionIndex]),
reason: 'Expect matching section $sectionIndex from "$_goldenFilePath"',
);
} else {
while (_results.length <= sectionIndex) {
_results.add('');
}
_results[sectionIndex] = actual;
_generatedNewData = true;
}
}
void _writeGoldenFile() {
// If we generated new data, then we need to write a new file, and fail the
// test case, or mark it as skipped.
if (_generatedNewData) {
// This enables writing the updated file when run in otherwise hermetic
// settings.
//
// This is to make updating the golden files easier in a bazel environment
// See https://docs.bazel.build/versions/2.0.0/user-manual.html#run .
var goldenFile = _goldenFile;
final workspaceDirectory =
Platform.environment['BUILD_WORKSPACE_DIRECTORY'];
if (workspaceDirectory != null) {
goldenFile = File(p.join(workspaceDirectory, _goldenFilePath));
}
goldenFile
..createSync(recursive: true)
..writeAsStringSync(_header + _results.join(_endOfSection));
// If running in CI we should fail if the golden file doesn't already
// exist, or is missing entries.
// This typically happens if we forgot to commit a file to git.
if (_isCI) {
fail('Missing golden file: "$_goldenFilePath", '
'try running tests again and commit the file');
} else {
// If not running in CI, then we consider the test as skipped, we've
// generated the file, but the user should run the tests again.
// Or push to CI in which case we'll run the tests again anyways.
markTestSkipped(
'Generated golden file: "$_goldenFilePath" instead of running test',
);
}
}
}
/// Expect the next section in the golden file to match [actual].
///
/// This will create the section if it is missing.
///
/// **Warning**: Take care when using this in an async context, sections are
/// numbered based on the other in which calls are made. Hence, ensure
/// consistent ordering of calls.
void expectNextSection(String actual) =>
_expectSection(_nextSectionIndex++, actual);
/// Run `pub` [args] with [environment] variables in [workingDirectory], and
/// log stdout/stderr and exitcode to golden file.
Future<void> run(
List<String> args, {
Map<String, String>? environment,
String? workingDirectory,
}) async {
// Create new section index number (before doing anything async)
final sectionIndex = _nextSectionIndex++;
final s = StringBuffer();
s.writeln('## Section $sectionIndex');
await runPubIntoBuffer(
args,
s,
environment: environment,
workingDirectory: workingDirectory,
);
_expectSection(sectionIndex, s.toString());
}
/// Log directory tree structure under [directory] to golden file.
Future<void> tree([String? directory]) async {
// Create new section index number (before doing anything async)
final sectionIndex = _nextSectionIndex++;
final target = p.join(d.sandbox, directory ?? '.');
final s = StringBuffer();
s.writeln('## Section $sectionIndex');
if (directory != null) {
s.writeln('\$ cd $directory');
}
s.writeln('\$ tree');
s.writeln(stripColors(ascii_tree.fromFiles(
listDir(target, recursive: true),
baseDir: target,
)));
_expectSection(sectionIndex, s.toString());
}
}
/// Create a [test] with [GoldenTestContext] which allows running golden tests.
///
/// This will create a golden file containing output of calls to:
/// * [GoldenTestContext.run]
/// * [GoldenTestContext.tree]
///
/// The golden file with the recorded output will be created at:
/// `test/testdata/goldens/path/to/myfile_test/<name>.txt`
/// , when `path/to/myfile_test.dart` is the `_test.dart` file from which this
/// function is called.
void testWithGolden(
String name,
FutureOr<void> Function(GoldenTestContext ctx) fn,
) {
final ctx = GoldenTestContext._(_findCurrentTestFilename(), name);
test(name, () async {
ctx._readGoldenFile();
await fn(ctx);
ctx._writeGoldenFile();
});
}