| // Copyright (c) 2026, 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 'dart:isolate'; |
| |
| import 'package:args/command_runner.dart'; |
| import 'package:dartdev/dartdev.dart'; |
| import 'package:dartdev/src/commands/run.dart'; |
| import 'package:dartdev/src/eval_packages.dart'; |
| import 'package:dartdev/src/utils.dart'; |
| import 'package:dartdev/src/vm_interop_handler.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:test/test.dart'; |
| |
| import '../utils.dart'; |
| |
| void main() { |
| group('eval option parsing', () { |
| test( |
| 'globalDartdevOptionsParser does not include -e, -P / --package-constraint, or --offline', |
| () { |
| final parser = globalDartdevOptionsParser(); |
| expect(parser.options.containsKey(evalOption), isFalse); |
| expect(parser.options.containsKey(packageConstraintOption), isFalse); |
| expect(parser.options.containsKey(offlineOption), isFalse); |
| expect(parser.options.containsKey('package'), isFalse); |
| expect(parser.options.containsKey('with'), isFalse); |
| }, |
| ); |
| |
| test( |
| 'RunCommand argParser includes -e, -P / --package-constraint, --offline', |
| () { |
| final runCmd = RunCommand(); |
| expect(runCmd.argParser.options.containsKey(evalOption), isTrue); |
| expect(runCmd.argParser.options[evalOption]?.abbr, equals('e')); |
| expect( |
| runCmd.argParser.options.containsKey(packageConstraintOption), |
| isTrue, |
| ); |
| expect( |
| runCmd.argParser.options[packageConstraintOption]?.abbr, |
| equals('P'), |
| ); |
| expect(runCmd.argParser.options.containsKey(offlineOption), isTrue); |
| expect(runCmd.argParser.options.containsKey('package'), isFalse); |
| expect(runCmd.argParser.options.containsKey('with'), isFalse); |
| }, |
| ); |
| |
| test( |
| 'RunCommand argParser accepts VM options after -P and -e while preserving script args', |
| () { |
| final runCmd = RunCommand(); |
| final parsed = runCmd.argParser.parse(const [ |
| '-P', |
| 'path', |
| '--enable-asserts', |
| '-e', |
| 'void main(List<String> args) { assert(args.isNotEmpty); }', |
| '--define=foo=bar', |
| 'script_arg', |
| '--script-flag', |
| ]); |
| expect(parsed.multiOption(packageConstraintOption), equals(['path'])); |
| expect(parsed.flag('enable-asserts'), isTrue); |
| expect(parsed.multiOption('define'), equals(['foo=bar'])); |
| expect( |
| parsed.option(evalOption), |
| equals('void main(List<String> args) { assert(args.isNotEmpty); }'), |
| ); |
| expect(parsed.rest, equals(['script_arg', '--script-flag'])); |
| }, |
| ); |
| |
| test('RunCommand argParser parses -e, -P, --offline, and arguments', () { |
| final runCmd = RunCommand(); |
| final results = runCmd.argParser.parse(const [ |
| '-P', |
| 'http', |
| '--offline', |
| '-e', |
| 'void main() {}', |
| 'arg1', |
| 'arg2', |
| ]); |
| expect(results.wasParsed(evalOption), isTrue); |
| expect(results.option(evalOption), equals('void main() {}')); |
| expect(results.wasParsed(packageConstraintOption), isTrue); |
| expect( |
| results.multiOption(packageConstraintOption), |
| equals(const ['http']), |
| ); |
| expect(results.wasParsed(offlineOption), isTrue); |
| expect(results.flag(offlineOption), isTrue); |
| expect(results.rest, equals(const ['arg1', 'arg2'])); |
| }); |
| }); |
| |
| group('PackageUriExtension', () { |
| test('isPackage correctly identifies valid package URIs', () { |
| expect(Uri.parse('package:http/http.dart').isPackage, isTrue); |
| expect(Uri.parse('package:path').isPackage, isTrue); |
| expect(Uri.parse('package:').isPackage, isFalse); |
| expect(Uri.parse('package:/').isPackage, isFalse); |
| expect(Uri.parse('dart:io').isPackage, isFalse); |
| expect(Uri.parse('file:///foo/bar.dart').isPackage, isFalse); |
| expect(Uri.parse('https://example.com').isPackage, isFalse); |
| }); |
| |
| test('packageName extracts package name from package URIs', () { |
| expect( |
| Uri.parse('package:http/http.dart').packageName, |
| equals('http'), |
| ); |
| expect( |
| Uri.parse('package:path').packageName, |
| equals('path'), |
| ); |
| expect(Uri.parse('package:').packageName, isNull); |
| expect(Uri.parse('package:/').packageName, isNull); |
| expect(Uri.parse('dart:io').packageName, isNull); |
| expect(Uri.parse('file:///foo/bar.dart').packageName, isNull); |
| }); |
| }); |
| |
| group('EvalPackageResolver AST import extraction', () { |
| test('extracts package imports using Dart AST parser', () { |
| const code = ''' |
| import 'dart:io'; |
| import 'package:http/http.dart' as http; |
| import 'package:path/path.dart' show join; |
| |
| // import 'package:commented_out/commented_out.dart'; |
| /* |
| import 'package:block_commented/block.dart'; |
| */ |
| |
| void main() { |
| const fakeImport = "import 'package:in_string/in_string.dart'"; |
| print(fakeImport); |
| } |
| '''; |
| final imports = EvalPackageResolver.extractPackageImports(code); |
| expect(imports, equals(const {'http', 'path'})); |
| }); |
| }); |
| |
| group('EvalPackageResolver package config resolution', () { |
| late Directory tempDir; |
| |
| setUp(() { |
| tempDir = Directory.systemTemp.createTempSync('dartdev_eval_test_'); |
| }); |
| |
| tearDown(() { |
| if (tempDir.existsSync()) { |
| tempDir.deleteSync(recursive: true); |
| } |
| }); |
| |
| test('throws StateError when localPackageConfig does not exist', () async { |
| final nonexistent = path.join(tempDir.path, 'missing_config.json'); |
| await expectLater( |
| EvalPackageResolver.resolvePackageConfig( |
| 'import "package:foo/foo.dart"; void main() {}', |
| localPackageConfig: nonexistent, |
| ), |
| throwsA( |
| isA<StateError>().having( |
| (e) => e.message, |
| 'message', |
| contains('Specified package configuration file does not exist'), |
| ), |
| ), |
| ); |
| }); |
| |
| test( |
| 'throws StateError when localPackageConfig has invalid JSON', |
| () async { |
| final invalidFile = File(path.join(tempDir.path, 'bad_config.json')) |
| ..writeAsStringSync('{ malformed json'); |
| await expectLater( |
| EvalPackageResolver.resolvePackageConfig( |
| 'import "package:foo/foo.dart"; void main() {}', |
| localPackageConfig: invalidFile.path, |
| ), |
| throwsA( |
| isA<StateError>().having( |
| (e) => e.message, |
| 'message', |
| contains('Failed to parse package configuration file'), |
| ), |
| ), |
| ); |
| }, |
| ); |
| |
| test( |
| 'reuses localPackageConfig when no explicit constraints and packages present', |
| () async { |
| final configFile = File(path.join(tempDir.path, 'valid_config.json')) |
| ..writeAsStringSync( |
| jsonEncode({ |
| 'configVersion': 2, |
| 'packages': [ |
| {'name': 'http', 'rootUri': 'file:///mock/http'}, |
| ], |
| }), |
| ); |
| |
| final result = await EvalPackageResolver.resolvePackageConfig( |
| 'import "package:http/http.dart"; void main() {}', |
| localPackageConfig: configFile.path, |
| ); |
| |
| expect(result, equals(configFile.path)); |
| }, |
| ); |
| |
| test( |
| 'resolves ephemeral pub get on pre-release SDKs using -0 version suffix', |
| () async { |
| final mockPkgDir = Directory(path.join(tempDir.path, 'mock_pkg')) |
| ..createSync(recursive: true); |
| File(path.join(mockPkgDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: mock_pkg |
| version: 1.0.0 |
| environment: |
| sdk: '>=3.0.0 <4.0.0' |
| '''); |
| Directory(path.join(mockPkgDir.path, 'lib')).createSync(); |
| File( |
| path.join(mockPkgDir.path, 'lib', 'mock_pkg.dart'), |
| ).writeAsStringSync('final int meaning = 42;\n'); |
| |
| final result = await EvalPackageResolver.resolvePackageConfig( |
| 'import "package:mock_pkg/mock_pkg.dart"; void main() {}', |
| packageConstraints: [ |
| 'mock_pkg: {path: "${mockPkgDir.path.replaceAll(r'\', '/')}"}', |
| ], |
| offline: true, |
| ); |
| |
| expect(result, isNotNull); |
| expect(File(result!).existsSync(), isTrue); |
| final generatedPubspec = File( |
| path.join(File(result).parent.parent.path, 'pubspec.yaml'), |
| ).readAsStringSync(); |
| expect(generatedPubspec, contains(RegExp(r'sdk: "\^\d+\.\d+\.\d+-0"'))); |
| }, |
| ); |
| }); |
| |
| group('RunCommand.runEval execution', () { |
| late ReceivePort receivePort; |
| late StreamController<List<Object?>> vmInteropEvents; |
| |
| setUp(() { |
| vmInteropEvents = StreamController<List<Object?>>(); |
| receivePort = ReceivePort() |
| ..listen((msg) { |
| if (msg is List) { |
| vmInteropEvents.add(msg); |
| } |
| }); |
| VmInteropHandler.initialize(receivePort.sendPort); |
| }); |
| |
| tearDown(() { |
| receivePort.close(); |
| vmInteropEvents.close(); |
| VmInteropHandler.initialize(null); |
| }); |
| |
| test( |
| 'runEval invokes VmInteropHandler with data URI and useExecProcess', |
| () async { |
| const code = 'void main(List<String> args) { print("hello"); }'; |
| final runCmd = RunCommand(); |
| final args = runCmd.argParser.parse( |
| const ['-e', code, 'arg1', 'arg2'], |
| ); |
| |
| final exitCode = await runCmd.runEval(args); |
| expect(exitCode, equals(0)); |
| |
| final message = await vmInteropEvents.stream.first; |
| expect(message[0], equals(2)); |
| final scriptUri = Uri.parse(message[1] as String); |
| expect(scriptUri.scheme, equals('data')); |
| expect(scriptUri.data?.mimeType, equals(dartMimeType)); |
| expect(scriptUri.data?.contentAsString(encoding: utf8), equals(code)); |
| expect(message[5], equals(const ['arg1', 'arg2'])); |
| }, |
| ); |
| |
| test( |
| 'runEval preserves multiline code verbatim without modification', |
| () async { |
| const multilineCode = ''' |
| import 'dart:math'; |
| |
| class Calculator { |
| int add(int a, int b) => a + b; |
| } |
| |
| void main(List<String> args) { |
| final calc = Calculator(); |
| print(calc.add(2, 3)); |
| } |
| '''; |
| final runCmd = RunCommand(); |
| final args = runCmd.argParser.parse(const ['-e', multilineCode]); |
| |
| final exitCode = await runCmd.runEval(args); |
| expect(exitCode, equals(0)); |
| |
| final message = await vmInteropEvents.stream.first; |
| expect(message[0], equals(2)); |
| final scriptUri = Uri.parse(message[1] as String); |
| expect(scriptUri.scheme, equals('data')); |
| expect( |
| scriptUri.data?.contentAsString(encoding: utf8), |
| equals(multilineCode), |
| ); |
| }, |
| ); |
| |
| test('runEval respects explicit --packages option', () async { |
| const code = 'void main() {}'; |
| final runCmd = RunCommand(); |
| final args = runCmd.argParser.parse(const [ |
| '--packages=/custom/path/package_config.json', |
| '-e', |
| code, |
| ]); |
| |
| final exitCode = await runCmd.runEval(args); |
| expect(exitCode, equals(0)); |
| |
| final message = await vmInteropEvents.stream.first; |
| expect(message[0], equals(2)); |
| expect(message[3], equals('/custom/path/package_config.json')); |
| }); |
| |
| test('runEval returns errorExitCode when code is empty', () async { |
| final runCmd = RunCommand(); |
| final args = runCmd.argParser.parse(const ['-e', '']); |
| |
| final exitCode = await runCmd.runEval(args); |
| expect(exitCode, equals(RunCommand.errorExitCode)); |
| }); |
| }); |
| |
| group('DartdevRunner top-level eval option rejection', () { |
| test('raw dart -e throws UsageException', () async { |
| const args = ['--suppress-analytics', '-e', 'void main() { print(1); }']; |
| final runner = DartdevRunner(args); |
| await expectLater( |
| runner.run(args), |
| throwsA(isA<UsageException>()), |
| ); |
| }); |
| |
| test('raw dart -P <pkg> throws UsageException', () async { |
| const args = ['--suppress-analytics', '-P', 'path']; |
| final runner = DartdevRunner(args); |
| await expectLater( |
| runner.run(args), |
| throwsA(isA<UsageException>()), |
| ); |
| }); |
| |
| test('raw dart --eval throws UsageException', () async { |
| const args = [ |
| '--suppress-analytics', |
| '--eval', |
| 'void main() { print(1); }', |
| ]; |
| final runner = DartdevRunner(args); |
| await expectLater( |
| runner.run(args), |
| throwsA(isA<UsageException>()), |
| ); |
| }); |
| }); |
| |
| group('end-to-end process execution', () { |
| test('dart -e exits with usage exit code 64', () async { |
| final p = project(); |
| final result = await p.run(['-e', 'void main() {}']); |
| expect(result.exitCode, equals(DartdevRunner.usageExitCode)); |
| }); |
| |
| test( |
| 'dart run -e succeeds from a directory with no pubspec.yaml or package_config.json', |
| () async { |
| final emptyDir = Directory.systemTemp.createTempSync('eval_no_pkg_'); |
| addTearDown(() { |
| if (emptyDir.existsSync()) { |
| emptyDir.deleteSync(recursive: true); |
| } |
| }); |
| final p = project(); |
| final result = await p.run( |
| ['run', '-e', 'void main() { print("hello_outside_package"); }'], |
| workingDir: emptyDir.path, |
| ); |
| expect(result.exitCode, equals(0), reason: 'stderr: ${result.stderr}'); |
| expect(result.stdout, contains('hello_outside_package')); |
| }, |
| ); |
| |
| test( |
| 'dart run enables assertions when --enable-asserts follows -e', |
| () async { |
| final p = project(); |
| final resultAfterE = await p.run([ |
| 'run', |
| '-e', |
| 'void main() { assert(false, "boom_after_e"); }', |
| '--enable-asserts', |
| ]); |
| expect(resultAfterE.exitCode, isNot(equals(0))); |
| expect(resultAfterE.stderr, contains('boom_after_e')); |
| }, |
| ); |
| |
| test( |
| 'dart run -e passes flags after -- separator to script args instead of VM', |
| () async { |
| final p = project(); |
| final result = await p.run([ |
| 'run', |
| '-e', |
| r'void main(List<String> args) { assert(false); print("args=$args"); }', |
| '--', |
| '--enable-asserts', |
| ]); |
| expect(result.exitCode, equals(0), reason: 'stderr: ${result.stderr}'); |
| expect(result.stdout, contains('args=[--enable-asserts]')); |
| }, |
| ); |
| }, timeout: longTimeout); |
| } |