Add support for a timeout flag.
Closes #279
R=lrn@google.com, kevmoo@google.com
Review URL: https://codereview.chromium.org//1604043003 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f7c789..3413bc2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
* Organize the `--help` output into sections.
+* Add a `--timeout` flag.
+
## 0.12.7
* Add the ability to re-run tests while debugging. When the browser is paused at
diff --git a/lib/src/frontend/timeout.dart b/lib/src/frontend/timeout.dart
index 54ce540..9aca1c0 100644
--- a/lib/src/frontend/timeout.dart
+++ b/lib/src/frontend/timeout.dart
@@ -2,6 +2,21 @@
// 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 'package:string_scanner/string_scanner.dart';
+
+/// A regular expression that matches text until a letter or whitespace.
+///
+/// This is intended to scan through a number without actually encoding the full
+/// Dart number grammar. It doesn't stop on "e" because that can be a component
+/// of numbers.
+final _untilUnit = new RegExp(r"[^a-df-z\s]+", caseSensitive: false);
+
+/// A regular expression that matches a time unit.
+final _unit = new RegExp(r"([um]s|[dhms])", caseSensitive: false);
+
+/// A regular expression that matches a section of whitespace.
+final _whitespace = new RegExp(r"\s+");
+
/// A class representing a modification to the default timeout for a test.
///
/// By default, a test will time out after 30 seconds. With [new Timeout], that
@@ -39,10 +54,76 @@
: scaleFactor = null,
duration = null;
+ /// Parse the timeout from a user-provided string.
+ ///
+ /// This supports the following formats:
+ ///
+ /// * `Number "x"`, which produces a relative timeout with the given scale
+ /// factor.
+ ///
+ /// * `(Number ("d" | "h" | "m" | "s" | "ms" | "us") (" ")?)+`, which produces
+ /// an absolute timeout with the duration given by the sum of the given
+ /// units.
+ ///
+ /// * `"none"`, which produces [Timeout.none].
+ ///
+ /// Throws a [FormatException] if [timeout] is not in a valid format
+ factory Timeout.parse(String timeout) {
+ var scanner = new StringScanner(timeout);
+
+ // First check for the string "none".
+ if (scanner.scan("none")) {
+ scanner.expectDone();
+ return Timeout.none;
+ }
+
+ // Scan a number. This will be either a time unit or a scale factor.
+ scanner.expect(_untilUnit, name: "number");
+ var number = double.parse(scanner.lastMatch[0]);
+
+ // A number followed by "x" is a scale factor.
+ if (scanner.scan("x") || scanner.scan("X")) {
+ scanner.expectDone();
+ return new Timeout.factor(number);
+ }
+
+ // Parse time units until none are left. The condition is in the middle of
+ // the loop because we've already parsed the first number.
+ var microseconds = 0;
+ while (true) {
+ scanner.expect(_unit, name: "unit");
+ microseconds += _microsecondsFor(number, scanner.lastMatch[0]);
+
+ scanner.scan(_whitespace);
+
+ // Scan the next number, if it's avaialble.
+ if (!scanner.scan(_untilUnit)) break;
+ number = double.parse(scanner.lastMatch[0]);
+ }
+
+ scanner.expectDone();
+ return new Timeout(new Duration(microseconds: microseconds.round()));
+ }
+
+ /// Returns the number of microseconds in [number] [unit]s.
+ static double _microsecondsFor(double number, String unit) {
+ switch (unit) {
+ case "d": return number * 24 * 60 * 60 * 1000000;
+ case "h": return number * 60 * 60 * 1000000;
+ case "m": return number * 60 * 1000000;
+ case "s": return number * 1000000;
+ case "ms": return number * 1000;
+ case "us": return number;
+ default: throw new ArgumentError("Unknown unit $unit.");
+ }
+ }
+
/// Returns a new [Timeout] that merges [this] with [other].
///
- /// If [other] declares a [duration], that takes precedence. Otherwise, this
- /// timeout's [duration] or [factor] are multiplied by [other]'s [factor].
+ /// [Timeout.none] takes precedence over everything. If timeout is
+ /// [Timeout.none] and [other] declares a [duration], that takes precedence.
+ /// Otherwise, this timeout's [duration] or [factor] are multiplied by
+ /// [other]'s [factor].
Timeout merge(Timeout other) {
if (this == none || other == none) return none;
if (other.duration != null) return new Timeout(other.duration);
@@ -58,6 +139,11 @@
return duration == null ? base * scaleFactor : duration;
}
+ int get hashCode => duration.hashCode ^ 5 * scaleFactor.hashCode;
+
+ bool operator==(other) => other is Timeout && other.duration == duration &&
+ other.scaleFactor == scaleFactor;
+
String toString() {
if (duration != null) return duration.toString();
if (scaleFactor != null) return "${scaleFactor}x";
diff --git a/lib/src/runner/configuration.dart b/lib/src/runner/configuration.dart
index 2876f15..b9e8494 100644
--- a/lib/src/runner/configuration.dart
+++ b/lib/src/runner/configuration.dart
@@ -72,9 +72,16 @@
parser.addOption("pub-serve",
help: 'The port of a pub serve instance serving "test/".',
valueHelp: 'port');
+
+ // Note: although we list the 30s default timeout as though it were a
+ // default value for this argument, it's actually encoded in the [Invoker]'s
+ // call to [Timeout.apply].
+ parser.addOption("timeout",
+ help: 'The default test timeout. For example: 15s, 2x, none\n'
+ '(defaults to 30s)');
parser.addFlag("pause-after-load",
help: 'Pauses for debugging before any tests execute.\n'
- 'Implies --concurrency=1.\n'
+ 'Implies --concurrency=1 and --timeout=none.\n'
'Currently only supported for browser tests.',
negatable: false);
@@ -129,6 +136,9 @@
/// if tests should be loaded from the filesystem.
final Uri pubServeUrl;
+ /// The default test timeout.
+ final Timeout timeout;
+
/// Whether to use command-line color escapes.
final bool color;
@@ -156,9 +166,7 @@
/// The global test metadata derived from this configuration.
Metadata get metadata =>
- new Metadata(
- timeout: pauseAfterLoad ? Timeout.none : null,
- verboseTrace: verboseTrace);
+ new Metadata(timeout: timeout, verboseTrace: verboseTrace);
/// Parses the configuration from [args].
///
@@ -208,6 +216,9 @@
pubServePort: _wrapFormatException(options, 'pub-serve', int.parse),
concurrency: _wrapFormatException(options, 'concurrency', int.parse,
orElse: () => _defaultConcurrency),
+ timeout: _wrapFormatException(options, 'timeout',
+ (value) => new Timeout.parse(value),
+ orElse: () => new Timeout.factor(1)),
pattern: pattern,
platforms: options['platform'].map(TestPlatform.find),
paths: options.rest.isEmpty ? null : options.rest,
@@ -233,9 +244,9 @@
Configuration({this.help: false, this.version: false,
this.verboseTrace: false, this.jsTrace: false,
bool pauseAfterLoad: false, bool color, String packageRoot,
- String reporter, int pubServePort, int concurrency, this.pattern,
- Iterable<TestPlatform> platforms, Iterable<String> paths,
- Set<String> tags, Set<String> excludeTags})
+ String reporter, int pubServePort, int concurrency, Timeout timeout,
+ this.pattern, Iterable<TestPlatform> platforms,
+ Iterable<String> paths, Set<String> tags, Set<String> excludeTags})
: pauseAfterLoad = pauseAfterLoad,
color = color == null ? canUseSpecialChars : color,
packageRoot = packageRoot == null
@@ -248,6 +259,9 @@
concurrency = pauseAfterLoad
? 1
: (concurrency == null ? _defaultConcurrency : concurrency),
+ timeout = pauseAfterLoad
+ ? Timeout.none
+ : (timeout == null ? new Timeout.factor(1) : timeout),
platforms = platforms == null ? [TestPlatform.vm] : platforms.toList(),
paths = paths == null ? ["test"] : paths.toList(),
explicitPaths = paths != null,
diff --git a/pubspec.yaml b/pubspec.yaml
index 8d6472e..71f545c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: test
-version: 0.12.8-dev
+version: 0.12.8
author: Dart Team <misc@dartlang.org>
description: A library for writing dart unit tests.
homepage: https://github.com/dart-lang/test
diff --git a/test/frontend/timeout_test.dart b/test/frontend/timeout_test.dart
new file mode 100644
index 0000000..382466c
--- /dev/null
+++ b/test/frontend/timeout_test.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2016, 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 'package:test/src/backend/state.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group("Timeout.parse", () {
+ group('for "none"', () {
+ test("successfully parses", () {
+ expect(new Timeout.parse("none"), equals(Timeout.none));
+ });
+
+ test("rejects invalid input", () {
+ expect(() => new Timeout.parse(" none"), throwsFormatException);
+ expect(() => new Timeout.parse("none "), throwsFormatException);
+ expect(() => new Timeout.parse("xnone"), throwsFormatException);
+ expect(() => new Timeout.parse("nonex"), throwsFormatException);
+ expect(() => new Timeout.parse("noxe"), throwsFormatException);
+ });
+ });
+
+ group("for a relative timeout", () {
+ test("successfully parses", () {
+ expect(new Timeout.parse("1x"), equals(new Timeout.factor(1)));
+ expect(new Timeout.parse("2.5x"), equals(new Timeout.factor(2.5)));
+ expect(new Timeout.parse("1.2e3x"), equals(new Timeout.factor(1.2e3)));
+ });
+
+ test("rejects invalid input", () {
+ expect(() => new Timeout.parse(".x"), throwsFormatException);
+ expect(() => new Timeout.parse("x"), throwsFormatException);
+ expect(() => new Timeout.parse("ax"), throwsFormatException);
+ expect(() => new Timeout.parse("1x "), throwsFormatException);
+ expect(() => new Timeout.parse("1x5m"), throwsFormatException);
+ });
+ });
+
+ group("for an absolute timeout", () {
+ test("successfully parses all supported units", () {
+ expect(new Timeout.parse("2d"),
+ equals(new Timeout(new Duration(days: 2))));
+ expect(new Timeout.parse("2h"),
+ equals(new Timeout(new Duration(hours: 2))));
+ expect(new Timeout.parse("2m"),
+ equals(new Timeout(new Duration(minutes: 2))));
+ expect(new Timeout.parse("2s"),
+ equals(new Timeout(new Duration(seconds: 2))));
+ expect(new Timeout.parse("2ms"),
+ equals(new Timeout(new Duration(milliseconds: 2))));
+ expect(new Timeout.parse("2us"),
+ equals(new Timeout(new Duration(microseconds: 2))));
+ });
+
+ test("supports non-integer units", () {
+ expect(new Timeout.parse("2.73d"),
+ equals(new Timeout(new Duration(days: 1) * 2.73)));
+ });
+
+ test("supports multiple units", () {
+ expect(new Timeout.parse("1d 2h3m 4s5ms\t6us"),
+ equals(new Timeout(new Duration(
+ days: 1,
+ hours: 2,
+ minutes: 3,
+ seconds: 4,
+ milliseconds: 5,
+ microseconds: 6))));
+ });
+
+ test("rejects invalid input", () {
+ expect(() => new Timeout.parse(".d"), throwsFormatException);
+ expect(() => new Timeout.parse("d"), throwsFormatException);
+ expect(() => new Timeout.parse("ad"), throwsFormatException);
+ expect(() => new Timeout.parse("1z"), throwsFormatException);
+ expect(() => new Timeout.parse("1u"), throwsFormatException);
+ expect(() => new Timeout.parse("1d5x"), throwsFormatException);
+ expect(() => new Timeout.parse("1d*5m"), throwsFormatException);
+ });
+ });
+ });
+}
diff --git a/test/runner/runner_test.dart b/test/runner/runner_test.dart
index 6bacd10..8a69547 100644
--- a/test/runner/runner_test.dart
+++ b/test/runner/runner_test.dart
@@ -344,27 +344,6 @@
test.shouldExit(1);
});
- test("respects top-level @Timeout declarations", () {
- d.file("test.dart", '''
-@Timeout(const Duration(seconds: 0))
-
-import 'dart:async';
-
-import 'package:test/test.dart';
-
-void main() {
- test("timeout", () {});
-}
-''').create();
-
- var test = runTest(["test.dart"]);
- test.stdout.expect(containsInOrder([
- "Test timed out after 0 seconds.",
- "-1: Some tests failed."
- ]));
- test.shouldExit(1);
- });
-
test("respects top-level @Skip declarations", () {
d.file("test.dart", '''
@Skip()
diff --git a/test/runner/timeout_test.dart b/test/runner/timeout_test.dart
new file mode 100644
index 0000000..e1c8a5f
--- /dev/null
+++ b/test/runner/timeout_test.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2016, 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")
+
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:scheduled_test/descriptor.dart' as d;
+import 'package:scheduled_test/scheduled_stream.dart';
+import 'package:scheduled_test/scheduled_test.dart';
+import 'package:test/src/util/exit_codes.dart' as exit_codes;
+
+import '../io.dart';
+
+void main() {
+ useSandbox();
+
+ test("respects top-level @Timeout declarations", () {
+ d.file("test.dart", '''
+@Timeout(const Duration(seconds: 0))
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+ test("timeout", () async {
+ await new Future.delayed(Duration.ZERO);
+ });
+}
+''').create();
+
+ var test = runTest(["test.dart"]);
+ test.stdout.expect(containsInOrder([
+ "Test timed out after 0 seconds.",
+ "-1: Some tests failed."
+ ]));
+ test.shouldExit(1);
+ });
+
+ test("respects the --timeout flag", () {
+ d.file("test.dart", '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+ test("timeout", () async {
+ await new Future.delayed(Duration.ZERO);
+ });
+}
+''').create();
+
+ var test = runTest(["--timeout=0s", "test.dart"]);
+ test.stdout.expect(containsInOrder([
+ "Test timed out after 0 seconds.",
+ "-1: Some tests failed."
+ ]));
+ test.shouldExit(1);
+ });
+
+ test("the --timeout flag applies on top of the default 30s timeout", () {
+ d.file("test.dart", '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+ test("no timeout", () async {
+ await new Future.delayed(new Duration(milliseconds: 250));
+ });
+
+ test("timeout", () async {
+ await new Future.delayed(new Duration(milliseconds: 750));
+ });
+}
+''').create();
+
+ // This should make the timeout about 500ms, which should cause exactly one
+ // test to fail.
+ var test = runTest(["--timeout=0.016x", "test.dart"]);
+ test.stdout.expect(containsInOrder([
+ "Test timed out after 0.4 seconds.",
+ "-1: Some tests failed."
+ ]));
+ test.shouldExit(1);
+ });
+}
+