diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 534a2d8..beda071 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,8 +47,8 @@ jobs: run: flutter analyze --fatal-infos --fatal-warnings - name: Dart format run: dart format --set-exit-if-changed . - # - name: Dart tests - # run: melos run test + - name: Dart tests + run: melos run test # macos_integration_test: # runs-on: macos-latest diff --git a/lib/native_toolchain_rs.dart b/lib/native_toolchain_rs.dart index f7845da..046af10 100644 --- a/lib/native_toolchain_rs.dart +++ b/lib/native_toolchain_rs.dart @@ -2,7 +2,9 @@ import 'package:code_assets/code_assets.dart'; import 'package:hooks/hooks.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; +import 'package:native_toolchain_rs/src/build_environment.dart'; import 'package:native_toolchain_rs/src/build_runner.dart'; +import 'package:native_toolchain_rs/src/crate_info_validator.dart'; import 'package:native_toolchain_rs/src/crate_resolver.dart'; import 'package:native_toolchain_rs/src/process_runner.dart'; import 'package:native_toolchain_rs/src/toml_parsing.dart'; @@ -97,15 +99,19 @@ final class RustBuilder { logger, tomlDocumentWrapperFactory, ); + const buildEnvironmentFactory = BuildEnvironmentFactory(); + final crateInfoValidator = CrateInfoValidator( + toolchainTomlParser: toolchainTomlParser, + cargoManifestParser: cargoManifestParser, + ); return RustBuildRunner( config: this, logger: logger, processRunner: processRunner, crateDirectoryResolver: crateDirectoryResolver, - tomlDocumentWrapperFactory: tomlDocumentWrapperFactory, - cargoManifestParser: cargoManifestParser, - toolchainTomlParser: toolchainTomlParser, + buildEnvironmentFactory: buildEnvironmentFactory, + crateInfoValidator: crateInfoValidator, ).run(input: input, output: output, assetRouting: assetRouting); } } diff --git a/lib/src/build_environment.dart b/lib/src/build_environment.dart new file mode 100644 index 0000000..c2f6902 --- /dev/null +++ b/lib/src/build_environment.dart @@ -0,0 +1,65 @@ +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:meta/meta.dart'; +import 'package:native_toolchain_rs/src/config_mapping.dart'; +import 'package:native_toolchain_rs/src/exception.dart'; +import 'package:path/path.dart' as path; + +@internal +interface class BuildEnvironmentFactory { + const BuildEnvironmentFactory(); + + Map createBuildEnvVars(CodeConfig codeConfig) { + final CodeConfig(:targetOS, :cCompiler) = codeConfig; + final targetTriple = codeConfig.targetTriple; + final targetTripleEnvVar = targetTriple.replaceAll('-', '_'); + + String getBinary(String binaryName) { + if (cCompiler == null) { + throw UnsupportedError( + 'CCompilerConfig was not provided but is required for $targetTriple', + ); + } + + final binaryPath = path.join( + path.dirname(path.fromUri(cCompiler.compiler)), + OS.current.executableFileName(binaryName), + ); + + if (!File(binaryPath).existsSync()) { + throw RustValidationException([ + 'Binary $binaryPath not found; is your installed compiler too old?', + ]); + } + + return binaryPath; + } + + return { + // NOTE: XCode makes some injections into PATH that break host build + // for crates with a build.rs + // See also: https://github.com/irondash/native_toolchain_rust/issues/17 + if (Platform.isMacOS) ...{ + 'PATH': Platform.environment['PATH']! + .split(':') + .where((e) => !e.contains('Contents/Developer/')) + .join(':'), + }, + + // NOTE: we need to point to NDK >=27 vended LLVM for Android. + // The `${targetTriple}35-clang`s were introduced in NDK 27, + // so using these binaries: + // 1. Ensures we are using a compatible NDK + // 2. Also fixes build issues when just using the `clang`s directly + if (targetOS == OS.android) ...{ + 'AR_$targetTripleEnvVar': getBinary('llvm-ar'), + 'CC_$targetTripleEnvVar': getBinary('${targetTriple}35-clang'), + 'CXX_$targetTripleEnvVar': getBinary('${targetTriple}35-clang++'), + 'CARGO_TARGET_${targetTripleEnvVar.toUpperCase()}_LINKER': getBinary( + '${targetTriple}35-clang', + ), + }, + }; + } +} diff --git a/lib/src/build_runner.dart b/lib/src/build_runner.dart index 2fc78b7..9d1bc2e 100644 --- a/lib/src/build_runner.dart +++ b/lib/src/build_runner.dart @@ -3,33 +3,32 @@ import 'dart:io'; import 'package:code_assets/code_assets.dart'; import 'package:hooks/hooks.dart'; import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:native_toolchain_rs/native_toolchain_rs.dart'; +import 'package:native_toolchain_rs/src/build_environment.dart'; +import 'package:native_toolchain_rs/src/config_mapping.dart'; +import 'package:native_toolchain_rs/src/crate_info_validator.dart'; import 'package:native_toolchain_rs/src/crate_resolver.dart'; import 'package:native_toolchain_rs/src/process_runner.dart'; -import 'package:native_toolchain_rs/src/toml_parsing.dart'; import 'package:path/path.dart' as path; -// NOTE: this is an internal implementation detail -// ignore_for_file: public_member_api_docs - -final class RustBuildRunner { +@internal +interface class RustBuildRunner { const RustBuildRunner({ required this.config, required this.logger, required this.crateDirectoryResolver, - required this.tomlDocumentWrapperFactory, - required this.toolchainTomlParser, - required this.cargoManifestParser, required this.processRunner, + required this.buildEnvironmentFactory, + required this.crateInfoValidator, }); final RustBuilder config; final Logger? logger; final CrateDirectoryResolver crateDirectoryResolver; - final TomlDocumentWrapperFactory tomlDocumentWrapperFactory; - final ToolchainTomlParser toolchainTomlParser; - final CargoManifestParser cargoManifestParser; final ProcessRunner processRunner; + final BuildEnvironmentFactory buildEnvironmentFactory; + final CrateInfoValidator crateInfoValidator; Future run({ required BuildInput input, @@ -49,7 +48,8 @@ final class RustBuildRunner { } logger?.info('Gathering all data required for the build'); - final CodeConfig(:targetOS, :targetTriple, :linkMode) = input.config.code; + final codeConfig = input.config.code; + final CodeConfig(:targetOS, :targetTriple, :linkMode) = codeConfig; final RustBuilder( :assetName, :cratePath, @@ -65,7 +65,10 @@ final class RustBuildRunner { ); final outputDir = path.join(path.fromUri(input.outputDirectory), 'target'); final manifestPath = path.join(crateDirectory.path, 'Cargo.toml'); - final (:crateName, :toolchainChannel) = fetchAndValidateCrateInfo( + final ( + :crateName, + :toolchainChannel, + ) = crateInfoValidator.fetchAndValidateCrateInfo( targetTriple: targetTriple, manifestPath: manifestPath, toolchainTomlPath: path.join(crateDirectory.path, 'rust-toolchain.toml'), @@ -98,7 +101,7 @@ final class RustBuildRunner { ...extraCargoBuildArgs, ], environment: { - ...createBuildEnvVars(input.config.code), + ...buildEnvironmentFactory.createBuildEnvVars(codeConfig), ...extraCargoEnvironmentVariables, }, ); @@ -133,158 +136,4 @@ final class RustBuildRunner { ], workingDirectory: crateDirectory), ); } - - ({String crateName, String toolchainChannel}) fetchAndValidateCrateInfo({ - required String manifestPath, - required String toolchainTomlPath, - required String targetTriple, - }) { - final [ - String crateName, - String toolchainChannel, - ] = RustValidationException.compose([ - () { - final (:crateName, :libCrateTypes) = cargoManifestParser.parseManifest( - manifestPath, - ); - - const requiredTypes = ['staticlib', 'cdylib']; - if (!requiredTypes.every(libCrateTypes.contains)) { - throw RustValidationException([ - 'Cargo.toml must specify $requiredTypes under lib.crate-types', - ]); - } - - return crateName; - }, - () { - final (:channel, :targets) = toolchainTomlParser.parseToolchainToml( - toolchainTomlPath, - ); - - final toolchainIssues = []; - - const deniedChannels = {'stable', 'beta', 'nightly'}; - if (deniedChannels.contains(channel)) { - toolchainIssues.add( - 'Your current channel in rust-toolchain.toml is $channel; ' - 'this is dangerous and consequently is not allowed! ' - 'Please specify an exact version to fix this issue.', - ); - } - - if (!targets.contains(targetTriple)) { - toolchainIssues.add( - '$targetTriple is not one of the supported targets: $targets', - ); - } - - return channel; - }, - ]); - - return (crateName: crateName, toolchainChannel: toolchainChannel); - } - - Map createBuildEnvVars(CodeConfig codeConfig) { - final CodeConfig(:targetOS, :targetTriple, :cCompiler) = codeConfig; - final targetTripleEnvVar = targetTriple.replaceAll('-', '_'); - - String getBinary(String binaryName) { - if (cCompiler == null) { - throw UnsupportedError( - 'CCompilerConfig was not provided but is required for $targetTriple', - ); - } - - final binaryPath = path.join( - path.dirname(path.fromUri(cCompiler.compiler)), - OS.current.executableFileName(binaryName), - ); - - if (!File(binaryPath).existsSync()) { - throw RustValidationException([ - 'Binary $binaryPath not found; is your installed compiler too old?', - ]); - } - - return binaryPath; - } - - return { - // NOTE: XCode makes some injections into PATH that break host build - // for crates with a build.rs - // See also: https://github.com/irondash/native_toolchain_rust/issues/17 - if (Platform.isMacOS) ...{ - 'PATH': Platform.environment['PATH']! - .split(':') - .where((e) => !e.contains('Contents/Developer/')) - .join(':'), - }, - - // NOTE: we need to point to NDK >=27 vended LLVM for Android. - // The `${targetTriple}35-clang`s were introduced in NDK 27, - // so using these binaries: - // 1. Ensures we are using a compatible NDK - // 2. Also fixes build issues when just using the `clang`s directly - if (targetOS == OS.android) ...{ - 'AR_$targetTripleEnvVar': getBinary('llvm-ar'), - 'CC_$targetTripleEnvVar': getBinary('${targetTriple}35-clang'), - 'CXX_$targetTripleEnvVar': getBinary('${targetTriple}35-clang++'), - 'CARGO_TARGET_${targetTripleEnvVar.toUpperCase()}_LINKER': getBinary( - '${targetTriple}35-clang', - ), - }, - }; - } -} - -extension on CodeConfig { - String get targetTriple { - return switch ((targetOS, targetArchitecture)) { - // Android - (OS.android, Architecture.arm64) => 'aarch64-linux-android', - (OS.android, Architecture.arm) => 'armv7-linux-androideabi', - (OS.android, Architecture.x64) => 'x86_64-linux-android', - - // iOS - (OS.iOS, Architecture.arm64) - when iOS.targetSdk == IOSSdk.iPhoneSimulator => - 'aarch64-apple-ios-sim', - (OS.iOS, Architecture.arm64) when iOS.targetSdk == IOSSdk.iPhoneOS => - 'aarch64-apple-ios', - (OS.iOS, Architecture.arm64) => throw UnsupportedError( - 'Unknown IOSSdk: ${iOS.targetSdk}', - ), - (OS.iOS, Architecture.x64) => 'x86_64-apple-ios', - - // Windows - (OS.windows, Architecture.arm64) => 'aarch64-pc-windows-msvc', - (OS.windows, Architecture.x64) => 'x86_64-pc-windows-msvc', - - // Linux - (OS.linux, Architecture.arm64) => 'aarch64-unknown-linux-gnu', - (OS.linux, Architecture.x64) => 'x86_64-unknown-linux-gnu', - - // macOS - (OS.macOS, Architecture.arm64) => 'aarch64-apple-darwin', - (OS.macOS, Architecture.x64) => 'x86_64-apple-darwin', - - (_, _) => throw UnsupportedError( - 'Unsupported target: $targetOS on $targetArchitecture', - ), - }; - } - - LinkMode get linkMode { - return switch (linkModePreference) { - LinkModePreference.dynamic || - LinkModePreference.preferDynamic => DynamicLoadingBundled(), - LinkModePreference.static || - LinkModePreference.preferStatic => StaticLinking(), - _ => throw UnsupportedError( - 'Unsupported LinkModePreference: $linkModePreference', - ), - }; - } } diff --git a/lib/src/config_mapping.dart b/lib/src/config_mapping.dart new file mode 100644 index 0000000..739b11d --- /dev/null +++ b/lib/src/config_mapping.dart @@ -0,0 +1,53 @@ +import 'package:code_assets/code_assets.dart'; +import 'package:meta/meta.dart'; + +@internal +extension CodeConfigMapping on CodeConfig { + String get targetTriple { + return switch ((targetOS, targetArchitecture)) { + // Android + (OS.android, Architecture.arm64) => 'aarch64-linux-android', + (OS.android, Architecture.arm) => 'armv7-linux-androideabi', + (OS.android, Architecture.x64) => 'x86_64-linux-android', + + // iOS + (OS.iOS, Architecture.arm64) + when iOS.targetSdk == IOSSdk.iPhoneSimulator => + 'aarch64-apple-ios-sim', + (OS.iOS, Architecture.arm64) when iOS.targetSdk == IOSSdk.iPhoneOS => + 'aarch64-apple-ios', + (OS.iOS, Architecture.arm64) => throw UnsupportedError( + 'Unknown IOSSdk: ${iOS.targetSdk}', + ), + (OS.iOS, Architecture.x64) => 'x86_64-apple-ios', + + // Windows + (OS.windows, Architecture.arm64) => 'aarch64-pc-windows-msvc', + (OS.windows, Architecture.x64) => 'x86_64-pc-windows-msvc', + + // Linux + (OS.linux, Architecture.arm64) => 'aarch64-unknown-linux-gnu', + (OS.linux, Architecture.x64) => 'x86_64-unknown-linux-gnu', + + // macOS + (OS.macOS, Architecture.arm64) => 'aarch64-apple-darwin', + (OS.macOS, Architecture.x64) => 'x86_64-apple-darwin', + + (_, _) => throw UnsupportedError( + 'Unsupported target: $targetOS on $targetArchitecture', + ), + }; + } + + LinkMode get linkMode { + return switch (linkModePreference) { + LinkModePreference.dynamic || + LinkModePreference.preferDynamic => DynamicLoadingBundled(), + LinkModePreference.static || + LinkModePreference.preferStatic => StaticLinking(), + _ => throw UnsupportedError( + 'Unsupported LinkModePreference: $linkModePreference', + ), + }; + } +} diff --git a/lib/src/crate_info_validator.dart b/lib/src/crate_info_validator.dart new file mode 100644 index 0000000..11b219d --- /dev/null +++ b/lib/src/crate_info_validator.dart @@ -0,0 +1,70 @@ +import 'package:meta/meta.dart'; +import 'package:native_toolchain_rs/src/exception.dart'; +import 'package:native_toolchain_rs/src/toml_parsing.dart'; + +@internal +interface class CrateInfoValidator { + const CrateInfoValidator({ + required this.toolchainTomlParser, + required this.cargoManifestParser, + }); + + final ToolchainTomlParser toolchainTomlParser; + final CargoManifestParser cargoManifestParser; + + ({String crateName, String toolchainChannel}) fetchAndValidateCrateInfo({ + required String manifestPath, + required String toolchainTomlPath, + required String targetTriple, + }) { + final [ + String crateName, + String toolchainChannel, + ] = RustValidationException.compose([ + () { + final (:crateName, :libCrateTypes) = cargoManifestParser.parseManifest( + manifestPath, + ); + + const requiredTypes = ['staticlib', 'cdylib']; + if (!requiredTypes.every(libCrateTypes.contains)) { + throw RustValidationException([ + 'Cargo.toml must specify $requiredTypes under lib.crate-types', + ]); + } + + return crateName; + }, + () { + final (:channel, :targets) = toolchainTomlParser.parseToolchainToml( + toolchainTomlPath, + ); + + final toolchainIssues = []; + + const deniedChannels = {'stable', 'beta', 'nightly'}; + if (deniedChannels.contains(channel)) { + toolchainIssues.add( + 'Your current channel in rust-toolchain.toml is $channel; ' + 'this is dangerous and consequently is not allowed! ' + 'Please specify an exact version to fix this issue.', + ); + } + + if (!targets.contains(targetTriple)) { + toolchainIssues.add( + '$targetTriple is not one of the supported targets: $targets', + ); + } + + if (toolchainIssues.isNotEmpty) { + throw RustValidationException(toolchainIssues); + } + + return channel; + }, + ]); + + return (crateName: crateName, toolchainChannel: toolchainChannel); + } +} diff --git a/lib/src/crate_resolver.dart b/lib/src/crate_resolver.dart index ec71355..110f7db 100644 --- a/lib/src/crate_resolver.dart +++ b/lib/src/crate_resolver.dart @@ -1,12 +1,11 @@ import 'dart:io'; +import 'package:meta/meta.dart'; import 'package:native_toolchain_rs/src/exception.dart'; import 'package:path/path.dart' as path; -// NOTE: this is an internal implementation detail -// ignore_for_file: public_member_api_docs - -final class CrateDirectoryResolver { +@internal +interface class CrateDirectoryResolver { const CrateDirectoryResolver(); Directory resolveCrateDirectory({ diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 4926809..26c211f 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -2,20 +2,22 @@ import 'dart:io'; import 'package:meta/meta.dart'; -// NOTE: the API for our exceptions is currently experimental -// ignore_for_file: public_member_api_docs - /// An [Exception] representing a failure while trying to build Rust assets. sealed class RustBuildException implements Exception {} +/// A [RustBuildException] that specifies there were some issues +/// while validating the project. +/// /// # WARNING /// This is experimental! /// It may change on any new release without notice! /// Please file an issue with your use-case for it, if you do use it. @experimental final class RustValidationException implements RustBuildException { - @experimental + /// Creates a [RustValidationException] with [validationErrors]. const RustValidationException(this.validationErrors); + + /// The validation issues encountered. final List validationErrors; /// Calls all [functions] and throws an aggregate [RustValidationException] @@ -45,15 +47,23 @@ final class RustValidationException implements RustBuildException { 'RustValidationException(validationErrors: $validationErrors)'; } +/// A [RustBuildException] that specifies there was an issue +/// when invoking an external process. +/// /// # WARNING /// This is experimental! /// It may change on any new release without notice! /// Please file an issue with your use-case for it, if you do use it. @experimental final class RustProcessException implements RustBuildException { - @experimental + /// Creates a [RustProcessException] with [message] and [inner]. const RustProcessException(this.message, {this.inner}); + + /// The message associated with this [RustProcessException]. final String message; + + /// The inner [ProcessException], + /// in case this [RustProcessException] is wrapping around one. final ProcessException? inner; @override diff --git a/lib/src/process_runner.dart b/lib/src/process_runner.dart index 18f1b1a..bf6e3ad 100644 --- a/lib/src/process_runner.dart +++ b/lib/src/process_runner.dart @@ -1,12 +1,11 @@ import 'dart:io'; import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:native_toolchain_rs/src/exception.dart'; -// NOTE: this is an internal implementation detail -// ignore_for_file: public_member_api_docs - -final class ProcessRunner { +@internal +interface class ProcessRunner { const ProcessRunner(this.logger); final Logger? logger; @@ -46,6 +45,7 @@ final class ProcessRunner { } } +@internal extension InvokeRustup on ProcessRunner { Future invokeRustup( List arguments, { diff --git a/lib/src/toml_parsing.dart b/lib/src/toml_parsing.dart index 01690d4..e8a5026 100644 --- a/lib/src/toml_parsing.dart +++ b/lib/src/toml_parsing.dart @@ -1,13 +1,12 @@ import 'dart:io'; import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:native_toolchain_rs/src/exception.dart'; import 'package:toml/toml.dart'; -// NOTE: this is an internal implementation detail -// ignore_for_file: public_member_api_docs - -final class TomlDocumentWrapperFactory { +@internal +interface class TomlDocumentWrapperFactory { const TomlDocumentWrapperFactory(this.logger); final Logger? logger; @@ -15,6 +14,7 @@ final class TomlDocumentWrapperFactory { TomlDocumentWrapper(logger, filePath, TomlDocument.loadSync(filePath)); } +@internal final class TomlDocumentWrapper { const TomlDocumentWrapper(this.logger, this.filePath, this.document); @@ -42,7 +42,8 @@ final class TomlDocumentWrapper { } } -final class CargoManifestParser { +@internal +interface class CargoManifestParser { const CargoManifestParser(this.logger, this.tomlDocumentFactory); final Logger? logger; final TomlDocumentWrapperFactory tomlDocumentFactory; @@ -76,7 +77,8 @@ final class CargoManifestParser { } } -final class ToolchainTomlParser { +@internal +interface class ToolchainTomlParser { const ToolchainTomlParser(this.logger, this.tomlDocumentFactory); final Logger? logger; final TomlDocumentWrapperFactory tomlDocumentFactory; diff --git a/pubspec.yaml b/pubspec.yaml index c048769..c1d2858 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,8 +18,18 @@ dependencies: dev_dependencies: melos: ^7.1.1 + mocktail: ^1.0.4 test: ^1.26.3 very_good_analysis: ^10.0.0 melos: useRootAsPackage: true + scripts: + test: + description: Run unit tests for a specific package in this project. + run: flutter test test + packageFilters: + dirExists: test + exec: + concurrency: 1 + failFast: true diff --git a/test/crate_info_validator_test.dart b/test/crate_info_validator_test.dart new file mode 100644 index 0000000..f590a03 --- /dev/null +++ b/test/crate_info_validator_test.dart @@ -0,0 +1,76 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:native_toolchain_rs/src/crate_info_validator.dart'; +import 'package:native_toolchain_rs/src/exception.dart'; +import 'package:native_toolchain_rs/src/toml_parsing.dart'; +import 'package:test/test.dart'; + +class MockCargoManifestParser extends Mock implements CargoManifestParser {} + +class MockToolchainTomlParser extends Mock implements ToolchainTomlParser {} + +void main() { + group('CrateInfoValidator', () { + late CrateInfoValidator validator; + late MockToolchainTomlParser mockToolchainTomlParser; + late MockCargoManifestParser mockCargoManifestParser; + + setUp(() { + mockToolchainTomlParser = MockToolchainTomlParser(); + mockCargoManifestParser = MockCargoManifestParser(); + validator = CrateInfoValidator( + toolchainTomlParser: mockToolchainTomlParser, + cargoManifestParser: mockCargoManifestParser, + ); + }); + + test('fetchAndValidateCrateInfo returns correct info on success', () { + when(() => mockCargoManifestParser.parseManifest(any())).thenReturn(( + crateName: 'my_crate', + libCrateTypes: ['staticlib', 'cdylib'], + )); + when(() => mockToolchainTomlParser.parseToolchainToml(any())).thenReturn(( + channel: '1.90.0', + targets: {'aarch64-linux-android'}, + )); + + final result = validator.fetchAndValidateCrateInfo( + manifestPath: 'dummy_manifest_path', + toolchainTomlPath: 'dummy_toolchain_path', + targetTriple: 'aarch64-linux-android', + ); + + expect(result.crateName, 'my_crate'); + expect(result.toolchainChannel, '1.90.0'); + }); + + test('fetchAndValidateCrateInfo throws exception on validation issues', () { + when(() => mockCargoManifestParser.parseManifest(any())).thenReturn(( + crateName: 'my_crate', + libCrateTypes: ['staticlib'], + )); + when(() => mockToolchainTomlParser.parseToolchainToml(any())).thenReturn(( + channel: 'stable', + targets: {'x86_64-linux-gnu'}, + )); + + expect( + () => validator.fetchAndValidateCrateInfo( + manifestPath: 'dummy_manifest_path', + toolchainTomlPath: 'dummy_toolchain_path', + targetTriple: 'aarch64-linux-android', + ), + throwsA( + isA().having( + (e) => e.validationErrors, + 'validationErrors', + containsAll([ + '''Cargo.toml must specify [staticlib, cdylib] under lib.crate-types''', + '''Your current channel in rust-toolchain.toml is stable; this is dangerous and consequently is not allowed! Please specify an exact version to fix this issue.''', + '''aarch64-linux-android is not one of the supported targets: {x86_64-linux-gnu}''', + ]), + ), + ), + ); + }); + }); +} diff --git a/test/crate_resolver_test.dart b/test/crate_resolver_test.dart new file mode 100644 index 0000000..a079b4e --- /dev/null +++ b/test/crate_resolver_test.dart @@ -0,0 +1,61 @@ +import 'dart:io'; + +import 'package:native_toolchain_rs/src/crate_resolver.dart'; +import 'package:native_toolchain_rs/src/exception.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + group('CrateDirectoryResolver', () { + late Directory tempDir; + const resolver = CrateDirectoryResolver(); + + setUp(() { + tempDir = Directory.systemTemp.createTempSync('crate_resolver_test'); + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + test('resolveCrateDirectory returns the correct directory', () { + final crateDir = Directory(path.join(tempDir.path, 'crate')) + ..createSync(); + + final result = resolver.resolveCrateDirectory( + rootPath: tempDir.path, + cratePathOptions: ['crate'], + ); + + expect(result.path, equals(crateDir.path)); + }); + + test( + 'resolveCrateDirectory throws RustValidationException ' + 'if no directory exists', + () { + expect( + () => resolver.resolveCrateDirectory( + rootPath: tempDir.path, + cratePathOptions: ['non_existent_crate'], + ), + throwsA(isA()), + ); + }, + ); + + test('resolveCrateDirectory returns the first existing directory', () { + final crateDir1 = Directory(path.join(tempDir.path, 'crate1')); + final crateDir2 = Directory(path.join(tempDir.path, 'crate2')); + crateDir1.createSync(); + crateDir2.createSync(); + + final result = resolver.resolveCrateDirectory( + rootPath: tempDir.path, + cratePathOptions: ['crate1', 'crate2'], + ); + + expect(result.path, equals(crateDir1.path)); + }); + }); +} diff --git a/test/exception_test.dart b/test/exception_test.dart new file mode 100644 index 0000000..08f9d19 --- /dev/null +++ b/test/exception_test.dart @@ -0,0 +1,40 @@ +import 'package:native_toolchain_rs/src/exception.dart'; +import 'package:test/test.dart'; + +void main() { + group('RustValidationException', () { + test('compose returns results when no exceptions are thrown', () { + final results = RustValidationException.compose([ + () => 1, + () => 2, + ]); + expect(results, equals([1, 2])); + }); + + test('compose throws aggregate exception', () { + expect( + () => RustValidationException.compose([ + () => throw const RustValidationException(['error1']), + () => 1, + () => throw const RustValidationException(['error2', 'error3']), + ]), + throwsA( + isA().having( + (e) => e.validationErrors, + 'validationErrors', + equals(['error1', 'error2', 'error3']), + ), + ), + ); + }); + + test('compose does not catch other exceptions', () { + expect( + () => RustValidationException.compose([ + () => throw Exception('some other error'), + ]), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/process_runner_test.dart b/test/process_runner_test.dart new file mode 100644 index 0000000..fbde97e --- /dev/null +++ b/test/process_runner_test.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:native_toolchain_rs/src/exception.dart'; +import 'package:native_toolchain_rs/src/process_runner.dart'; +import 'package:test/test.dart'; + +void main() { + group('ProcessRunner', () { + late ProcessRunner processRunner; + late List records; + + setUp(() { + records = []; + final logger = Logger.detached('test')..onRecord.listen(records.add); + processRunner = ProcessRunner(logger); + }); + + test('invoke succeeds with exit code 0', () async { + final result = await processRunner.invoke('echo', ['hello']); + expect(result.trim(), equals('hello')); + expect(records, hasLength(1)); + expect(records.single.message, contains('Invoking "echo [hello]')); + }); + + test('invoke throws RustProcessException on non-zero exit code', () { + expect( + () => processRunner.invoke('dart', ['run', 'non_existent_file.dart']), + throwsA(isA()), + ); + }); + + test('invoke throws ProcessException on command not found', () { + expect( + () => processRunner.invoke('command_not_found', []), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/toml_parsing_test.dart b/test/toml_parsing_test.dart new file mode 100644 index 0000000..7174617 --- /dev/null +++ b/test/toml_parsing_test.dart @@ -0,0 +1,130 @@ +import 'dart:io'; + +import 'package:native_toolchain_rs/src/exception.dart'; +import 'package:native_toolchain_rs/src/toml_parsing.dart'; +import 'package:test/test.dart'; + +void main() { + group('TomlDocumentWrapper', () { + late Directory tempDir; + late String tempFilePath; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync(); + tempFilePath = '${tempDir.path}/test.toml'; + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + test('walk returns value when path is valid', () { + File(tempFilePath).writeAsStringSync('key = "value"\n'); + const factory = TomlDocumentWrapperFactory(null); + final wrapper = factory.parseFile(tempFilePath); + expect(wrapper.walk('key'), 'value'); + }); + + test('walk throws RustValidationException when path is invalid', () { + File(tempFilePath).writeAsStringSync('key = "value"\n'); + const factory = TomlDocumentWrapperFactory(null); + final wrapper = factory.parseFile(tempFilePath); + expect( + () => wrapper.walk('invalid.path'), + throwsA(isA()), + ); + }); + }); + + group('CargoManifestParser', () { + const tomlDocumentFactory = TomlDocumentWrapperFactory(null); + const parser = CargoManifestParser(null, tomlDocumentFactory); + late Directory tempDir; + late String tempManifestPath; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync(); + tempManifestPath = '${tempDir.path}/Cargo.toml'; + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + test('parseManifest returns crate name and lib crate types', () { + File(tempManifestPath).writeAsStringSync(''' +[package] +name = "my_crate" + +[lib] +crate-type = ["staticlib"] +'''); + + final result = parser.parseManifest(tempManifestPath); + + expect(result.crateName, 'my_crate'); + expect(result.libCrateTypes, ['staticlib']); + }); + + test('parseManifest throws when Cargo.toml not found', () { + expect( + () => parser.parseManifest('non_existent_Cargo.toml'), + throwsA(isA()), + ); + }); + + test('parseManifest throws when parsing fails', () { + File(tempManifestPath).writeAsStringSync('invalid toml'); + + expect( + () => parser.parseManifest(tempManifestPath), + throwsA(isA()), + ); + }); + }); + + group('ToolchainTomlParser', () { + const tomlDocumentFactory = TomlDocumentWrapperFactory(null); + const parser = ToolchainTomlParser(null, tomlDocumentFactory); + late Directory tempDir; + late String tempToolchainTomlPath; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync(); + tempToolchainTomlPath = '${tempDir.path}/rust-toolchain.toml'; + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + test('parseToolchainToml returns channel and targets', () { + File(tempToolchainTomlPath).writeAsStringSync(''' +[toolchain] +channel = "stable" +targets = ["aarch64-apple-darwin"] +'''); + + final result = parser.parseToolchainToml(tempToolchainTomlPath); + + expect(result.channel, 'stable'); + expect(result.targets, {'aarch64-apple-darwin'}); + }); + + test('parseToolchainToml throws when rust-toolchain.toml not found', () { + expect( + () => parser.parseToolchainToml('non_existent_rust-toolchain.toml'), + throwsA(isA()), + ); + }); + + test('parseToolchainToml throws when parsing fails', () { + File(tempToolchainTomlPath).writeAsStringSync('invalid toml'); + + expect( + () => parser.parseToolchainToml(tempToolchainTomlPath), + throwsA(isA()), + ); + }); + }); +}