blob: d60076df737461e40fe17c81c847aac3ec03d219 [file] [log] [blame] [edit]
// Copyright (c) 2023, 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:code_assets/code_assets.dart';
import 'package:data_assets/data_assets.dart';
import 'package:file/local.dart' show LocalFileSystem;
import 'package:hooks/hooks.dart';
import 'package:hooks_runner/src/utils/run_process.dart' as run_process;
import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
extension UriExtension on Uri {
String get name => pathSegments.where((e) => e != '').last;
Uri get parent => File(toFilePath()).parent.uri;
FileSystemEntity get fileSystemEntity {
if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) {
return Directory.fromUri(this);
}
return File.fromUri(this);
}
}
const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES';
Future<void> inTempDir(
Future<void> Function(Uri tempUri) fun, {
String? prefix,
bool keepTemp = false,
}) async {
final tempDir = await Directory.systemTemp.createTemp(prefix);
// Deal with Windows temp folder aliases.
final tempUri = Directory(
await tempDir.resolveSymbolicLinks(),
).uri.normalizePath();
try {
await fun(tempUri);
} finally {
if ((!Platform.environment.containsKey(keepTempKey) ||
Platform.environment[keepTempKey]!.isEmpty) &&
!keepTemp) {
try {
await tempDir.delete(recursive: true);
} on FileSystemException {
// On Windows, the temp dir might still be locked even though all
// process invocations have finished.
if (!Platform.isWindows) rethrow;
}
} else {
print('$keepTempKey $tempUri');
}
}
}
Future<Uri> tempDirForTest({String? prefix, bool keepTemp = false}) async {
final tempDir = await Directory.systemTemp.createTemp(prefix);
// Deal with Windows temp folder aliases.
final tempUri = Directory(
await tempDir.resolveSymbolicLinks(),
).uri.normalizePath();
if ((!Platform.environment.containsKey(keepTempKey) ||
Platform.environment[keepTempKey]!.isEmpty) &&
!keepTemp) {
addTearDown(() async {
try {
await tempDir.delete(recursive: true);
} on FileSystemException {
// On Windows, the temp dir might still be locked even though all
// process invocations have finished.
if (!Platform.isWindows) rethrow;
}
});
} else {
print('$keepTempKey $tempUri');
}
return tempUri;
}
/// Runs a [Process].
///
/// If [logger] is provided, stream stdout and stderr to it.
///
/// If [captureOutput], captures stdout and stderr.
Future<run_process.RunProcessResult> runProcess({
required Uri executable,
List<String> arguments = const [],
Uri? workingDirectory,
Map<String, String>? environment,
required Logger? logger,
bool captureOutput = true,
int expectedExitCode = 0,
bool throwOnUnexpectedExitCode = false,
}) => run_process.runProcess(
filesystem: const LocalFileSystem(),
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
environment: environment,
logger: logger,
captureOutput: captureOutput,
expectedExitCode: expectedExitCode,
throwOnUnexpectedExitCode: throwOnUnexpectedExitCode,
);
/// Test files are run in a variety of ways, find this package root in all.
///
/// Test files can be run from source from any working directory. The Dart SDK
/// `tools/test.py` runs them from the root of the SDK for example.
///
/// Test files can be run from dill from the root of package. `package:test`
/// does this.
///
/// https://github.com/dart-lang/test/issues/110
Uri findPackageRoot(String packageName) {
final script = Platform.script;
final fileName = script.name;
if (fileName.endsWith('.dart')) {
// We're likely running from source in the package somewhere.
var directory = script.resolve('.');
while (true) {
final dirName = directory.name;
if (dirName == packageName) {
return directory;
}
final parent = directory.resolve('..');
if (parent == directory) break;
directory = parent;
}
} else if (fileName.endsWith('.dill')) {
// Probably from the package root.
final cwd = Directory.current.uri;
final dirName = cwd.name;
if (dirName == packageName) {
return cwd;
}
// Or the workspace root.
final candidate = cwd.resolve('pkgs/$packageName/');
if (Directory.fromUri(candidate).existsSync()) {
return candidate;
}
}
// Or the workspace root.
final cwd = Directory.current.uri;
final candidate = cwd.resolve('pkgs/$packageName/');
if (Directory.fromUri(candidate).existsSync()) {
return candidate;
}
throw StateError(
"Could not find package root for package '$packageName'. "
'Tried finding the package root via Platform.script '
"'${Platform.script.toFilePath()}' and Directory.current "
"'${Directory.current.uri.toFilePath()}'.",
);
}
final pkgNativeAssetsBuilderUri = findPackageRoot('hooks_runner');
final testDataUri = pkgNativeAssetsBuilderUri.resolve('test_data/');
/// Archiver provided by the environment.
///
/// Provided on Dart CI.
final Uri? _ar = Platform.environment['DART_HOOK_TESTING_C_COMPILER__AR']
?.asFileUri();
/// Compiler provided by the environment.
///
/// Provided on Dart CI.
final Uri? _cc = Platform.environment['DART_HOOK_TESTING_C_COMPILER__CC']
?.asFileUri();
/// Linker provided by the environment.
///
/// Provided on Dart CI.
final Uri? _ld = Platform.environment['DART_HOOK_TESTING_C_COMPILER__LD']
?.asFileUri();
/// Path to script that sets environment variables for [_cc], [_ld], and [_ar].
///
/// Provided on Dart CI.
final Uri? _envScript = Platform
.environment['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT']
?.asFileUri();
/// Arguments for [_envScript] provided by environment.
///
/// Provided on Dart CI.
final List<String>? _envScriptArgs = Platform
.environment['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT_ARGUMENTS']
?.split(' ');
/// Configuration for the native toolchain.
///
/// Provided on Dart CI.
final cCompiler = (_cc == null || _ar == null || _ld == null)
? null
: CCompilerConfig(
compiler: _cc!,
archiver: _ar!,
linker: _ld!,
windows: _envScript == null
? null
: WindowsCCompilerConfig(
developerCommandPrompt: DeveloperCommandPrompt(
script: _envScript!,
arguments: _envScriptArgs ?? [],
),
),
);
extension on String {
Uri asFileUri() => Uri.file(this);
}
extension AssetIterable on Iterable<EncodedAsset> {
Future<bool> allExist() async {
for (final encodedAsset in this) {
if (encodedAsset.isDataAsset) {
final dataAsset = encodedAsset.asDataAsset;
if (!await dataAsset.file.fileSystemEntity.exists()) {
return false;
}
} else if (encodedAsset.isCodeAsset) {
final codeAsset = encodedAsset.asCodeAsset;
if (!await (codeAsset.file?.fileSystemEntity.exists() ?? true)) {
return false;
}
} else {
throw UnimplementedError('Unknown asset type ${encodedAsset.type}');
}
}
return true;
}
}
Future<void> copyTestProjects({
Uri? sourceUri,
required Uri targetUri,
bool addDependencyOverrides = true,
}) async {
sourceUri ??= testDataUri;
final manifestUri = sourceUri.resolve('manifest.yaml');
final manifestFile = File.fromUri(manifestUri);
final manifestString = await manifestFile.readAsString();
final manifestYaml = loadYamlDocument(manifestString);
final manifest = [
for (final path in manifestYaml.contents as List<Object?>)
Uri(path: path as String),
];
final filesToCopy = manifest
.where(
(e) =>
!(e.pathSegments.last.startsWith('pubspec') &&
e.pathSegments.last.endsWith('.yaml')),
)
.toList();
final filesToModify = manifest
.where(
(e) =>
e.pathSegments.last.startsWith('pubspec') &&
e.pathSegments.last.endsWith('.yaml'),
)
.toList();
for (final pathToCopy in filesToCopy) {
final sourceFile = File.fromUri(sourceUri.resolveUri(pathToCopy));
final targetFileUri = targetUri.resolveUri(pathToCopy);
final targetDirUri = targetFileUri.parent;
final targetDir = Directory.fromUri(targetDirUri);
if (!(await targetDir.exists())) {
await targetDir.create(recursive: true);
}
// Copying files on MacOS and Windows preserves the source timestamps.
// The builder will use the cached build if the timestamps are equal.
// So just write the file instead.
final targetFile = File.fromUri(targetFileUri);
await targetFile.writeAsBytes(await sourceFile.readAsBytes());
}
for (final pathToModify in filesToModify) {
final sourceFile = File.fromUri(sourceUri.resolveUri(pathToModify));
final targetFileUri = targetUri.resolveUri(pathToModify);
var sourceString = await sourceFile.readAsString();
if (addDependencyOverrides &&
!pathToModify.path.contains('native_add_version_skew')) {
sourceString += '''
dependency_overrides:
''';
const packagesToOverride = [
'code_assets',
'data_assets',
'hooks',
'native_toolchain_c',
];
for (final package in packagesToOverride) {
sourceString +=
'''
$package:
path: ${pkgNativeAssetsBuilderUri.resolve('../$package/').toFilePath()}
''';
}
}
final modifiedString = sourceString.replaceAll('resolution: workspace', '');
await File.fromUri(
targetFileUri,
).writeAsString(modifiedString, flush: true);
}
}
extension UnescapePath on String {
/// Remove double encoding of slashes on windows, for string comparison with
/// Unix-style encoded strings.
String unescape() => replaceAll('\\', '/');
}
/// Logger that outputs the full trace when a test fails.
Logger get logger => _logger ??= () {
// A new logger is lazily created for each test so that the messages
// captured by printOnFailure are scoped to the correct test.
addTearDown(() => _logger = null);
return _createTestLogger();
}();
Logger? _logger;
Logger createCapturingLogger(
List<String> capturedMessages, {
Level level = Level.ALL,
}) => _createTestLogger(capturedMessages: capturedMessages, level: level);
Logger _createTestLogger({
List<String>? capturedMessages,
Level level = Level.ALL,
}) => Logger.detached('')
..level = level
..onRecord.listen((record) {
printOnFailure('${record.level.name}: ${record.time}: ${record.message}');
capturedMessages?.add(record.message);
});
final dartExecutable = File(Platform.resolvedExecutable).uri;