// Copyright (c) 2024, 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:convert'; import 'dart:io'; import 'package:yaml/yaml.dart'; import 'config.dart'; import 'encoded_asset.dart'; import 'extension.dart'; import 'user_defines.dart'; import 'validation.dart'; /// Tests the main function of a `hook/build.dart`. /// /// This method will throw an exception on validation errors. /// /// This is intended to be used from tests, e.g.: /// /// /// ```dart /// import 'package:hooks/src/test.dart'; /// import 'package:test/test.dart'; /// /// void main() { /// test('test my build hook', () async { /// await testBuildHook( /// mainMethod: (args) {}, /// check: (input, output) {}, /// extensions: [], /// ); /// }); /// } /// ``` /// /// The hook is run in isolation. No user-defines are read from the pubspec, /// they must be provided via [userDefines]. No other hooks are run, if the hook /// requires assets from other build hooks, the must be provided in [assets]. Future testBuildHook({ required FutureOr Function(List arguments) mainMethod, required FutureOr Function(BuildInput input, BuildOutput output) check, bool? linkingEnabled, required List extensions, // TODO(https://github.com/dart-lang/native/issues/2241): Cleanup how the // following parameters are passed in. PackageUserDefines? userDefines, Map>? assets, }) async { linkingEnabled ??= false; const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; final tempDir = await Directory.systemTemp.createTemp(); try { // Deal with Windows temp folder aliases. final tempUri = Directory( await tempDir.resolveSymbolicLinks(), ).uri.normalizePath(); final outputDirectoryShared = tempUri.resolve('output_shared/'); final outputFile = tempUri.resolve('output.json'); await Directory.fromUri(outputDirectoryShared).create(); final inputBuilder = BuildInputBuilder(); inputBuilder ..setupShared( packageRoot: Directory.current.uri, packageName: _readPackageNameFromPubspec(), outputFile: outputFile, outputDirectoryShared: outputDirectoryShared, userDefines: userDefines, ) ..setupBuildInput(assets: assets) ..config.setupBuild(linkingEnabled: linkingEnabled); for (final extension in extensions) { extension.setupBuildInput(inputBuilder); } final input = inputBuilder.build(); final inputErrors = [ for (final extension in extensions) ...await extension.validateBuildInput(input), ]; if (inputErrors.isNotEmpty) { throw ValidationFailure( 'Encountered build input validation issues: $inputErrors', ); } final inputUri = tempUri.resolve('input.json'); _writeJsonTo(inputUri, input.json); await mainMethod(['--config=${inputUri.toFilePath()}']); final output = BuildOutput(_readJsonFrom(input.outputFile)); final outputErrors = [ ...await ProtocolBase.validateBuildOutput(input, output), for (final extension in extensions) ...[ ...await extension.validateBuildOutput(input, output), ...await extension.validateApplicationAssets( output.assets.encodedAssets, ), ], ]; if (outputErrors.isNotEmpty) { throw ValidationFailure( 'Encountered build output validation issues: $outputErrors', ); } await check(input, output); } finally { final keepTempDir = (Platform.environment[keepTempKey] ?? '').isNotEmpty; if (!keepTempDir) { tempDir.deleteSync(recursive: true); } else { print('$keepTempKey ${tempDir.uri}'); } } } void _writeJsonTo(Uri uri, Map json) { final encoder = const JsonEncoder().fuse(const Utf8Encoder()); File.fromUri(uri).writeAsBytesSync(encoder.convert(json)); } Map _readJsonFrom(Uri uri) { final decoder = const Utf8Decoder().fuse(const JsonDecoder()); final bytes = File.fromUri(uri).readAsBytesSync(); return decoder.convert(bytes) as Map; } String _readPackageNameFromPubspec() { final uri = Directory.current.uri.resolve('pubspec.yaml'); final readAsString = File.fromUri(uri).readAsStringSync(); final yaml = loadYaml(readAsString) as YamlMap; return yaml['name'] as String; }