diff --git a/.github/workflow/test.yml b/.github/workflow/test.yml new file mode 100644 index 0000000..946c551 --- /dev/null +++ b/.github/workflow/test.yml @@ -0,0 +1,38 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Dart + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: dart-lang/setup-dart@stable + + - name: Install dependencies + run: dart pub get + + # Uncomment this step to verify the use of 'dart format' on each commit. + # - name: Verify formatting + # run: dart format --output=none --set-exit-if-changed . + + # Consider passing '--fatal-infos' for slightly stricter analysis. + - name: Analyze project source + run: dart analyze + + # Your project will need to have tests in test/ and a dependency on + # package:test for this step to succeed. Note that Flutter projects will + # want to change this to 'flutter test'. + - name: Run tests + run: dart test diff --git a/.gitignore b/.gitignore index 1586ec0..886b8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ pubspec.lock # Directory created by dartdoc doc/api/ -*.iml \ No newline at end of file +*.iml +.tool-versions \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b4b912d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: dart -dart: - - stable - -jobs: - include: - - stage: test - script: pub get && pub run test -j1 - -stages: -- test - -branches: - only: - - master diff --git a/analysis_options.yaml b/analysis_options.yaml index 97f0908..f4b1672 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,15 +1,5 @@ -analyzer: - strong-mode: true -# exclude: -# - path/to/excluded/files/** +include: package:lint/analysis_options.yaml -# Lint rules and documentation, see http://dart-lang.github.io/linter/lints -linter: - rules: - - cancel_subscriptions - - hash_and_equals - - iterable_contains_unrelated_type - - list_remove_unrelated_type - - test_types_in_equals - - unrelated_type_equality_checks - - valid_regexps +analyzer: + strong-mode: + implicit-casts: true diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index be0c50d..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,19 +0,0 @@ -branches: - only: - - master - -install: - - ps: wget https://storage.googleapis.com/dart-archive/channels/stable/release/latest/sdk/dartsdk-windows-x64-release.zip -OutFile dart-sdk.zip - - cmd: echo "Unzipping dart-sdk..." - - cmd: 7z x dart-sdk.zip -o"C:\tools" -y > nul - - set PATH=%PATH%;C:\tools\dart-sdk\bin - - set PATH=%PATH%;%APPDATA%\Pub\Cache\bin - -build: off - -test_script: - - pub get - - pub run test -j1 - -cache: - - C:\Users\appveyor\AppData\Roaming\Pub\Cache \ No newline at end of file diff --git a/lib/isolate_executor.dart b/lib/isolate_executor.dart index e24d4aa..9b621b5 100644 --- a/lib/isolate_executor.dart +++ b/lib/isolate_executor.dart @@ -3,6 +3,6 @@ /// library isolate_executor; -export 'src/source_generator.dart'; -export 'src/executor.dart'; export 'src/executable.dart'; +export 'src/executor.dart'; +export 'src/source_generator.dart'; diff --git a/lib/src/executable.dart b/lib/src/executable.dart index cf982d9..260d063 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -8,30 +8,46 @@ abstract class Executable { Future execute(); final Map message; - final SendPort _sendPort; - - U instanceOf(String typeName, - {List positionalArguments: const [], Map namedArguments, Symbol constructorName}) { - ClassMirror typeMirror = currentMirrorSystem().isolate.rootLibrary.declarations[new Symbol(typeName)]; - if (typeMirror == null) { - typeMirror = currentMirrorSystem() - .libraries - .values - .where((lib) => lib.uri.scheme == "package" || lib.uri.scheme == "file") - .expand((lib) => lib.declarations.values) - .firstWhere((decl) => decl is ClassMirror && MirrorSystem.getName(decl.simpleName) == typeName, - orElse: () => throw new ArgumentError("Unknown type '$typeName'. Did you forget to import it?")); - } - - return typeMirror.newInstance(constructorName ?? const Symbol(""), positionalArguments, namedArguments).reflectee - as U; + final SendPort? _sendPort; + + U instanceOf( + String typeName, { + List positionalArguments = const [], + Map namedArguments = const {}, + Symbol constructorName = const Symbol(""), + }) { + ClassMirror? typeMirror = currentMirrorSystem() + .isolate + .rootLibrary + .declarations[Symbol(typeName)] as ClassMirror?; + + typeMirror ??= currentMirrorSystem() + .libraries + .values + .where((lib) => lib.uri.scheme == "package" || lib.uri.scheme == "file") + .expand((lib) => lib.declarations.values) + .firstWhere( + (decl) => + decl is ClassMirror && + MirrorSystem.getName(decl.simpleName) == typeName, + orElse: () => throw ArgumentError( + "Unknown type '$typeName'. Did you forget to import it?"), + ) as ClassMirror?; + + return typeMirror! + .newInstance( + constructorName, + positionalArguments, + namedArguments, + ) + .reflectee as U; } void send(dynamic message) { - _sendPort.send(message); + _sendPort!.send(message); } void log(String message) { - _sendPort.send({"_line_": message}); + _sendPort!.send({"_line_": message}); } } diff --git a/lib/src/executor.dart b/lib/src/executor.dart index 9367b23..0e3b999 100644 --- a/lib/src/executor.dart +++ b/lib/src/executor.dart @@ -2,43 +2,51 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; -import 'package:isolate_executor/src/executable.dart'; -import 'package:isolate_executor/src/source_generator.dart'; +import 'package:conduit_isolate_exec/src/executable.dart'; +import 'package:conduit_isolate_exec/src/source_generator.dart'; class IsolateExecutor { - IsolateExecutor(this.generator, {this.packageConfigURI, Map message}) : this.message = message ?? {}; + IsolateExecutor( + this.generator, { + this.packageConfigURI, + this.message = const {}, + }); final SourceGenerator generator; final Map message; - final Uri packageConfigURI; - final Completer completer = new Completer(); + final Uri? packageConfigURI; + final Completer completer = Completer(); Stream get events => _eventListener.stream; Stream get console => _logListener.stream; - final StreamController _logListener = new StreamController(); - final StreamController _eventListener = new StreamController(); + final StreamController _logListener = StreamController(); + final StreamController _eventListener = StreamController(); Future execute() async { - if (packageConfigURI != null && !(new File.fromUri(packageConfigURI).existsSync())) { - throw new StateError("Package file '$packageConfigURI' not found. Run 'pub get' and retry."); + if (packageConfigURI != null && + !File.fromUri(packageConfigURI!).existsSync()) { + throw StateError( + "Package file '$packageConfigURI' not found. Run 'pub get' and retry.", + ); } final scriptSource = Uri.encodeComponent(await generator.scriptSource); - var onErrorPort = new ReceivePort() + final onErrorPort = ReceivePort() ..listen((err) async { if (err is List) { - final stack = new StackTrace.fromString(err.last.replaceAll(scriptSource, "")); + final stack = + StackTrace.fromString(err.last.replaceAll(scriptSource, "")); - completer.completeError(new StateError(err.first), stack); + completer.completeError(StateError(err.first), stack); } else { completer.completeError(err); } }); - var controlPort = new ReceivePort() + final controlPort = ReceivePort() ..listen((results) { if (results is Map && results.length == 1) { if (results.containsKey("_result")) { @@ -55,13 +63,25 @@ class IsolateExecutor { try { message["_sendPort"] = controlPort.sendPort; - final dataUri = Uri.parse("data:application/dart;charset=utf-8,${scriptSource}"); + final dataUri = Uri.parse( + "data:application/dart;charset=utf-8,$scriptSource", + ); if (packageConfigURI != null) { - await Isolate.spawnUri(dataUri, [], message, - errorsAreFatal: true, onError: onErrorPort.sendPort, packageConfig: packageConfigURI); + await Isolate.spawnUri( + dataUri, + [], + message, + onError: onErrorPort.sendPort, + packageConfig: packageConfigURI, + ); } else { - await Isolate.spawnUri(dataUri, [], message, - errorsAreFatal: true, onError: onErrorPort.sendPort, automaticPackageResolution: true); + await Isolate.spawnUri( + dataUri, + [], + message, + onError: onErrorPort.sendPort, + automaticPackageResolution: true, + ); } return await completer.future; @@ -73,15 +93,27 @@ class IsolateExecutor { } } - static Future run(Executable executable, - {Uri packageConfigURI, - List imports, - String additionalContents, - void eventHandler(dynamic event), - void logHandler(String line), - List additionalTypes}) async { - final source = new SourceGenerator(executable.runtimeType, imports: imports, additionalContents: additionalContents, additionalTypes: additionalTypes); - var executor = new IsolateExecutor(source, packageConfigURI: packageConfigURI, message: executable.message); + static Future run( + Executable executable, { + List imports = const [], + Uri? packageConfigURI, + String? additionalContents, + List additionalTypes = const [], + void Function(dynamic event)? eventHandler, + void Function(String line)? logHandler, + }) async { + final source = SourceGenerator( + executable.runtimeType, + imports: imports, + additionalContents: additionalContents, + additionalTypes: additionalTypes, + ); + + final executor = IsolateExecutor( + source, + packageConfigURI: packageConfigURI, + message: executable.message, + ); if (eventHandler != null) { executor.events.listen(eventHandler); diff --git a/lib/src/source_generator.dart b/lib/src/source_generator.dart index 3793957..6fb9bcf 100644 --- a/lib/src/source_generator.dart +++ b/lib/src/source_generator.dart @@ -1,35 +1,45 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:mirrors'; -import 'dart:async'; -import 'package:analyzer/analyzer.dart'; -import 'package:isolate_executor/src/executable.dart'; +import 'package:analyzer/dart/analysis/analysis_context.dart'; +import 'package:analyzer/dart/analysis/context_builder.dart'; +import 'package:analyzer/dart/analysis/context_locator.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:conduit_isolate_exec/src/executable.dart'; class SourceGenerator { - SourceGenerator(this.executableType, {this.imports, this.additionalContents, this.additionalTypes}); + SourceGenerator( + this.executableType, { + this.imports = const [], + this.additionalTypes = const [], + this.additionalContents, + }); Type executableType; - String get typeName => MirrorSystem.getName(reflectType(executableType).simpleName); + String get typeName => + MirrorSystem.getName(reflectType(executableType).simpleName); final List imports; - final String additionalContents; + final String? additionalContents; final List additionalTypes; Future get scriptSource async { final typeSource = (await _getClass(executableType)).toSource(); - var builder = new StringBuffer(); + final builder = StringBuffer(); builder.writeln("import 'dart:async';"); builder.writeln("import 'dart:isolate';"); builder.writeln("import 'dart:mirrors';"); - imports?.forEach((import) { - builder.writeln("import '$import';"); - }); + for (final anImport in imports) { + builder.writeln("import '$anImport';"); + } builder.writeln(""" Future main (List args, Map message) async { final sendPort = message['_sendPort']; - final executable = new $typeName(message); + final executable = $typeName(message); final result = await executable.execute(); sendPort.send({"_result": result}); } @@ -37,7 +47,7 @@ Future main (List args, Map message) async { builder.writeln(typeSource); builder.writeln((await _getClass(Executable)).toSource()); - for (var type in additionalTypes ?? []) { + for (final type in additionalTypes) { final source = await _getClass(type); builder.writeln(source.toSource()); } @@ -50,13 +60,32 @@ Future main (List args, Map message) async { } static Future _getClass(Type type) async { - final uri = await Isolate.resolvePackageUri(reflectClass(type).location.sourceUri); - final fileUnit = parseDartFile(uri.toFilePath(windows: Platform.isWindows)); + final uri = + await Isolate.resolvePackageUri(reflectClass(type).location!.sourceUri); + final path = uri!.toFilePath(windows: Platform.isWindows); + + final context = _createContext(path); + final session = context.currentSession; + final unit = session.getParsedUnit(path); final typeName = MirrorSystem.getName(reflectClass(type).simpleName); - return fileUnit.declarations - .where((u) => u is ClassDeclaration) - .map((cu) => cu as ClassDeclaration) + return unit.unit.declarations + .whereType() .firstWhere((classDecl) => classDecl.name.name == typeName); } } + +AnalysisContext _createContext( + String path, { + ResourceProvider? resourceProvider, +}) { + resourceProvider ??= PhysicalResourceProvider.INSTANCE; + final builder = ContextBuilder(resourceProvider: resourceProvider); + final contextLocator = ContextLocator( + resourceProvider: resourceProvider, + ); + final root = contextLocator.locateRoots( + includedPaths: [path], + ); + return builder.createContext(contextRoot: root.first); +} diff --git a/pubspec.yaml b/pubspec.yaml index a963ba2..5afcbb3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,18 @@ -name: isolate_executor +name: conduit_isolate_exec description: This library contains types that allow for executing code in a spawned isolate, perhaps with additional imports. -version: 2.0.2+3 -homepage: https://github.com/stablekernel/dart-isolate-executor +version: 1.0.0-b1 +homepage: https://github.com/conduit-dart/isolate-exec author: stable|kernel environment: - sdk: ">=2.0.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" dependencies: - analyzer: '>=0.32.0 <0.50.0' + analyzer: ^1.3.0 + glob: ^2.0.0 dev_dependencies: - test: ^1.3.0 + lint: ^1.5.3 + test: ^1.6.5 test_package: path: test/test_package diff --git a/test/isolate_executor_test.dart b/test/isolate_executor_test.dart index 6284641..a094b51 100644 --- a/test/isolate_executor_test.dart +++ b/test/isolate_executor_test.dart @@ -1,47 +1,62 @@ import 'dart:async'; import 'dart:io'; -import 'package:isolate_executor/isolate_executor.dart'; +import 'package:conduit_isolate_exec/isolate_executor.dart'; import 'package:test/test.dart'; void main() { - final projDir = Directory.current.uri.resolve("test/").resolve("test_package/"); + final projDir = + Directory.current.uri.resolve("test/").resolve("test_package/"); setUpAll(() async { await getDependencies(projDir); }); test("Can run an Executable and get its return value", () async { - final result = - await IsolateExecutor.run(SimpleReturner({}), packageConfigURI: projDir.resolve(".packages")); + final result = await IsolateExecutor.run(SimpleReturner({}), + packageConfigURI: projDir.resolve(".packages")); expect(result, 1); }); test("Logged messages are available through logger stream", () async { final msgs = []; await IsolateExecutor.run(SimpleReturner({}), - logHandler: (msg) => msgs.add(msg), packageConfigURI: projDir.resolve(".packages")); + logHandler: (msg) => msgs.add(msg), + packageConfigURI: projDir.resolve(".packages")); expect(msgs, ["hello"]); }); test("Send values to Executable and use them", () async { - final result = await IsolateExecutor.run(Echo({'echo': 'hello'}), packageConfigURI: projDir.resolve(".packages")); + final result = await IsolateExecutor.run( + Echo({'echo': 'hello'}), + packageConfigURI: projDir.resolve(".packages"), + ); expect(result, 'hello'); }); test("Run from another package", () async { final result = await IsolateExecutor.run(InPackage({}), - packageConfigURI: projDir.resolve(".packages"), imports: ["package:test_package/lib.dart"]); - - expect(result, {"def": "default", "pos": "positionalArgs", "nam": "namedArgs", "con": "fromID"}); + packageConfigURI: projDir.resolve(".packages"), + imports: ["package:test_package/lib.dart"]); + + expect(result, { + "def": "default", + "pos": "positionalArgs", + "nam": "namedArgs", + "con": "fromID" + }); }); test("Can get messages thru stream", () async { - var completers = [new Completer(), new Completer(), new Completer()]; - var futures = [completers[0].future, completers[1].future, completers[2].future]; - - final result = await IsolateExecutor.run(Streamer({}), packageConfigURI: projDir.resolve(".packages"), - eventHandler: (event) { + final completers = [Completer(), Completer(), Completer()]; + final futures = [ + completers[0].future, + completers[1].future, + completers[2].future + ]; + + final result = await IsolateExecutor.run(Streamer({}), + packageConfigURI: projDir.resolve(".packages"), eventHandler: (event) { completers.last.complete(event); completers.removeLast(); }); @@ -50,7 +65,10 @@ void main() { final completed = await Future.wait(futures); expect(completed.any((i) => i == 1), true); expect(completed.any((i) => i is Map && i["key"] == "value"), true); - expect(completed.any((i) => i is Map && i["key1"] == "value1" && i["key2"] == "value2"), true); + expect( + completed.any( + (i) => i is Map && i["key1"] == "value1" && i["key2"] == "value2"), + true); }); test("Can instantiate types including in additionalContents", () async { @@ -62,10 +80,15 @@ class AdditionalContents { int get id => 10; } expect(result, 10); }); - test("If error is thrown, it is made available to consumer and the stack trace has been trimmed of script source", () async { + test( + "If error is thrown, it is made available to consumer and the stack trace has been trimmed of script source", + () async { try { - await IsolateExecutor.run(Thrower({}), packageConfigURI: projDir.resolve(".packages")); + await IsolateExecutor.run(Thrower({}), + packageConfigURI: projDir.resolve(".packages")); fail('unreachable'); + + //ignore: avoid_catching_errors } on StateError catch (e, st) { expect(e.toString(), contains("thrower-error")); expect(st.toString().contains("import"), false); @@ -85,7 +108,7 @@ class SimpleReturner extends Executable { class Echo extends Executable { Echo(Map message) - : echoMessage = message['echo'], + : echoMessage = message['echo']!.toString(), super(message); final String echoMessage; @@ -105,10 +128,22 @@ class InPackage extends Executable> { @override Future> execute() async { - SomeObjectBaseClass def = instanceOf("DefaultObject"); - SomeObjectBaseClass pos = instanceOf("PositionalArgumentsObject", positionalArguments: ["positionalArgs"]); - SomeObjectBaseClass nam = instanceOf("NamedArgumentsObject", namedArguments: {#id: "namedArgs"}); - SomeObjectBaseClass con = instanceOf("NamedConstructorObject", constructorName: #fromID); + final SomeObjectBaseClass def = instanceOf( + "DefaultObject", + namedArguments: {}, + ); + final SomeObjectBaseClass pos = instanceOf( + "PositionalArgumentsObject", + positionalArguments: ["positionalArgs"], + namedArguments: {}, + ); + final SomeObjectBaseClass nam = instanceOf( + "NamedArgumentsObject", + namedArguments: {#id: "namedArgs"}, + ); + final SomeObjectBaseClass con = + instanceOf("NamedConstructorObject", constructorName: #fromID) + as SomeObjectBaseClass; return {"def": def.id, "pos": pos.id, "nam": nam.id, "con": con.id}; } } @@ -146,5 +181,6 @@ class AdditionalContentsInstantiator extends Executable { Future getDependencies(Uri projectDir) async { final cmd = Platform.isWindows ? "pub.bat" : "pub"; - return Process.run(cmd, ["get"], workingDirectory: projectDir.toFilePath(windows: Platform.isWindows)); + return Process.run(cmd, ["get"], + workingDirectory: projectDir.toFilePath(windows: Platform.isWindows)); } diff --git a/test/test_package/lib/lib.dart b/test/test_package/lib/lib.dart index 79d2007..9c3b55b 100644 --- a/test/test_package/lib/lib.dart +++ b/test/test_package/lib/lib.dart @@ -4,21 +4,27 @@ export 'package:test_package/src/src.dart'; String libFunction() => "libFunction"; class DefaultObject implements SomeObjectBaseClass { + @override String get id => "default"; } class PositionalArgumentsObject implements SomeObjectBaseClass { PositionalArgumentsObject(this.id); + + @override String id; } class NamedArgumentsObject implements SomeObjectBaseClass { - NamedArgumentsObject({this.id}); + NamedArgumentsObject({this.id = ''}); + @override String id; } class NamedConstructorObject implements SomeObjectBaseClass { NamedConstructorObject.fromID(); + + @override String get id => "fromID"; -} \ No newline at end of file +} diff --git a/test/test_package/pubspec.yaml b/test/test_package/pubspec.yaml index 252f232..7faa01e 100644 --- a/test/test_package/pubspec.yaml +++ b/test/test_package/pubspec.yaml @@ -3,4 +3,4 @@ description: test dependency version: 1.0.0 environment: - sdk: '>=2.0.0 <3.0.0' + sdk: '>=2.12.0-0 <3.0.0'