Finish override logic (#4780)
* Make applicationConfigHome conditional, avoid it when dart:io is unavailable
* Added override logic
* Better override logic
* Test that we can run pub inmemory
* Expose override IO logic
* Enable overriding and add tests
* Ignore harmless .tar.gz padding
* Fix golden stack trace
* Nit
diff --git a/lib/pub.dart b/lib/pub.dart
index 12f364c..321b905 100644
--- a/lib/pub.dart
+++ b/lib/pub.dart
@@ -2,11 +2,16 @@
// 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' show StreamSink;
+
import 'package:args/command_runner.dart';
+import 'package:file/file.dart' as f;
+import 'package:http/http.dart' as http;
import 'src/entrypoint.dart';
import 'src/exceptions.dart';
import 'src/http.dart';
+import 'src/io.dart';
import 'src/pub_embeddable_command.dart';
import 'src/source/git.dart';
import 'src/system_cache.dart';
@@ -26,7 +31,33 @@
Command<int> pubCommand({
required bool Function() isVerbose,
String category = '',
-}) => PubEmbeddableCommand(isVerbose, category);
+ f.FileSystem? fileSystem,
+ Map<String, String>? environment,
+ String? platformVersion,
+ Stream<List<int>>? stdin,
+ StreamSink<List<int>>? stdout,
+ StreamSink<List<int>>? stderr,
+ http.Client? httpClient,
+}) => withOverrides(
+ () => PubEmbeddableCommand(
+ isVerbose,
+ category,
+ fileSystem: fileSystem,
+ environment: environment,
+ platformVersion: platformVersion,
+ stdin: stdin,
+ stdout: stdout,
+ stderr: stderr,
+ httpClient: httpClient,
+ ),
+ fileSystem: fileSystem,
+ environment: environment,
+ platformVersion: platformVersion,
+ stdin: stdin,
+ stdout: stdout,
+ stderr: stderr,
+ httpClient: httpClient,
+);
/// Makes sure that [dir]/pubspec.yaml is resolved such that pubspec.lock and
/// .dart_tool/package_config.json are up-to-date and all packages are
@@ -46,22 +77,40 @@
bool isOffline = false,
bool summaryOnly = true,
bool onlyOutputWhenTerminal = true,
+ f.FileSystem? fileSystem,
+ Map<String, String>? environment,
+ String? platformVersion,
+ Stream<List<int>>? stdin,
+ StreamSink<List<int>>? stdout,
+ StreamSink<List<int>>? stderr,
+ http.Client? httpClient,
}) async {
- try {
- await Entrypoint.ensureUpToDate(
- dir,
- cache: SystemCache(isOffline: isOffline),
- summaryOnly: summaryOnly,
- onlyOutputWhenTerminal: onlyOutputWhenTerminal,
- );
- } on ApplicationException catch (e) {
- throw ResolutionFailedException._(e.toString());
- } finally {
- // TODO(https://github.com/dart-lang/pub/issues/4200)
- // This is a bit of a hack.
- // We should most likely take a client here.
- globalHttpClient.close();
- }
+ return await withOverrides(
+ () async {
+ try {
+ await Entrypoint.ensureUpToDate(
+ dir,
+ cache: SystemCache(isOffline: isOffline),
+ summaryOnly: summaryOnly,
+ onlyOutputWhenTerminal: onlyOutputWhenTerminal,
+ );
+ } on ApplicationException catch (e) {
+ throw ResolutionFailedException._(e.toString());
+ } finally {
+ // TODO(https://github.com/dart-lang/pub/issues/4200)
+ // This is a bit of a hack.
+ // We should most likely take a client here.
+ globalHttpClient.close();
+ }
+ },
+ fileSystem: fileSystem,
+ environment: environment,
+ platformVersion: platformVersion,
+ stdin: stdin,
+ stdout: stdout,
+ stderr: stderr,
+ httpClient: httpClient,
+ );
}
class ResolutionFailedException implements Exception {
diff --git a/lib/src/authentication/client.dart b/lib/src/authentication/client.dart
index 1a8823b..40e7cd9 100644
--- a/lib/src/authentication/client.dart
+++ b/lib/src/authentication/client.dart
@@ -26,7 +26,7 @@
/// not be injected to requests.
_AuthenticatedClient(this._inner, this._credential);
- final http.BaseClient _inner;
+ final http.Client _inner;
/// Authentication scheme that could be used for authenticating requests.
final Credential? _credential;
diff --git a/lib/src/command.dart b/lib/src/command.dart
index d1a9d0b..8932723 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -18,6 +18,7 @@
import 'git.dart' as git;
import 'global_packages.dart';
import 'http.dart';
+import 'io.dart';
import 'log.dart' as log;
import 'path.dart';
import 'platform_info.dart';
@@ -186,7 +187,20 @@
@override
@nonVirtual
- FutureOr<int> run() async {
+ Future<int> run() async {
+ return await withOverrides(
+ _run,
+ fileSystem: _pubEmbeddableCommand?.fileSystem,
+ environment: _pubEmbeddableCommand?.environment,
+ platformVersion: _pubEmbeddableCommand?.platformVersion,
+ stdin: _pubEmbeddableCommand?.stdin,
+ stdout: _pubEmbeddableCommand?.stdout,
+ stderr: _pubEmbeddableCommand?.stderr,
+ httpClient: _pubEmbeddableCommand?.httpClient,
+ );
+ }
+
+ Future<int> _run() async {
_computeCommand(_pubTopLevel.argResults);
_decideOnColors(_pubTopLevel.argResults);
diff --git a/lib/src/gzip/gzip.dart b/lib/src/gzip/gzip.dart
index 74f2b9b..6b1bc91 100644
--- a/lib/src/gzip/gzip.dart
+++ b/lib/src/gzip/gzip.dart
@@ -6,7 +6,7 @@
import 'gzip_stub.dart'
if (dart.library.io) 'gzip_io.dart'
- if (dart.library.js_util) 'gzip_js.dart'
+ if (dart.library.js_interop) 'gzip_js.dart'
as impl;
/// A [Converter] that decompresses gzip-compressed data.
diff --git a/lib/src/gzip/gzip_js.dart b/lib/src/gzip/gzip_js.dart
index 1a3cb46..c00c01a 100644
--- a/lib/src/gzip/gzip_js.dart
+++ b/lib/src/gzip/gzip_js.dart
@@ -33,39 +33,60 @@
return controller.stream;
}
+ bool _isHarmlessPaddingError(Object e) {
+ // Some .tar.gz may contain harmless padding, which DecrompressionStream in
+ // the browsers are sensitive to. Example:
+ // https://pub.dev/api/archives/lints-1.0.1.tar.gz
+ final errorMessage = e.toString().toLowerCase();
+ // Match the specific EOF padding errors from V8, SpiderMonkey, and WebKit
+ return errorMessage.contains('junk found') ||
+ errorMessage.contains('unexpected input') ||
+ errorMessage.contains('extra bytes');
+ }
+
Future<void> _pipe(
Stream<List<int>> stream,
StreamController<List<int>> controller,
) async {
- try {
- final decompressionStream = web.DecompressionStream('gzip');
- final writer = decompressionStream.writable.getWriter();
- final reader =
- decompressionStream.readable.getReader()
- as web.ReadableStreamDefaultReader;
+ final decompressionStream = web.DecompressionStream('gzip');
+ final writer = decompressionStream.writable.getWriter();
+ final reader =
+ decompressionStream.readable.getReader()
+ as web.ReadableStreamDefaultReader;
- final readFuture = () async {
- try {
- while (true) {
- final result = await reader.read().toDart;
- if (result.done) break;
- final value = result.value as JSUint8Array;
- controller.add(value.toDart);
- }
- } finally {
- reader.releaseLock();
+ final readFuture = () async {
+ try {
+ while (true) {
+ final result = await reader.read().toDart;
+ if (result.done) break;
+ final value = result.value as JSUint8Array;
+ controller.add(value.toDart);
}
- }();
+ } catch (e, st) {
+ if (_isHarmlessPaddingError(e)) {
+ // Ignore trailing junk
+ return;
+ }
+ controller.addError(e, st);
+ } finally {
+ reader.releaseLock();
+ }
+ }();
+ try {
await for (final chunk in stream) {
final bytes = chunk is Uint8List ? chunk : Uint8List.fromList(chunk);
await writer.write(bytes.toJS).toDart;
}
await writer.close().toDart;
- await readFuture;
- await controller.close();
} catch (e, st) {
- controller.addError(e, st);
+ if (_isHarmlessPaddingError(e)) {
+ // Ignore trailing junk
+ } else {
+ controller.addError(e, st);
+ }
+ } finally {
+ await readFuture;
await controller.close();
}
}
diff --git a/lib/src/http.dart b/lib/src/http.dart
index a5b461a..e954836 100644
--- a/lib/src/http.dart
+++ b/lib/src/http.dart
@@ -34,14 +34,14 @@
class _PubHttpClient extends http.BaseClient {
final _requestStopwatches = <http.BaseRequest, Stopwatch>{};
- http.Client _inner;
+ final http.Client _inner;
/// We manually keep track of whether the client was closed,
/// indicating that no more networking should be done. (And thus we don't need
/// to retry failed requests).
bool _wasClosed = false;
- _PubHttpClient([http.Client? inner]) : _inner = inner ?? http.Client();
+ _PubHttpClient(this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
@@ -156,16 +156,22 @@
}
}
-/// The [_PubHttpClient] wrapped by [globalHttpClient].
-final _pubClient = _PubHttpClient();
+final _defaultGlobalHttpClient = _PubHttpClient(http.Client());
/// The HTTP client to use for all HTTP requests.
-final globalHttpClient = _pubClient;
+http.Client get globalHttpClient =>
+ Zone.current[_globalHttpClientKey] as http.Client? ??
+ _defaultGlobalHttpClient;
-/// The underlying HTTP client wrapped by [globalHttpClient].
-/// This enables the ability to use a mock client in tests.
-http.Client get innerHttpClient => _pubClient._inner;
-set innerHttpClient(http.Client client) => _pubClient._inner = client;
+/// The key for the [globalHttpClient] in the current [Zone].
+final _globalHttpClientKey = Object();
+
+/// Runs [callback] in a [Zone] where [globalHttpClient] wraps [client].
+R withHttpClient<R>(R Function() callback, {required http.Client client}) =>
+ runZoned(
+ callback,
+ zoneValues: {_globalHttpClientKey: _PubHttpClient(client)},
+ );
extension AttachHeaders on http.Request {
/// Adds headers required for pub.dev API requests.
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 13462be..367e350 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -8,6 +8,7 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
+import 'dart:io' as io;
import 'dart:io';
import 'dart:typed_data';
@@ -15,7 +16,10 @@
import 'package:cli_util/cli_util.dart'
show EnvironmentNotFoundException, applicationConfigHome;
import 'package:collection/collection.dart';
+import 'package:file/file.dart' as f;
+import 'package:file/local.dart' as f;
import 'package:http/http.dart' show ByteStream;
+import 'package:http/http.dart' as http;
import 'package:http_multi_server/http_multi_server.dart';
import 'package:meta/meta.dart';
import 'package:pool/pool.dart';
@@ -26,6 +30,7 @@
import 'exceptions.dart';
import 'exit_codes.dart' as exit_codes;
import 'gzip/gzip.dart';
+import 'http.dart';
import 'log.dart' as log;
import 'path.dart';
import 'platform_info.dart';
@@ -1392,3 +1397,251 @@
d = parent;
}
}
+
+/// Run [fn] in a zone with overrides.
+R withOverrides<R>(
+ R Function() fn, {
+ f.FileSystem? fileSystem,
+ Map<String, String>? environment,
+ String? platformVersion,
+ Stream<List<int>>? stdin,
+ StreamSink<List<int>>? stdout,
+ StreamSink<List<int>>? stderr,
+ http.Client? httpClient,
+}) {
+ // If there are no overrides we're done
+ if (fileSystem == null &&
+ environment == null &&
+ platformVersion == null &&
+ stdin == null &&
+ stdout == null &&
+ stderr == null &&
+ httpClient == null) {
+ return fn();
+ }
+
+ fileSystem ??= const f.LocalFileSystem();
+ environment ??= platform.environment;
+ platformVersion ??= platform.version;
+ stdin ??= io.stdin;
+ stdout ??= io.stdout;
+ stderr ??= io.stderr;
+ final client = httpClient ?? http.Client();
+
+ final pathContext = fileSystem.path;
+
+ return IOOverrides.runWithIOOverrides(
+ () {
+ return withPlatform(
+ () {
+ return withHttpClient(() {
+ return withPathContext(fn, pathContext: pathContext);
+ }, client: client);
+ },
+ platform: PlatformInfo.override(
+ environment: environment,
+ version: platformVersion,
+ pathSeparator: pathContext.separator,
+ ),
+ );
+ },
+ _IOOverrides(
+ fileSystem: fileSystem,
+ stdin: stdin is Stdin ? stdin : StdinStream(stdin),
+ stdout: stdout is Stdout ? stdout : StdoutSink(stdout),
+ stderr: stderr is Stdout ? stderr : StdoutSink(stderr),
+ ),
+ );
+}
+
+/// An [IOOverrides] that uses a [f.FileSystem] for all operations.
+final class _IOOverrides extends IOOverrides {
+ final f.FileSystem fileSystem;
+
+ @override
+ final Stdin stdin;
+
+ @override
+ final Stdout stdout;
+
+ @override
+ final Stdout stderr;
+
+ _IOOverrides({
+ required this.fileSystem,
+ required this.stdin,
+ required this.stdout,
+ required this.stderr,
+ });
+
+ @override
+ File createFile(String path) => fileSystem.file(path);
+
+ @override
+ Directory createDirectory(String path) => fileSystem.directory(path);
+
+ @override
+ Link createLink(String path) => fileSystem.link(path);
+
+ @override
+ Future<FileStat> stat(String path) => fileSystem.stat(path);
+
+ @override
+ FileStat statSync(String path) => fileSystem.statSync(path);
+
+ @override
+ Future<bool> fseIdentical(String path1, String path2) =>
+ fileSystem.identical(path1, path2);
+
+ @override
+ bool fseIdenticalSync(String path1, String path2) =>
+ fileSystem.identicalSync(path1, path2);
+
+ @override
+ Future<FileSystemEntityType> fseGetType(String path, bool followLinks) =>
+ fileSystem.type(path, followLinks: followLinks);
+
+ @override
+ FileSystemEntityType fseGetTypeSync(String path, bool followLinks) =>
+ fileSystem.typeSync(path, followLinks: followLinks);
+
+ @override
+ Directory getCurrentDirectory() => fileSystem.currentDirectory;
+
+ @override
+ void setCurrentDirectory(String path) {
+ fileSystem.currentDirectory = path;
+ }
+
+ @override
+ Directory getSystemTempDirectory() => fileSystem.systemTempDirectory;
+
+ @override
+ bool fsWatchIsSupported() => fileSystem.isWatchSupported;
+
+ @override
+ Stream<FileSystemEvent> fsWatch(String path, int events, bool recursive) =>
+ fileSystem.directory(path).watch(events: events, recursive: recursive);
+}
+
+/// Wrap a [Stream<List<int>>] as [Stdin].
+final class StdinStream extends StreamView<List<int>> implements Stdin {
+ StdinStream(super.stream);
+
+ @override
+ bool get echoMode => false;
+
+ @override
+ set echoMode(bool value) {}
+
+ @override
+ bool get echoNewlineMode => false;
+
+ @override
+ set echoNewlineMode(bool value) {}
+
+ @override
+ bool get lineMode => false;
+
+ @override
+ set lineMode(bool value) {}
+
+ @override
+ bool get hasTerminal => false;
+
+ @override
+ int readByteSync() => throw UnsupportedError('cannot read sync from stdin');
+
+ @override
+ String? readLineSync({
+ Encoding encoding = systemEncoding,
+ bool retainNewlines = false,
+ }) => throw UnsupportedError('cannot read sync from stdin');
+
+ @override
+ bool get supportsAnsiEscapes => false;
+}
+
+/// Wrap a [StreamSink<List<int>>] as [Stdout].
+final class StdoutSink implements Stdout {
+ final StreamSink<List<int>> _sink;
+
+ StdoutSink(this._sink);
+
+ @override
+ Encoding encoding = utf8;
+
+ @override
+ void add(List<int> data) {
+ _sink.add(data);
+ }
+
+ @override
+ void addError(Object error, [StackTrace? stackTrace]) {
+ _sink.addError(error, stackTrace);
+ }
+
+ @override
+ Future addStream(Stream<List<int>> stream) => _sink.addStream(stream);
+
+ @override
+ Future close() => _sink.close();
+
+ @override
+ Future get done => _sink.done;
+
+ @override
+ Future flush() async {}
+
+ @override
+ bool get hasTerminal => false;
+
+ @override
+ IOSink get nonBlocking => this;
+
+ @override
+ bool get supportsAnsiEscapes => false;
+
+ @override
+ int get terminalColumns =>
+ throw const StdoutException('no terminal attached');
+
+ @override
+ int get terminalLines => throw const StdoutException('no terminal attached');
+
+ @override
+ void write(Object? object) {
+ add(encoding.encode('$object'));
+ }
+
+ @override
+ void writeAll(Iterable objects, [String separator = '']) {
+ final iterator = objects.iterator;
+ if (!iterator.moveNext()) return;
+ if (separator.isEmpty) {
+ do {
+ write(iterator.current);
+ } while (iterator.moveNext());
+ } else {
+ write(iterator.current);
+ while (iterator.moveNext()) {
+ write(separator);
+ write(iterator.current);
+ }
+ }
+ }
+
+ @override
+ void writeCharCode(int charCode) {
+ write(String.fromCharCode(charCode));
+ }
+
+ @override
+ void writeln([Object? object = '']) {
+ write(object);
+ write(lineTerminator);
+ }
+
+ @override
+ String lineTerminator = '\n';
+}
diff --git a/lib/src/path.dart b/lib/src/path.dart
index f32135f..068297d 100644
--- a/lib/src/path.dart
+++ b/lib/src/path.dart
@@ -14,14 +14,10 @@
final _pathContextKey = Object();
/// Runs [callback] in a [Zone] where [p] is overridden by [pathContext].
-Future<T> withPathContext<T>(
- FutureOr<T> Function() callback, {
+R withPathContext<R>(
+ R Function() callback, {
required path.Context pathContext,
-}) {
- return runZoned(() async {
- return await callback();
- }, zoneValues: {_pathContextKey: pathContext});
-}
+}) => runZoned(callback, zoneValues: {_pathContextKey: pathContext});
extension PathContextExt on path.Context {
/// A default context for manipulating POSIX paths.
diff --git a/lib/src/platform_info.dart b/lib/src/platform_info.dart
index 0a75feb..d37cfad 100644
--- a/lib/src/platform_info.dart
+++ b/lib/src/platform_info.dart
@@ -8,43 +8,56 @@
/// A proxy for [Platform] from `dart:io` which can be overridden.
PlatformInfo get platform =>
Zone.current[_platformInfoKey] as PlatformInfo? ??
- PlatformInfo.nativePlatform();
+ PlatformInfo.defaultPlatform();
/// The key for the [platform] in the current [Zone].
final _platformInfoKey = Object();
/// Runs [callback] in a [Zone] where `platform` is overridden by [platform].
-Future<T> withPlatform<T>(
- FutureOr<T> Function() callback, {
- required PlatformInfo platform,
-}) {
- return runZoned(() async {
- return await callback();
- }, zoneValues: {_platformInfoKey: platform});
-}
+R withPlatform<R>(R Function() callback, {required PlatformInfo platform}) =>
+ runZoned(callback, zoneValues: {_platformInfoKey: platform});
abstract final class PlatformInfo {
const PlatformInfo._();
- factory PlatformInfo.nativePlatform() = _NativePlatformInfo;
+ factory PlatformInfo.defaultPlatform() =>
+ const bool.fromEnvironment('dart.library.io')
+ ? const _NativePlatformInfo()
+ : const _BrowserPlatformInfo();
factory PlatformInfo.override({
- required Map<String, String> environment,
- required String executable,
- required bool isAndroid,
- required bool isFuchsia,
- required bool isIOS,
- required bool isLinux,
- required bool isMacOS,
- required bool isWindows,
- required String lineTerminator,
- required String operatingSystem,
- required String pathSeparator,
- required String resolvedExecutable,
- required String version,
- required int numberOfProcessors,
- required Uri script,
- }) = _PlatformInfoOverride;
+ Map<String, String>? environment,
+ String? executable,
+ bool? isAndroid,
+ bool? isFuchsia,
+ bool? isIOS,
+ bool? isLinux,
+ bool? isMacOS,
+ bool? isWindows,
+ String? lineTerminator,
+ String? operatingSystem,
+ String? pathSeparator,
+ String? resolvedExecutable,
+ String? version,
+ int? numberOfProcessors,
+ Uri? script,
+ }) => _PlatformInfoOverride(
+ environment: environment ?? platform.environment,
+ executable: executable ?? platform.executable,
+ isAndroid: isAndroid ?? platform.isAndroid,
+ isFuchsia: isFuchsia ?? platform.isFuchsia,
+ isIOS: isIOS ?? platform.isIOS,
+ isLinux: isLinux ?? platform.isLinux,
+ isMacOS: isMacOS ?? platform.isMacOS,
+ isWindows: isWindows ?? platform.isWindows,
+ lineTerminator: lineTerminator ?? platform.lineTerminator,
+ operatingSystem: operatingSystem ?? platform.operatingSystem,
+ pathSeparator: pathSeparator ?? platform.pathSeparator,
+ resolvedExecutable: resolvedExecutable ?? platform.resolvedExecutable,
+ version: version ?? platform.version,
+ numberOfProcessors: numberOfProcessors ?? platform.numberOfProcessors,
+ script: script ?? platform.script,
+ );
/// Returns [Platform.environment].
Map<String, String> get environment;
@@ -141,6 +154,55 @@
Uri get script => Platform.script;
}
+final class _BrowserPlatformInfo extends PlatformInfo {
+ const _BrowserPlatformInfo() : super._();
+
+ @override
+ Map<String, String> get environment => const {};
+
+ @override
+ String get executable => '';
+
+ @override
+ bool get isAndroid => false;
+
+ @override
+ bool get isFuchsia => false;
+
+ @override
+ bool get isIOS => false;
+
+ @override
+ bool get isLinux => false;
+
+ @override
+ bool get isMacOS => false;
+
+ @override
+ bool get isWindows => false;
+
+ @override
+ String get lineTerminator => '\n';
+
+ @override
+ String get operatingSystem => '';
+
+ @override
+ String get pathSeparator => '/';
+
+ @override
+ String get resolvedExecutable => '';
+
+ @override
+ String get version => '';
+
+ @override
+ int get numberOfProcessors => 1;
+
+ @override
+ Uri get script => Uri();
+}
+
final class _PlatformInfoOverride extends PlatformInfo {
const _PlatformInfoOverride({
required this.environment,
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index 0ed7723..854af65 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -2,6 +2,11 @@
// 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 'package:file/file.dart' as f;
+import 'package:http/http.dart' as http;
+
import 'command.dart' show PubCommand, PubTopLevel;
import 'command.dart';
import 'command/add.dart';
@@ -49,7 +54,25 @@
@override
final String category;
- PubEmbeddableCommand(this.isVerbose, this.category) : super() {
+ final f.FileSystem? fileSystem;
+ final Map<String, String>? environment;
+ final String? platformVersion;
+ final Stream<List<int>>? stdin;
+ final StreamSink<List<int>>? stdout;
+ final StreamSink<List<int>>? stderr;
+ final http.Client? httpClient;
+
+ PubEmbeddableCommand(
+ this.isVerbose,
+ this.category, {
+ this.fileSystem,
+ this.environment,
+ this.platformVersion,
+ this.stdin,
+ this.stdout,
+ this.stderr,
+ this.httpClient,
+ }) : super() {
// This flag was never honored in the embedding but since it was accepted we
// leave it as a hidden flag to avoid breaking clients that pass it.
argParser.addFlag('trace', hide: true);
diff --git a/pubspec.lock b/pubspec.lock
index 4d9ed0f..cdb25e8 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -106,7 +106,7 @@
source: hosted
version: "3.5.2"
file:
- dependency: "direct dev"
+ dependency: "direct main"
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
diff --git a/pubspec.yaml b/pubspec.yaml
index 54973d2..a7e35b3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -11,6 +11,7 @@
collection: ^1.19.1
convert: ^3.1.2
crypto: ^3.0.7
+ file: ^7.0.1
frontend_server_client: ^4.0.0
glob: ^2.1.3
graphs: ^2.3.2
@@ -33,7 +34,6 @@
dev_dependencies:
checks: ^0.3.1
dart_flutter_team_lints: ^3.5.2
- file: ^7.0.1
shelf_test_handler: ^2.0.2
test: ^1.26.3
test_descriptor: ^2.0.2
diff --git a/test/overrides/bytesink.dart b/test/overrides/bytesink.dart
new file mode 100644
index 0000000..fbcf105
--- /dev/null
+++ b/test/overrides/bytesink.dart
@@ -0,0 +1,41 @@
+// 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:typed_data';
+
+final class ByteSink implements StreamSink<List<int>> {
+ final builder = BytesBuilder();
+ final _completer = Completer<void>();
+
+ /// Access the buffered bytes as a Uint8List
+ Uint8List get bytes => builder.toBytes();
+
+ @override
+ void add(List<int> data) {
+ builder.add(data);
+ }
+
+ @override
+ void addError(Object error, [StackTrace? stackTrace]) {
+ if (!_completer.isCompleted) {
+ _completer.completeError(error, stackTrace);
+ }
+ }
+
+ @override
+ Future<void> addStream(Stream<List<int>> stream) async {
+ await stream.forEach(add);
+ }
+
+ @override
+ Future<void> close() async {
+ if (!_completer.isCompleted) {
+ _completer.complete();
+ }
+ }
+
+ @override
+ Future<void> get done => _completer.future;
+}
diff --git a/test/overrides/ensure_resolved_test.dart b/test/overrides/ensure_resolved_test.dart
new file mode 100644
index 0000000..1ed0fa1
--- /dev/null
+++ b/test/overrides/ensure_resolved_test.dart
@@ -0,0 +1,57 @@
+// 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.
+
+@TestOn('vm || browser')
+library;
+
+import 'dart:async';
+import 'dart:convert' show utf8;
+
+import 'package:file/memory.dart';
+import 'package:pub/pub.dart';
+import 'package:test/test.dart';
+
+import 'bytesink.dart';
+
+void main() {
+ test('ensurePubspecResolved in memory', () async {
+ final fs = MemoryFileSystem();
+
+ fs.directory('/workspace').createSync();
+ fs.currentDirectory = '/workspace';
+ fs.directory('/sdk/bin').createSync(recursive: true);
+
+ final bs = ByteSink();
+
+ final pubspec = fs.file('/workspace/pubspec.yaml');
+ await pubspec.writeAsString('''
+name: my_app
+version: 1.0.0
+environment:
+ sdk: ^3.0.0
+dependencies:
+ retry:
+''');
+
+ await ensurePubspecResolved(
+ '/workspace',
+ summaryOnly: false,
+ onlyOutputWhenTerminal: false,
+
+ fileSystem: fs,
+ stdout: bs,
+ stderr: bs,
+ stdin: const Stream.empty(),
+ platformVersion: '3.11.0',
+ environment: {'PUB_CACHE': '/tmp/pub_cache', 'DART_ROOT': '/sdk'},
+ );
+
+ expect(
+ fs.file('/workspace/.dart_tool/package_config.json').existsSync(),
+ isTrue,
+ );
+
+ expect(utf8.decode(bs.bytes), contains('Changed 1 dependency'));
+ });
+}
diff --git a/test/overrides/pubcommand_test.dart b/test/overrides/pubcommand_test.dart
new file mode 100644
index 0000000..49fa711
--- /dev/null
+++ b/test/overrides/pubcommand_test.dart
@@ -0,0 +1,106 @@
+// 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.
+
+@TestOn('vm || browser')
+library;
+
+import 'dart:async';
+import 'dart:convert' show utf8;
+
+import 'package:args/command_runner.dart';
+import 'package:file/file.dart' as f;
+import 'package:file/memory.dart';
+import 'package:http/http.dart' as http;
+import 'package:pub/pub.dart';
+import 'package:test/test.dart';
+
+import 'bytesink.dart';
+
+void main() {
+ test('pubCommand() in memory', () async {
+ final fs = MemoryFileSystem();
+
+ fs.directory('/workspace').createSync();
+ fs.currentDirectory = '/workspace';
+ fs.directory('/sdk/bin').createSync(recursive: true);
+
+ final bs = ByteSink();
+
+ Future<int?> pub(List<String> args) async {
+ bs.add(utf8.encode('\$ dart pub ${args.join(' ')}\n'));
+ return await Runner(
+ fileSystem: fs,
+ stdout: bs,
+ stderr: bs,
+ stdin: const Stream.empty(),
+ platformVersion: '3.11.0',
+ environment: {'PUB_CACHE': '/tmp/pub_cache', 'DART_ROOT': '/sdk'},
+ httpClient: http.Client(),
+ ).run(['pub', ...args]);
+ }
+
+ final pubspec = fs.file('/workspace/pubspec.yaml');
+ await pubspec.writeAsString('''
+name: my_app
+version: 1.0.0
+environment:
+ sdk: ^3.0.0
+''');
+
+ try {
+ final exitCode = await pub(['get']);
+ expect(exitCode, 0);
+
+ expect(
+ fs.file('/workspace/.dart_tool/package_config.json').existsSync(),
+ isTrue,
+ );
+
+ expect(await pub(['add', 'retry']), 0);
+ expect(await pub(['downgrade']), 0);
+ expect(await pub(['outdated']), 0);
+ expect(await pub(['upgrade']), 0);
+ expect(await pub(['remove', 'retry']), 0);
+ expect(await pub(['unpack', 'retry']), 0);
+ } catch (_) {
+ printOnFailure(utf8.decode(bs.bytes));
+ rethrow;
+ }
+
+ expect(utf8.decode(bs.bytes), contains('Changed 1 dependency'));
+ });
+}
+
+class Runner extends CommandRunner<int> {
+ final f.FileSystem fileSystem;
+ final Map<String, String> environment;
+ final String platformVersion;
+ final Stream<List<int>> stdin;
+ final StreamSink<List<int>> stdout;
+ final StreamSink<List<int>> stderr;
+ final http.Client httpClient;
+
+ Runner({
+ required this.fileSystem,
+ required this.environment,
+ required this.platformVersion,
+ required this.stdin,
+ required this.stdout,
+ required this.stderr,
+ required this.httpClient,
+ }) : super('dart', 'dart pub emulator') {
+ addCommand(
+ pubCommand(
+ isVerbose: () => false,
+ fileSystem: fileSystem,
+ environment: environment,
+ platformVersion: platformVersion,
+ stdin: stdin,
+ stdout: stdout,
+ stderr: stderr,
+ httpClient: httpClient,
+ ),
+ );
+ }
+}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 7698901..a4adec3 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -17,11 +17,9 @@
import 'dart:typed_data';
import 'package:async/async.dart';
-import 'package:http/testing.dart';
import 'package:pub/src/entrypoint.dart';
import 'package:pub/src/exit_codes.dart' as exit_codes;
import 'package:pub/src/git.dart' as git;
-import 'package:pub/src/http.dart';
import 'package:pub/src/io.dart';
import 'package:pub/src/lock_file.dart';
import 'package:pub/src/log.dart' as log;
@@ -754,18 +752,6 @@
return LockFile(packages);
}
-/// Uses [client] as the mock HTTP client for this test.
-///
-/// Note that this will only affect HTTP requests made via http.dart in the
-/// parent process.
-void useMockClient(MockClient client) {
- final oldInnerClient = innerHttpClient;
- innerHttpClient = client;
- addTearDown(() {
- innerHttpClient = oldInnerClient;
- });
-}
-
/// Describes a map representing a library package with the given [name],
/// [version], and [dependencies].
Map<String, Object> packageMap(
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
index 9615a0f..cf58928 100644
--- a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -327,11 +327,13 @@
$ tool/test-bin/pub_command_runner.dart pub fail
[E] Bad state: Pub has crashed
[E] tool/test-bin/pub_command_runner.dart $LINE:$COL ThrowingCommand.runProtected
-[E] package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn>
+[E] package:pub/src/command.dart $LINE:$COL PubCommand._run.<fn>
[E] dart:async new Future.sync
[E] package:pub/src/utils.dart $LINE:$COL captureErrors.wrappedCallback
[E] dart:async runZonedGuarded
[E] package:pub/src/utils.dart $LINE:$COL captureErrors
+[E] package:pub/src/command.dart $LINE:$COL PubCommand._run
+[E] package:pub/src/io.dart $LINE:$COL withOverrides
[E] package:pub/src/command.dart $LINE:$COL PubCommand.run
[E] package:args/command_runner.dart $LINE:$COL CommandRunner.runCommand
[E] tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.runCommand
@@ -368,11 +370,13 @@
ERR : Bad state: Pub has crashed
FINE: Exception type: StateError
ERR : tool/test-bin/pub_command_runner.dart $LINE:$COL ThrowingCommand.runProtected
- | package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn>
+ | package:pub/src/command.dart $LINE:$COL PubCommand._run.<fn>
| dart:async new Future.sync
| package:pub/src/utils.dart $LINE:$COL captureErrors.wrappedCallback
| dart:async runZonedGuarded
| package:pub/src/utils.dart $LINE:$COL captureErrors
+ | package:pub/src/command.dart $LINE:$COL PubCommand._run
+ | package:pub/src/io.dart $LINE:$COL withOverrides
| package:pub/src/command.dart $LINE:$COL PubCommand.run
| package:args/command_runner.dart $LINE:$COL CommandRunner.runCommand
| tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.runCommand
diff --git a/test/wasm/iooverrides.dart b/test/wasm/iooverrides.dart
deleted file mode 100644
index 4532703..0000000
--- a/test/wasm/iooverrides.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-import 'dart:io';
-
-import 'package:file/file.dart' as f;
-
-/// Creates an [IOOverrides] that uses [fs] for all operations.
-IOOverrides createFileSystemIOOverrides(f.FileSystem fs) =>
- _FileSystemIOOverrides(fs);
-
-/// An [IOOverrides] that uses a [f.FileSystem] for all operations.
-final class _FileSystemIOOverrides extends IOOverrides {
- final f.FileSystem _fs;
-
- _FileSystemIOOverrides(this._fs);
-
- @override
- File createFile(String path) => _fs.file(path);
-
- @override
- Directory createDirectory(String path) => _fs.directory(path);
-
- @override
- Link createLink(String path) => _fs.link(path);
-
- @override
- Future<FileStat> stat(String path) => _fs.stat(path);
-
- @override
- FileStat statSync(String path) => _fs.statSync(path);
-
- @override
- Future<bool> fseIdentical(String path1, String path2) =>
- _fs.identical(path1, path2);
-
- @override
- bool fseIdenticalSync(String path1, String path2) =>
- _fs.identicalSync(path1, path2);
-
- @override
- Future<FileSystemEntityType> fseGetType(String path, bool followLinks) =>
- _fs.type(path, followLinks: followLinks);
-
- @override
- FileSystemEntityType fseGetTypeSync(String path, bool followLinks) =>
- _fs.typeSync(path, followLinks: followLinks);
-
- @override
- Directory getCurrentDirectory() => _fs.currentDirectory;
-
- @override
- void setCurrentDirectory(String path) {
- _fs.currentDirectory = path;
- }
-
- @override
- Directory getSystemTempDirectory() => _fs.systemTempDirectory;
-}
diff --git a/test/wasm/iooverrides_test.dart b/test/wasm/iooverrides_test.dart
deleted file mode 100644
index 0986462..0000000
--- a/test/wasm/iooverrides_test.dart
+++ /dev/null
@@ -1,44 +0,0 @@
-import 'dart:io';
-
-import 'package:file/memory.dart';
-import 'package:test/test.dart';
-
-import 'iooverrides.dart';
-
-void main() {
- test('FileSystemIOOverrides redirects IO operations', () async {
- final fs = MemoryFileSystem();
- await IOOverrides.runWithIOOverrides(() async {
- final file = File('/test.txt');
- await file.writeAsString('hello');
-
- expect(fs.file('/test.txt').readAsStringSync(), 'hello');
- expect(await file.readAsString(), 'hello');
-
- final dir = Directory('/subdir');
- await dir.create();
- expect(fs.directory('/subdir').existsSync(), isTrue);
-
- final list = await Directory('/').list().toList();
- expect(list.map((e) => e.path), containsAll(['test.txt', 'subdir']));
- }, createFileSystemIOOverrides(fs));
- });
-
- test('getCurrentDirectory and setCurrentDirectory', () async {
- final fs = MemoryFileSystem();
- fs.directory('/a/b').createSync(recursive: true);
- await IOOverrides.runWithIOOverrides(() async {
- expect(Directory.current.path, '/');
- Directory.current = '/a/b';
- expect(Directory.current.path, '/a/b');
- expect(fs.currentDirectory.path, '/a/b');
- }, createFileSystemIOOverrides(fs));
- });
-
- test('systemTempDirectory', () async {
- final fs = MemoryFileSystem();
- await IOOverrides.runWithIOOverrides(() async {
- expect(Directory.systemTemp.path, fs.systemTempDirectory.path);
- }, createFileSystemIOOverrides(fs));
- });
-}