Files
sdk/pkg/dartdev/test/native_assets/helpers.dart
T
Konstantin Shcheglov e4ef831dfc Prepare to publish analyzer 13.2.0, _fe_analyzer_shared 102.0.0, analysis_server_plugin 0.3.17, analyzer_plugin 0.14.11, analyzer_testing 0.3.1
Change-Id: I534e30c37570cf5fdc194eb9668d37747823aa82
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/510480
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
2026-06-10 10:52:25 -07:00

376 lines
11 KiB
Dart

// 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',
'record_use',
])
package: {
'path': sdkRoot
.resolve('third_party/pkg/native/pkgs/$package/')
.toFilePath(),
},
for (final package in [
'_fe_analyzer_shared',
'analyzer', // package:analyzer depends on package:meta.
'meta',
])
package: {'path': sdkRoot.resolve('pkg/$package/').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 usePubWorkspace = false,
}) async => await runPackageTest(
packageUnderTest,
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',
'recursive_invocation',
'system_library',
'treeshaking_native_libs',
'user_defines',
],
sdkRootUri.resolve('third_party/pkg/native/pkgs/hooks_runner/'),
sdkRootUri,
usePubWorkspace,
);
Future<void> recordUseNoHooksTest(
String packageUnderTest,
Future<void> Function(Uri) fun,
) async => await runPackageTest(
packageUnderTest,
(packageUri) async {
final pubspecFile = File.fromUri(packageUri.resolve('pubspec.yaml'));
final pubspecString = await pubspecFile.readAsString();
final pubspec = YamlEditor(pubspecString);
final pubspecYaml = loadYaml(pubspecString) as YamlMap;
if (pubspecYaml.containsKey('hook')) {
pubspec.remove(['hook']);
}
if (pubspecYaml.containsKey('hooks')) {
pubspec.remove(['hooks']);
}
await pubspecFile.writeAsString(pubspec.toString());
final hookDir = Directory.fromUri(packageUri.resolve('hook/'));
if (await hookDir.exists()) {
await hookDir.delete(recursive: true);
}
return await fun(packageUri);
},
const ['drop_dylib_recording', 'drop_data_asset'],
sdkRootUri.resolve('third_party/pkg/native/pkgs/record_use/'),
sdkRootUri,
false,
);
Future<void> recordUseTest(
String packageUnderTest,
Future<void> Function(Uri) fun,
) async => await runPackageTest(
packageUnderTest,
fun,
const ['drop_dylib_recording', 'drop_data_asset'],
sdkRootUri.resolve('third_party/pkg/native/pkgs/record_use/'),
sdkRootUri,
false,
);
Future<void> runPackageTest(
String packageUnderTest,
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/');
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);