blob: dc25c7ced8c941572acf6b132a1f69605ea346a5 [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:analyzer/src/dart/analysis/experiments.dart';
import 'package:dartdev/src/sdk.dart';
import 'package:file/local.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';
import 'package:yaml_edit/yaml_edit.dart';
import '../utils.dart';
export 'package:hooks_runner/src/utils/run_process.dart' show RunProcessResult;
extension UriExtension on Uri {
Uri get parent {
return File(toFilePath()).parent.uri;
}
}
const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES';
Future<void> inTempDir(Future<void> Function(Uri tempUri) fun) async {
final tempDir = await Directory.systemTemp.createTemp();
// 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) {
try {
await tempDir.delete(recursive: true);
} on PathAccessException {
if (Platform.isWindows) {
// Don't fail on files being in use.
} else {
rethrow;
}
}
}
}
}
/// 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,
bool includeParentEnvironment = true,
required Logger? logger,
bool captureOutput = true,
int expectedExitCode = 0,
bool throwOnUnexpectedExitCode = false,
}) =>
run_process.runProcess(
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
logger: logger,
captureOutput: captureOutput,
expectedExitCode: expectedExitCode,
throwOnUnexpectedExitCode: throwOnUnexpectedExitCode,
filesystem: const LocalFileSystem(),
);
Future<void> copyTestProjects(Uri copyTargetUri, Logger logger,
Uri packageLocation, Uri sdkRoot, bool usePubWorkspace) async {
// Reuse the test projects from `pkg:native`.
final testProjectsUri = packageLocation.resolve('test_data/');
final manifestUri = testProjectsUri.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 YamlList) Uri(path: path)
];
final filesToCopy = manifest
.where((e) => !(e.pathSegments.last.startsWith('pubspec') &&
e.pathSegments.last.endsWith('.yaml')))
.toList();
final pubspecPaths = manifest
.where((e) =>
e.pathSegments.last.startsWith('pubspec') &&
e.pathSegments.last.endsWith('.yaml'))
.toList();
for (final pathToCopy in filesToCopy) {
final sourceFile = File.fromUri(testProjectsUri.resolveUri(pathToCopy));
final targetUri = copyTargetUri.resolveUri(pathToCopy);
final targetDirUri = targetUri.parent;
final targetDir = Directory.fromUri(targetDirUri);
if (!(await targetDir.exists())) {
await targetDir.create(recursive: true);
}
await sourceFile.copy(targetUri.toFilePath());
}
final dependencyOverrides = {
for (final package in [
'code_assets',
'data_assets',
'hooks',
'native_toolchain_c'
])
package: {
'path': sdkRoot
.resolve('third_party/pkg/native/pkgs/$package/')
.toFilePath(),
},
'meta': {
'path': sdkRoot.resolve('pkg/meta/').toFilePath(),
},
'record_use': {
'path': sdkRoot.resolve('pkg/record_use/').toFilePath(),
},
};
final userDefinesWorkspace = {};
for (final pubspecPath in pubspecPaths) {
final sourceFile = File.fromUri(testProjectsUri.resolveUri(pubspecPath));
final targetUri = copyTargetUri.resolveUri(pubspecPath);
final sourceString = await sourceFile.readAsString();
final pubspec = YamlEditor(sourceString);
final pubspecRead = loadYamlNode(sourceString) as Map;
if (!usePubWorkspace) {
if (pubspecRead['resolution'] != null) {
pubspec.remove(['resolution']);
}
pubspec.update(['dependency_overrides'], dependencyOverrides);
} else {
final userDefines = pubspecRead['hooks']?['user_defines'];
if (userDefines is Map) {
// Remove the user defines from the root package pubspec.
pubspec.remove(['hooks', 'user_defines']);
// Add the user defines to the workspace pubspec.
// But make sure to rewrite relative paths to point to the right place.
// Deep-copy the map because the read map is unmodifiable.
for (final MapEntry(:key, :value) in userDefines.entries) {
final packageName = key;
final defines = value as Map;
userDefinesWorkspace[packageName] = <String, Object?>{};
for (final MapEntry(:key, :value) in defines.entries) {
if (value == 'assets/data.json') {
// We're constructing a workspace, so the paths in the workspace pubspec must point to the right place.
userDefinesWorkspace[packageName]![key] =
'$packageName/assets/data.json';
} else {
userDefinesWorkspace[packageName]![key] = value;
}
}
}
}
}
final modifiedString = pubspec.toString();
await File.fromUri(targetUri).writeAsString(modifiedString);
}
if (usePubWorkspace) {
final workspacePubspec = YamlEditor('');
workspacePubspec.update([], {
'name': 'my_pub_workspace',
'environment': {'sdk': '>=3.7.0 <4.0.0'},
'workspace': [
for (final pubspec in pubspecPaths)
if (!pubspec.toFilePath().contains('version_skew'))
pubspec.toFilePath().replaceAll('pubspec.yaml', ''),
],
'dependency_overrides': dependencyOverrides,
'hooks': {
'user_defines': userDefinesWorkspace,
}
});
final pubspecUri = copyTargetUri.resolve('pubspec.yaml');
await File.fromUri(pubspecUri).writeAsString(workspacePubspec.toString());
}
// If we're copying `my_native_library/` we need to simulate that its
// native assets are pre-built
final myNativeLibraryUri = copyTargetUri.resolve('my_native_library/');
if (await Directory(myNativeLibraryUri.toFilePath()).exists()) {
await runPubGet(
workingDirectory: myNativeLibraryUri,
logger: logger,
);
await runDart(
arguments: ['tool/native.dart', 'build'],
workingDirectory: myNativeLibraryUri,
logger: logger,
);
}
}
Future<void> runPubGet({
required Uri workingDirectory,
required Logger logger,
}) async {
final result = await runDart(
arguments: ['pub', 'get'],
workingDirectory: workingDirectory,
logger: logger,
);
expect(result.exitCode, 0);
}
void expectDartAppStdout(String stdout) {
expect(
stdout,
stringContainsInOrder(
[
'add(5, 6) = 11',
'subtract(5, 6) = -1',
],
),
);
}
/// 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) =>
_createTestLogger(capturedMessages: capturedMessages);
Logger _createTestLogger({List<String>? capturedMessages}) =>
Logger.detached('')
..level = Level.ALL
..onRecord.listen((record) {
printOnFailure(
'${record.level.name}: ${record.time}: ${record.message}');
capturedMessages?.add(record.message);
});
final dartExecutable = Uri.file(Platform.resolvedExecutable);
Future<void> nativeAssetsTest(
String packageUnderTest,
Future<void> Function(Uri) fun, {
bool skipPubGet = false,
bool usePubWorkspace = false,
}) async =>
await runPackageTest(
packageUnderTest,
skipPubGet,
fun,
const [
'add_asset_link',
'dart_app',
'dev_dependency_with_hook',
'drop_dylib_link',
'native_add_duplicate',
'native_add_version_skew',
'native_add',
'native_dynamic_linking',
'system_library',
'treeshaking_native_libs',
'user_defines',
],
sdkRootUri.resolve('third_party/pkg/native/pkgs/hooks_runner/'),
sdkRootUri,
usePubWorkspace,
);
Future<void> recordUseTest(
String packageUnderTest,
Future<void> Function(Uri) fun, {
bool skipPubGet = false,
}) async =>
await runPackageTest(
packageUnderTest,
skipPubGet,
fun,
const ['drop_dylib_recording'],
sdkRootUri.resolve('pkg/record_use/'),
sdkRootUri,
false,
);
Future<void> runPackageTest(
String packageUnderTest,
bool skipPubGet,
Future<void> Function(Uri) fun,
List<String> validPackages,
Uri packageLocation,
Uri sdkRoot,
bool usePubWorkspace,
) async {
assert(validPackages.contains(packageUnderTest));
return await inTempDir((tempUri) async {
await copyTestProjects(
tempUri, logger, packageLocation, sdkRoot, usePubWorkspace);
final packageUri = tempUri.resolve('$packageUnderTest/');
if (!skipPubGet) {
await runPubGet(workingDirectory: packageUri, logger: logger);
}
return await fun(packageUri);
});
}
Future<run_process.RunProcessResult> runDart({
required List<String> arguments,
Uri? workingDirectory,
required Logger? logger,
bool expectExitCodeZero = true,
Map<String, String>? environment,
}) async {
final result = await runProcess(
executable: dartExecutable,
arguments: arguments,
workingDirectory: workingDirectory,
logger: logger,
environment: environment,
);
if (expectExitCodeZero) {
if (result.exitCode != 0) {
print(result.stdout);
print(result.stderr);
print(result.exitCode);
}
expect(result.exitCode, 0);
}
return result;
}
final nativeAssetsExperimentAvailableOnCurrentChannel = ExperimentalFeatures
.native_assets.channels
.contains(Runtime.runtime.channel);