Line data Source code
1 : // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'dart:async';
6 :
7 : import 'package:stack_trace/stack_trace.dart';
8 : import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
9 : import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
10 : import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
11 : import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
12 : import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
13 : import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
14 : import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
15 : // ignore: deprecated_member_use
16 : import 'package:test_api/scaffolding.dart' show Timeout;
17 :
18 : import '../util/async.dart';
19 : import '../util/io_stub.dart' if (dart.library.io) '../util/io.dart';
20 : import '../util/pair.dart';
21 : import 'load_exception.dart';
22 : import 'plugin/environment.dart';
23 : import 'runner_suite.dart';
24 : import 'suite.dart';
25 :
26 : /// The timeout for loading a test suite.
27 : ///
28 : /// We want this to be long enough that even a very large application being
29 : /// compiled with dart2js doesn't trigger it, but short enough that it fires
30 : /// before the host kills it. For example, Google's Forge service has a
31 : /// 15-minute timeout.
32 0 : final _timeout = Duration(minutes: 12);
33 :
34 : /// A [Suite] emitted by a [Loader] that provides a test-like interface for
35 : /// loading a test file.
36 : ///
37 : /// This is used to expose the current status of test loading to the user. It's
38 : /// important to provide users visibility into what's taking a long time and
39 : /// where failures occur. And since some tests may be loaded at the same time as
40 : /// others are run, it's useful to provide that visibility in the form of a test
41 : /// suite so that it can integrate well into the existing reporting interface
42 : /// without too much extra logic.
43 : ///
44 : /// A suite is constructed with logic necessary to produce a test suite. As with
45 : /// a normal test body, this logic isn't run until [LiveTest.run] is called. The
46 : /// suite itself is returned by [suite] once it's avaialble, but any errors or
47 : /// prints will be emitted through the running [LiveTest].
48 : class LoadSuite extends Suite implements RunnerSuite {
49 : @override
50 : final environment = const PluginEnvironment();
51 : @override
52 : final SuiteConfiguration config;
53 : @override
54 : final isDebugging = false;
55 : @override
56 : final onDebugging = StreamController<bool>().stream;
57 :
58 0 : @override
59 : bool get isLoadSuite => true;
60 :
61 : /// A future that completes to the loaded suite once the suite's test has been
62 : /// run and completed successfully.
63 : ///
64 : /// This will return `null` if the suite is unavailable for some reason (for
65 : /// example if an error occurred while loading it).
66 0 : Future<RunnerSuite?> get suite async => (await _suiteAndZone)?.first;
67 :
68 : /// A future that completes to a pair of [suite] and the load test's [Zone].
69 : ///
70 : /// This will return `null` if the suite is unavailable for some reason (for
71 : /// example if an error occurred while loading it).
72 : final Future<Pair<RunnerSuite, Zone>?> _suiteAndZone;
73 :
74 : /// Returns the test that loads the suite.
75 : ///
76 : /// Load suites are guaranteed to only contain one test. This is a utility
77 : /// method for accessing it directly.
78 0 : Test get test => group.entries.single as Test;
79 :
80 : /// Creates a load suite named [name] on [platform].
81 : ///
82 : /// [body] may return either a [RunnerSuite] or a [Future] that completes to a
83 : /// [RunnerSuite]. Its return value is forwarded through [suite], although if
84 : /// it throws an error that will be forwarded through the suite's test.
85 : ///
86 : /// If the the load test is closed before [body] is complete, it will close
87 : /// the suite returned by [body] once it completes.
88 0 : factory LoadSuite(String name, SuiteConfiguration config,
89 : SuitePlatform platform, FutureOr<RunnerSuite?> Function() body,
90 : {String? path}) {
91 0 : var completer = Completer<Pair<RunnerSuite, Zone>?>.sync();
92 0 : return LoadSuite._(name, config, platform, () {
93 0 : var invoker = Invoker.current;
94 0 : invoker!.addOutstandingCallback();
95 :
96 0 : unawaited(() async {
97 0 : var suite = await body();
98 0 : if (completer.isCompleted) {
99 : // If the load test has already been closed, close the suite it
100 : // generated.
101 0 : await suite?.close();
102 : return;
103 : }
104 :
105 0 : completer.complete(suite == null ? null : Pair(suite, Zone.current));
106 0 : invoker.removeOutstandingCallback();
107 : }());
108 :
109 : // If the test completes before the body callback, either an out-of-band
110 : // error occurred or the test was canceled. Either way, we return a `null`
111 : // suite.
112 0 : invoker.liveTest.onComplete.then((_) {
113 0 : if (!completer.isCompleted) completer.complete();
114 : });
115 :
116 : // If the test is forcibly closed, let it complete, since load tests don't
117 : // have timeouts.
118 0 : invoker.onClose.then((_) => invoker.removeOutstandingCallback());
119 0 : }, completer.future, path: path);
120 : }
121 :
122 : /// A utility constructor for a load suite that just throws [exception].
123 : ///
124 : /// The suite's name will be based on [exception]'s path.
125 0 : factory LoadSuite.forLoadException(
126 : LoadException exception, SuiteConfiguration? config,
127 : {SuitePlatform? platform, StackTrace? stackTrace}) {
128 0 : stackTrace ??= Trace.current();
129 :
130 0 : return LoadSuite(
131 0 : 'loading ${exception.path}',
132 0 : config ?? SuiteConfiguration.empty,
133 0 : platform ?? currentPlatform(Runtime.vm),
134 0 : () => Future.error(exception, stackTrace),
135 0 : path: exception.path);
136 : }
137 :
138 : /// A utility constructor for a load suite that just emits [suite].
139 0 : factory LoadSuite.forSuite(RunnerSuite suite) {
140 0 : return LoadSuite(
141 0 : 'loading ${suite.path}', suite.config, suite.platform, () => suite,
142 0 : path: suite.path);
143 : }
144 :
145 0 : LoadSuite._(String name, this.config, SuitePlatform platform,
146 : void Function() body, this._suiteAndZone, {String? path})
147 0 : : super(
148 0 : Group.root(
149 0 : [LocalTest(name, Metadata(timeout: Timeout(_timeout)), body)]),
150 : platform,
151 : path: path);
152 :
153 : /// A constructor used by [changeSuite].
154 0 : LoadSuite._changeSuite(LoadSuite old, this._suiteAndZone)
155 0 : : config = old.config,
156 0 : super(old.group, old.platform, path: old.path);
157 :
158 : /// A constructor used by [filter].
159 0 : LoadSuite._filtered(LoadSuite old, Group filtered)
160 0 : : config = old.config,
161 0 : _suiteAndZone = old._suiteAndZone,
162 0 : super(old.group, old.platform, path: old.path);
163 :
164 : /// Creates a new [LoadSuite] that's identical to this one, but that
165 : /// transforms [suite] once it's loaded.
166 : ///
167 : /// If [suite] completes to `null`, [change] won't be run. [change] is run
168 : /// within the load test's zone, so any errors or prints it emits will be
169 : /// associated with that test.
170 0 : LoadSuite changeSuite(RunnerSuite? Function(RunnerSuite) change) {
171 0 : return LoadSuite._changeSuite(this, _suiteAndZone.then((pair) {
172 : if (pair == null) return null;
173 :
174 0 : var zone = pair.last;
175 : RunnerSuite? newSuite;
176 0 : zone.runGuarded(() {
177 0 : newSuite = change(pair.first);
178 : });
179 0 : return newSuite == null ? null : Pair(newSuite!, zone);
180 : }));
181 : }
182 :
183 : /// Runs the test and returns the suite.
184 : ///
185 : /// Rather than emitting errors through a [LiveTest], this just pipes them
186 : /// through the return value.
187 0 : Future<RunnerSuite?> getSuite() async {
188 0 : var liveTest = test.load(this);
189 0 : liveTest.onMessage.listen((message) => print(message.text));
190 0 : await liveTest.run();
191 :
192 0 : if (liveTest.errors.isEmpty) return await suite;
193 :
194 0 : var error = liveTest.errors.first;
195 0 : await Future.error(error.error, error.stackTrace);
196 : throw 'unreachable';
197 : }
198 :
199 0 : @override
200 : LoadSuite filter(bool Function(Test) callback) {
201 0 : var filtered = group.filter(callback);
202 0 : filtered ??= Group.root([], metadata: metadata);
203 0 : return LoadSuite._filtered(this, filtered);
204 : }
205 :
206 : @override
207 0 : Future close() async {}
208 :
209 0 : @override
210 : Future<Map<String, dynamic>> gatherCoverage() =>
211 0 : throw UnsupportedError('Coverage is not supported for LoadSuite tests.');
212 : }
|