Backport spawnHybridUri fix for NNBD
Backport of https://github.com/dart-lang/test/pull/1369, fixes https://github.com/dart-lang/test/issues/1372
After merging I will create a PR from the `pre-nnbd` branch into master to update the changelog.
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index dab1ac9..a16e171 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.15.5 (Backport)
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+
## 1.15.4
* Allow analyzer 0.40.x.
diff --git a/pkgs/test/lib/src/runner/browser/platform.dart b/pkgs/test/lib/src/runner/browser/platform.dart
index 32162bc..cd8347c 100644
--- a/pkgs/test/lib/src/runner/browser/platform.dart
+++ b/pkgs/test/lib/src/runner/browser/platform.dart
@@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
-import 'dart:isolate';
import 'package:async/async.dart';
import 'package:http_multi_server/http_multi_server.dart';
@@ -29,6 +28,7 @@
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:yaml/yaml.dart';
@@ -48,12 +48,13 @@
/// the working directory.
static Future<BrowserPlatform> start({String root}) async {
var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
+ var packageConfig = await currentPackageConfig;
return BrowserPlatform._(
server,
Configuration.current,
- p.fromUri(await Isolate.resolvePackageUri(
+ p.fromUri(packageConfig.resolve(
Uri.parse('package:test/src/runner/browser/static/favicon.ico'))),
- p.fromUri(await Isolate.resolvePackageUri(Uri.parse(
+ p.fromUri(packageConfig.resolve(Uri.parse(
'package:test/src/runner/browser/static/default.html.tpl'))),
root: root);
}
diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart
index a9601ed..671e7ad 100644
--- a/pkgs/test/lib/src/runner/node/platform.dart
+++ b/pkgs/test/lib/src/runner/node/platform.dart
@@ -21,6 +21,7 @@
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/compiler_pool.dart'; // ignore: implementation_imports
diff --git a/pkgs/test/lib/src/util/package_map.dart b/pkgs/test/lib/src/util/package_map.dart
index 948b6a6..6b30afa 100644
--- a/pkgs/test/lib/src/util/package_map.dart
+++ b/pkgs/test/lib/src/util/package_map.dart
@@ -2,15 +2,8 @@
// 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:isolate';
-
import 'package:package_config/package_config.dart';
-/// The [PackageConfig] parsed from the current isolates package config file.
-final Future<PackageConfig> currentPackageConfig = () async {
- return loadPackageConfigUri(await Isolate.packageConfig);
-}();
-
/// Adds methods to convert to a package map on [PackageConfig].
extension PackageMap on PackageConfig {
/// A package map exactly matching the current package config
diff --git a/pkgs/test/pubspec.yaml b/pkgs/test/pubspec.yaml
index ebacee2..e66201a 100644
--- a/pkgs/test/pubspec.yaml
+++ b/pkgs/test/pubspec.yaml
@@ -1,5 +1,5 @@
name: test
-version: 1.15.4
+version: 1.15.5
description: A full featured library for writing and running Dart tests.
homepage: https://github.com/dart-lang/test/blob/master/pkgs/test
@@ -32,8 +32,8 @@
webkit_inspection_protocol: ">=0.5.0 <0.8.0"
yaml: ^2.0.0
# Use an exact version until the test_api and test_core package are stable.
- test_api: 0.2.18
- test_core: 0.3.11+1
+ test_api: 0.2.18+1
+ test_core: 0.3.11+2
dev_dependencies:
fake_async: ^1.0.0
diff --git a/pkgs/test/test/runner/hybrid_test.dart b/pkgs/test/test/runner/hybrid_test.dart
index 7c1b0dd..827611a 100644
--- a/pkgs/test/test/runner/hybrid_test.dart
+++ b/pkgs/test/test/runner/hybrid_test.dart
@@ -7,6 +7,7 @@
import 'dart:io';
import 'dart:isolate';
+import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
import 'package:test_descriptor/test_descriptor.dart' as d;
@@ -394,6 +395,33 @@
]));
await test.shouldExit(0);
});
+
+ test('can opt out of null safety', () async {
+ expect(spawnHybridCode('''
+ // @dart=2.9
+ import "package:stream_channel/stream_channel.dart";
+
+ // Would cause an error in null safety mode.
+ int x;
+
+ void hybridMain(StreamChannel channel) {
+ channel.sink..add(1)..add(2)..add(3)..close();
+ }
+ ''').stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test('opts in to null safety by default', () async {
+ expect(spawnHybridCode('''
+ import "package:stream_channel/stream_channel.dart";
+
+ // Use some null safety syntax
+ int? x;
+
+ void hybridMain(StreamChannel channel) {
+ channel.sink..add(1)..add(2)..add(3)..close();
+ }
+ ''').stream.toList(), completion(equals([1, 2, 3])));
+ });
});
}
@@ -584,4 +612,107 @@
expect(spawnHybridUri('non existent file').stream.first,
throwsA(TypeMatcher<Exception>()));
});
+
+ test('can opt out of nnbd via language version comments', () async {
+ await d.file('test.dart', '''
+ import "package:test/test.dart";
+
+ void main() {
+ test("hybrid emits numbers", () {
+ expect(spawnHybridUri("hybrid.dart").stream.toList(),
+ completion(equals([1, 2, 3])));
+ });
+ }
+ ''').create();
+
+ await d.file('hybrid.dart', '''
+ // @dart=2.9
+ import "package:stream_channel/stream_channel.dart";
+
+ // Would fail if null safety were enabled.
+ int x;
+
+ void hybridMain(StreamChannel channel) {
+ channel.sink..add(1)..add(2)..add(3)..close();
+ }
+ ''').create();
+
+ var test = await runTest(['test.dart', ...arguments]);
+ expect(test.stdout,
+ containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
+ await test.shouldExit(0);
+ });
+
+ test('can opt in to nnbd via language version comments', () async {
+ await d.file('test.dart', '''
+ import "package:test/test.dart";
+
+ void main() {
+ test("hybrid emits numbers", () {
+ expect(spawnHybridUri("hybrid.dart").stream.toList(),
+ completion(equals([1, 2, 3])));
+ });
+ }
+ ''').create();
+
+ await d.file('hybrid.dart', '''
+ // @dart=2.12
+ import "package:stream_channel/stream_channel.dart";
+
+ // Use some null safety syntax to confirm we are opted in.
+ int? x;
+
+ void hybridMain(StreamChannel channel) {
+ channel.sink..add(1)..add(2)..add(3)..close();
+ }
+ ''').create();
+
+ var test = await runTest(['test.dart', ...arguments]);
+ expect(test.stdout,
+ containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
+ await test.shouldExit(0);
+ });
+
+ test('the default language version is used for hybrid code', () async {
+ await d.file('test.dart', '''
+ import "package:test/test.dart";
+
+ void main() {
+ test("hybrid emits numbers", () {
+ expect(spawnHybridUri("hybrid.dart").stream.toList(),
+ completion(equals([1, 2, 3])));
+ });
+ }
+ ''').create();
+
+ await d.file('hybrid.dart', '''
+ import "package:stream_channel/stream_channel.dart";
+
+ // Would fail if null safety were enabled.
+ int x;
+
+ void hybridMain(StreamChannel channel) {
+ channel.sink..add(1)..add(2)..add(3)..close();
+ }
+ ''').create();
+
+ // Adds the sandbox dir as a new package to the existing config,
+ // opting it out.
+ var originalPackageConfig =
+ await loadPackageConfigUri(await Isolate.packageConfig);
+ var extraPackage = Package('_test', Uri.file('${d.sandbox}/'),
+ languageVersion: LanguageVersion(2, 9));
+ var newConfig = PackageConfig([
+ ...originalPackageConfig.packages,
+ extraPackage,
+ ], extraData: originalPackageConfig.extraData);
+ await d.dir('.dart_tool').create();
+ await savePackageConfig(newConfig, Directory(d.sandbox));
+
+ var test = await runTest(['test.dart', ...arguments],
+ packageConfig: p.join(d.sandbox, '.dart_tool', 'package_config.json'));
+ expect(test.stdout,
+ containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
+ await test.shouldExit(0);
+ });
}
diff --git a/pkgs/test_api/CHANGELOG.md b/pkgs/test_api/CHANGELOG.md
index eb65ce8..232a62a 100644
--- a/pkgs/test_api/CHANGELOG.md
+++ b/pkgs/test_api/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.18+1 (Backport)
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+
## 0.2.18
* Update to `matcher` version `0.12.9`.
diff --git a/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart b/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart
index 1b0cde2..cab7ff2 100644
--- a/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart
+++ b/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart
@@ -6,11 +6,9 @@
import 'dart:convert';
import 'package:async/async.dart';
-import 'package:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart';
import '../../test_api.dart';
-import '../backend/invoker.dart';
import '../util/remote_exception.dart';
import '../utils.dart';
@@ -92,52 +90,13 @@
/// **Note**: If you use this API, be sure to add a dependency on the
/// **`stream_channel` package, since you're using its API as well!
StreamChannel spawnHybridUri(uri, {Object message, bool stayAlive = false}) {
- Uri parsedUrl;
- if (uri is Uri) {
- parsedUrl = uri;
- } else if (uri is String) {
- parsedUrl = Uri.parse(uri);
- } else {
+ if (uri is String) {
+ // Ensure that it can be parsed as a uri.
+ Uri.parse(uri);
+ } else if (uri is! Uri) {
throw ArgumentError.value(uri, 'uri', 'must be a Uri or a String.');
}
-
- String absoluteUri;
- if (parsedUrl.scheme.isEmpty) {
- var isRootRelative = parsedUrl.path.startsWith('/');
-
- // If we're running in a browser context, the working directory is already
- // relative to the test file, whereas on the VM the working directory is the
- // root of the package.
- if (p.style == p.Style.url) {
- if (isRootRelative) {
- // A root-relative URL is interpreted as relative to the package root,
- // which means placing it beneath the URL secret.
- var secret = Uri.encodeComponent(Uri.base.pathSegments[0]);
- absoluteUri = p.absolute('/$secret$parsedUrl');
- print('Uri.base: ${Uri.base}');
- print('absoluteUri: ${absoluteUri}');
- } else {
- absoluteUri = p.absolute(parsedUrl.toString());
- }
- } else {
- if (isRootRelative) {
- // We assume that the current path is the package root. `pub run`
- // enforces this currently, but at some point it would probably be good
- // to pass in an explicit root.
- absoluteUri = p.url
- .join(p.toUri(p.current).toString(), parsedUrl.path.substring(1));
- } else {
- var suitePath = Invoker.current.liveTest.suite.path;
- absoluteUri = p.url.join(
- p.url.dirname(p.toUri(p.absolute(suitePath)).toString()),
- parsedUrl.toString());
- }
- }
- } else {
- absoluteUri = uri.toString();
- }
-
- return _spawn(absoluteUri, message, stayAlive: stayAlive);
+ return _spawn(uri.toString(), message, stayAlive: stayAlive);
}
/// Spawns a VM isolate that runs the given [dartCode], which is loaded as the
diff --git a/pkgs/test_api/pubspec.yaml b/pkgs/test_api/pubspec.yaml
index 40d4171..186a3a2 100644
--- a/pkgs/test_api/pubspec.yaml
+++ b/pkgs/test_api/pubspec.yaml
@@ -1,5 +1,5 @@
name: test_api
-version: 0.2.18
+version: 0.2.18+1
description: A library for writing Dart tests.
homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_api
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index 8c3b03f..7e27b7d 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.11+2 (Backport)
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+
## 0.3.11+1
* Allow analyzer 0.40.x.
diff --git a/pkgs/test_core/lib/src/runner/runner_test.dart b/pkgs/test_core/lib/src/runner/runner_test.dart
index 0dc6229..d910cac 100644
--- a/pkgs/test_core/lib/src/runner/runner_test.dart
+++ b/pkgs/test_core/lib/src/runner/runner_test.dart
@@ -71,7 +71,7 @@
// When we kill the isolate that the test lives in, that will close
// this virtual channel and cause the spawned isolate to close as
// well.
- spawnHybridUri(message['url'] as String, message['message'])
+ spawnHybridUri(message['url'] as String, message['message'], suite)
.pipe(testChannel.virtualChannel(message['channel'] as int));
break;
}
diff --git a/pkgs/test_core/lib/src/runner/spawn_hybrid.dart b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
index 1db6769..76e448a 100644
--- a/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
+++ b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
@@ -3,14 +3,19 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'dart:io';
import 'dart:isolate';
+import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
import 'package:stream_channel/isolate_channel.dart';
import 'package:stream_channel/stream_channel.dart';
import '../util/dart.dart' as dart;
+import '../util/package_config.dart';
+import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
import 'package:test_api/src/util/remote_exception.dart'; // ignore: implementation_imports
/// Spawns a hybrid isolate from [url] with the given [message], and returns a
@@ -19,12 +24,23 @@
/// This connects the main isolate to the hybrid isolate, whereas
/// `lib/src/frontend/spawn_hybrid.dart` connects the test isolate to the main
/// isolate.
-StreamChannel spawnHybridUri(String url, Object message) {
+///
+/// If [uri] is relative, it will be interpreted relative to the `file:` URL
+/// for [suite]. If it's root-relative (that is, if it begins with `/`) it will
+/// be interpreted relative to the root of the package (the directory that
+/// contains `pubspec.yaml`, *not* the `test/` directory). If it's a `package:`
+/// URL, it will be resolved using the current package's dependency
+/// constellation.
+StreamChannel /*!*/ spawnHybridUri(
+ String url, Object /*?*/ message, Suite suite) {
+ url = _normalizeUrl(url, suite);
return StreamChannelCompleter.fromFuture(() async {
var port = ReceivePort();
var onExitPort = ReceivePort();
try {
var code = '''
+ ${await _languageVersionCommentFor(url)}
+
import "package:test_core/src/runner/hybrid_listener.dart";
import "${url.replaceAll(r'$', '%24')}" as lib;
@@ -64,3 +80,75 @@
}
}());
}
+
+/// Normalizes [url] to an absolute url, or returns it as is if it has a
+/// scheme.
+///
+/// Follows the rules for relatives/absolute paths outlit
+String _normalizeUrl(String url, Suite suite) {
+ final parsedUri = Uri.parse(url);
+
+ if (parsedUri.scheme.isEmpty) {
+ var isRootRelative = parsedUri.path.startsWith('/');
+
+ if (isRootRelative) {
+ // We assume that the current path is the package root. `pub run`
+ // enforces this currently, but at some point it would probably be good
+ // to pass in an explicit root.
+ return p.url
+ .join(p.toUri(p.current).toString(), parsedUri.path.substring(1));
+ } else {
+ var suitePath = suite.path;
+ return p.url.join(
+ p.url.dirname(p.toUri(p.absolute(suitePath)).toString()),
+ parsedUri.toString());
+ }
+ } else {
+ return url;
+ }
+}
+
+/// Computes the a language version comment for the library at [uri].
+///
+/// If there is a language version comment in the file, that is returned.
+///
+/// Otherwise a comment representing the default version from the
+/// [currentPackageConfig] is returned.
+///
+/// If no default language version is known (data: uri for instance), then
+/// an empty string is returned.
+Future<String> _languageVersionCommentFor(String url) async {
+ var parsedUri = Uri.parse(url);
+
+ // Returns the explicit language version comment if one exists.
+ var result = parseString(
+ content: await _readUri(parsedUri),
+ path: parsedUri.path,
+ throwIfDiagnostics: false);
+ var languageVersionComment = result.unit.languageVersionToken?.value();
+ if (languageVersionComment != null) return languageVersionComment.toString();
+
+ // Returns the default language version for the package if one exists.
+ if (parsedUri.scheme.isEmpty || parsedUri.scheme == 'file') {
+ var packageConfig = await currentPackageConfig;
+ var package = packageConfig.packageOf(parsedUri);
+ var version = package?.languageVersion;
+ if (version != null) return '// @dart=${version}';
+ }
+
+ // Fall back on no language comment.
+ return '';
+}
+
+Future<String> _readUri(Uri uri) async {
+ switch (uri.scheme) {
+ case '':
+ case 'file':
+ return File.fromUri(uri).readAsString();
+ case 'data':
+ return uri.data.contentAsString();
+ default:
+ throw ArgumentError.value(uri, 'uri',
+ 'Only data and file uris (as well as relative paths) are supported');
+ }
+}
diff --git a/pkgs/test_core/lib/src/util/package_config.dart b/pkgs/test_core/lib/src/util/package_config.dart
new file mode 100644
index 0000000..b3743f8
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/package_config.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2020, 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.
+//
+// @dart=2.7
+
+import 'dart:isolate';
+
+import 'package:package_config/package_config.dart';
+
+/// The [PackageConfig] parsed from the current isolates package config file.
+final Future<PackageConfig> currentPackageConfig = () async {
+ return loadPackageConfigUri(await Isolate.packageConfig);
+}();
diff --git a/pkgs/test_core/pubspec.yaml b/pkgs/test_core/pubspec.yaml
index 1375468..7e26f8c 100644
--- a/pkgs/test_core/pubspec.yaml
+++ b/pkgs/test_core/pubspec.yaml
@@ -1,5 +1,5 @@
name: test_core
-version: 0.3.11+1
+version: 0.3.11+2
description: A basic library for writing tests and running them on the VM.
homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_core
@@ -30,7 +30,7 @@
# matcher is tightly constrained by test_api
matcher: any
# Use an exact version until the test_api package is stable.
- test_api: 0.2.18
+ test_api: 0.2.18+1
dependency_overrides:
test_api: