Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add offline mode #2483

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions webdev/lib/src/command/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const nullSafetyAuto = 'auto';
const disableDdsFlag = 'disable-dds';
const enableExperimentOption = 'enable-experiment';
const canaryFeaturesFlag = 'canary';
const offlineFlag = 'offline';

ReloadConfiguration _parseReloadConfiguration(ArgResults argResults) {
var auto = argResults.options.contains(autoOption)
Expand Down Expand Up @@ -107,6 +108,7 @@ class Configuration {
final String? _nullSafety;
final List<String>? _experiments;
final bool? _canaryFeatures;
final bool? _offline;

Configuration({
bool? autoRun,
Expand All @@ -133,6 +135,7 @@ class Configuration {
String? nullSafety,
List<String>? experiments,
bool? canaryFeatures,
bool? offline,
}) : _autoRun = autoRun,
_chromeDebugPort = chromeDebugPort,
_debugExtension = debugExtension,
Expand All @@ -154,7 +157,8 @@ class Configuration {
_verbose = verbose,
_nullSafety = nullSafety,
_experiments = experiments,
_canaryFeatures = canaryFeatures {
_canaryFeatures = canaryFeatures,
_offline = offline {
_validateConfiguration();
}

Expand Down Expand Up @@ -229,7 +233,8 @@ class Configuration {
verbose: other._verbose ?? _verbose,
nullSafety: other._nullSafety ?? _nullSafety,
experiments: other._experiments ?? _experiments,
canaryFeatures: other._canaryFeatures ?? _canaryFeatures);
canaryFeatures: other._canaryFeatures ?? _canaryFeatures,
offline: other._offline ?? _offline);

factory Configuration.noInjectedClientDefaults() =>
Configuration(autoRun: false, debug: false, debugExtension: false);
Expand Down Expand Up @@ -284,6 +289,8 @@ class Configuration {

bool get canaryFeatures => _canaryFeatures ?? false;

bool get offline => _offline ?? false;

/// Returns a new configuration with values updated from the parsed args.
static Configuration fromArgs(ArgResults? argResults,
{Configuration? defaultConfiguration}) {
Expand Down Expand Up @@ -408,6 +415,10 @@ class Configuration {
? argResults[canaryFeaturesFlag] as bool?
: defaultConfiguration.canaryFeatures;

final offline = argResults.options.contains(offlineFlag)
? argResults[offlineFlag] as bool?
: defaultConfiguration.verbose;

return Configuration(
autoRun: defaultConfiguration.autoRun,
chromeDebugPort: chromeDebugPort,
Expand All @@ -433,6 +444,7 @@ class Configuration {
nullSafety: nullSafety,
experiments: experiments,
canaryFeatures: canaryFeatures,
offline: offline,
);
}
}
Expand Down
10 changes: 8 additions & 2 deletions webdev/lib/src/command/shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ void addSharedArgs(ArgParser argParser,
abbr: 'v',
defaultsTo: false,
negatable: false,
help: 'Enables verbose logging.');
help: 'Enables verbose logging.')
..addFlag(offlineFlag,
defaultsTo: false,
negatable: false,
help: 'Disable feching from pub.dev.');
}

/// Parses the provided [Configuration] to return a list of
Expand Down Expand Up @@ -103,7 +107,9 @@ List<String> buildRunnerArgs(Configuration configuration) {
}

Future<void> validatePubspecLock(Configuration configuration) async {
final pubspecLock = await PubspecLock.read();
final pubspecLock = await PubspecLock.read(
offline: configuration.offline
);
await checkPubspecLock(pubspecLock,
requireBuildWebCompilers: configuration.requireBuildWebCompilers);
}
Expand Down
8 changes: 6 additions & 2 deletions webdev/lib/src/pubspec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ class PubspecLock {

PubspecLock(this._packages);

static Future<PubspecLock> read() async {
await _runPubDeps();
static Future<PubspecLock> read({
bool offline = false
}) async {
if (!offline) {
await _runPubDeps();
}
var dir = p.absolute(p.current);
while (true) {
final candidate = p.join(
Expand Down
43 changes: 43 additions & 0 deletions webdev/test/installation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ void main() {
Process? serveProcess;
Directory? tempDir0;

final testScript =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure what's going on with these lines. Could we not get the package directory simply by doing p.dirname(p.dirname(Platform.script.toFilePath())?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That gives me /tmp

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, because the test runner generates an entrypoint...

In that case, I think we should continue to activate webdev from pub instead of using --source path. This test should still work as long as we don't run dart pub get on the created project before running webdev serve --offline.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok, I was assuming we would do it the way I did so new updates would actually work. I was getting an error saying --offline wasn't a recognized flag even if I did activate it outside the test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that's a great point. Given that, I think we should move the test to webdev/test/integration_test.dart. The tests there run webdev from source so we won't have to worry about publishing before this new test will pass.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbf, I could see how it could fit into both. Especially since once the next update is published and this PR is in that, then the flag would be available.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got the test added to the integration set.

File(p.join(p.dirname(Platform.script.toFilePath()), 'test.dart'))
.readAsStringSync();
final thisScript = File.fromUri(Uri.parse(testScript.substring(
testScript.lastIndexOf('import', testScript.indexOf('as test;')) + 8,
testScript.indexOf('as test;') - 2)));
final packageDir = p.dirname(p.dirname(thisScript.path));

Future<void> expectStdoutAndCleanExit(Process process,
{required String expectedStdout}) async {
final stdoutCompleter = _captureOutput(
Expand Down Expand Up @@ -141,6 +149,41 @@ void main() {
await expectStdoutThenExit(serveProcess!,
expectedStdout: 'Serving `web` on');
});

test('activate and serve webdev fails with offline', () async {
final tempDir = tempDir0!;
final tempPath = tempDir.path;

// Verify that we can create a new Dart app:
createProcess = await Process.start(
'dart',
['create', '--no-pub', '--template', 'web', 'temp_app'],
workingDirectory: tempPath,
);
await expectStdoutAndCleanExit(
createProcess!,
expectedStdout: 'Created project temp_app in temp_app!',
);
final appPath = p.join(tempPath, 'temp_app');
expect(await Directory(appPath).exists(), isTrue);

// Verify that `dart pub global activate` works:
activateProcess = await Process.start(
'dart',
['pub', 'global', 'activate', '--source', 'path', packageDir],
);
await expectStdoutAndCleanExit(
activateProcess!,
expectedStdout: 'Activated webdev',
);

// Verify that `webdev serve` works for our new app:
serveProcess = await Process.start('dart',
['pub', 'global', 'run', 'webdev', 'serve', '--offline', 'web:8081'],
workingDirectory: appPath);
await expectStdoutThenExit(serveProcess!,
expectedStdout: 'Cannot open file\n pubspec.lock\n');
});
}

Future<int> _waitForExitOrTimeout(Process process) {
Expand Down
97 changes: 97 additions & 0 deletions webdev/test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,62 @@
// 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.

@Timeout(Duration(minutes: 3))

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

import 'test_utils.dart';

enum StreamType {
stdout,
stderr,
}

const processTimeout = Duration(minutes: 1);

void main() {
final testRunner = TestRunner();
setUpAll(testRunner.setUpAll);
tearDownAll(testRunner.tearDownAll);

Future<void> expectStdoutAndCleanExit(Process process,
{required String expectedStdout}) async {
final stdoutCompleter = _captureOutput(
process,
streamType: StreamType.stdout,
stopCaptureFuture: process.exitCode,
);
final stderrCompleter = _captureOutput(
process,
streamType: StreamType.stderr,
stopCaptureFuture: process.exitCode,
);
final exitCode = await _waitForExitOrTimeout(process);
final stderrLogs = await stderrCompleter.future;
final stdoutLogs = await stdoutCompleter.future;
expect(
exitCode,
equals(0),
// Include the stderr and stdout logs if the process does not terminate
// cleanly:
reason: 'stderr: $stderrLogs, stdout: $stdoutLogs',
);
expect(
stderrLogs,
isEmpty,
);
expect(
stdoutLogs,
contains(expectedStdout),
);
}

test('non-existent commands create errors', () async {
final process = await testRunner.runWebDev(['monkey']);

Expand Down Expand Up @@ -214,6 +258,26 @@ dependencies:
await checkProcessStdout(process, ['webdev could not run']);
await process.shouldExit(78);
});

if (command != 'daemon') {
test('failure with offline and unresolved dependencies', () async {
final createProcess = await Process.start(
'dart',
['create', '--no-pub', '--template', 'web', 'temp_app'],
workingDirectory: d.sandbox,
);
await expectStdoutAndCleanExit(createProcess,
expectedStdout: 'Created project temp_app');

final appPath = p.join(d.sandbox, 'temp_app');

final process = await testRunner
.runWebDev([command, '--offline'], workingDirectory: appPath);

await checkProcessStdout(process, ['webdev could not run']);
await process.shouldExit(78);
});
}
});
}
}
Expand Down Expand Up @@ -286,3 +350,36 @@ packages:

return buffer.toString();
}

Future<int> _waitForExitOrTimeout(Process process) {
Timer(processTimeout, () {
process.kill(ProcessSignal.sigint);
});
return process.exitCode;
}

Completer<String> _captureOutput(
Process process, {
required StreamType streamType,
required Future stopCaptureFuture,
}) {
final stream =
streamType == StreamType.stdout ? process.stdout : process.stderr;
final completer = Completer<String>();
var output = '';
stream.transform(utf8.decoder).listen((line) {
output += line;
if (line.contains('[SEVERE]')) {
process.kill(ProcessSignal.sigint);
if (!completer.isCompleted) {
completer.complete(output);
}
}
});
unawaited(stopCaptureFuture.then((_) {
if (!completer.isCompleted) {
completer.complete(output);
}
}));
return completer;
}