-
Notifications
You must be signed in to change notification settings - Fork 28
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
#2398. Remove excessive async. Add explicit void
#2400
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks.
Removing redundant async
s LGTM.
I'm not sure about the placements of asyncEnd
s. I've added inline questions/comments.
I may make reviewing easier to separate simple changes (like the async
removals here) from stuff that needs careful reviews (the asyncEnd
s) as otherwise it becomes too easy to miss important details in the large diffs.
await Process.run( | ||
executable, [...Platform.executableArguments, eScript, "run"]) | ||
.then((ProcessResult results) { | ||
Expect.equals(0, results.exitCode); | ||
called++; | ||
asyncEnd(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this asyncEnd
be called after the Expect.equals(1, ...)
below? My understanding is unittest-suite-success
should be printed as the last thing, and there's an assertion that will run after this line. (the Expect.equals(1, ...)
on line 30)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's Ok to place it there. The idea is to make sure that async method was called and only once. For example
import "dart:async";
import "../../Utils/expect.dart";
main() async {
asyncStart();
await Future<int>.delayed(Duration(seconds: 1), () {return 42;}).then((value) {
print(value);
asyncEnd(); // Ok. It works, no matter that anything is printed after unittest-suite-success
print(0);
});
}
The test above will fail if, for some reason, Future.then()
won't be called (then test runner determines asyncStart()
without asyncEnd()
) or will be called more than once (number of asyncEnd()
calls should be equal the number of asyncStart()
calls).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that's the correct usage of asyncEnd
. The test controller that runs in the browser declares a pass when it sees a unittest-suite-success
: https://github.com/dart-lang/sdk/blob/12e52d8fde254a0aad0102223324f7aa953146c3/pkg/test_runner/lib/src/test_controller.js#L242-L244
So if you call asyncEnd
and then do other checks, those checks will potentially run after the test result is sent back to the test driver and the test result is accepted as "pass".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed this with @mkustermann. The key points are that (1) there should be no assertions after the last asyncEnd
(when the async task counter hits), and (2) asyncEnd
should never be called when there are more asyncStart
s to be run in the program.
As an example to (1), here we can't call asyncEnd
in the awaited future:
main() async {
asyncStart();
await Future<int>.delayed(Duration(seconds: 1), () {
... // We can't call `asyncEnd` here
});
Expect.equals(...);
asyncEnd();
}
This is because the part after the await
will run after the future completes, so if we call asyncEnd
in the future that concludes the test before the rest of the main
runs.
If we didn't have await
:
main() {
asyncStart();
Future<int>.delayed(Duration(seconds: 1), () { // no `await` here
...
asyncEnd();
return 123;
});
Expect.equals(...);
}
Now we need to call asyncEnd
in the future because the future will complete after main
returns (as main
does not wait for the future to complete).
As an example to (2), this is a bug:
main() async {
asyncStart();
test1();
asyncEnd(); // BUG: the test result will be reported here.
asyncStart();
test2();
asyncEnd();
}
Here the bug is that the first asyncEnd()
decrements the async task counter to 0 and prints 'unittest-suite-success', which again concludes the test early.
For this particular test (1) applies, the asyncEnd
should be after the Expect.equals
in main
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@osa1 and @mkustermann, it's great that this topic gets clarified, thanks!
A few things that I noticed in language tests using asyncStart(...)
and asyncEnd()
and from looking at the declarations of asyncStart
and asyncEnd
:
asyncStart(k)
can be used to get the same effect as calling asyncStart()
k times; this is never used.
Dynamically, the invocations of asyncStart()
and asyncEnd()
must determine a correct parenthesis structure. That is, if we use (
to indicate that asyncStart()
has been invoked and )
to indicate asyncEnd()
, we're allowed to have invocation histories like '()' or '((()))' or '(()()()(()))', but not ')' or '(()()))('. In other words, for every asyncEnd()
there must be a corresponding asyncStart()
in the history, and the total execution history must have a corresponding asyncEnd()
for every asyncStart()
. The final asyncEnd()
is special in that it posts the 'unittest-suite-success' message which is interpreted by processMessage
in 'test_controller.js'.
A common pattern in tests is to have an outer pair of asyncStart/asyncEnd
invocations, and then possible a bunch of inner pairs. E.g., in 'language/await/await_test.dart':
main() {
asyncStart();
for (int i = 0; i < 11; i++) {
asyncTest(staticMembers);
...
}
asyncEnd();
}
The asyncStart()
is typically performed directly at the top level (here: of the body of main
), and the asyncEnd()
is usually performed in a callback, e.g., in 'language/await/postfix_expr_test.dart':
main() {
asyncStart();
test().then((_) { asyncEnd(); });
}
In the case where asyncEnd()
is invoked at the top level it is typically guarded by a synchronization command (like await completer.future
in 'async_star/async_star_cancel_test.dart'). In any case, some amount of synchronization may be required in order to ensure that the parenthesis structure is correct at run time, and also to ensure that the final asyncEnd()
occurs after all elements of the test have been performed.
It may seem more visually appealing to have asyncStart()
and asyncEnd()
occur symmetrically: They both occur in the body of the same function, with the same amount of indentation. However, the approach where asyncEnd()
is called in an argument to .then
of a future may be simpler because there is no need to write extra code to achieve the required synchronization. So the style from 'postfix_expr_test.dart' could be a useful standard.
When we have a top-level asyncStart/asyncEnd
pair (say, in main
), we're free to have nested pairs, as shown in @mkustermann's example in the comment on 'element_type_A02_t03.dart'. This is like '(())'; however, as @mkustermann mentions, we're actually running the asyncStart()
in main
, then the asyncStart()
in isRuntime...
, then the asyncEnd()
in main
and finally asyncEnd()
in isRuntime...
. In other words, the parenthesis structure of the dynamic invocation sequence exists, but it doesn't reflect the syntactic location of the invocations of asyncStart
and asyncEnd
.
I think it might be helpful for readers of the code if the invocations of asyncStart
and asyncEnd
that end up matching each other in the dynamic invocation sequence were also the ones that are located together syntactically. Anyway, that's good for reasoning about the sequence of events, but it isn't a requirement for passing the test run.
We can also have a structure that looks like ()()()()()
, as shown in 'async/async_test.dart'. The actual sequence of events in that case is probably ((((()))))
because the top-level statement list in main
is executed without suspension, and then all the callbacks (function literals passed to .then
) are executed after main
has returned. Again, this works, but the actual sequencing of events is not necessarily self-explanatory.
@osa1 wrote that @mkustermann said:
asyncEnd
should never be called when there are moreasyncStart
s to be run in the program.
I do think 'async/error_timing_test.dart' will do this, and this test succeeds on 79 out of 83 configurations at this time. I think the actual requirement is that the parenthesis structure is maintained. This basically means that we aren't executing too many asyncEnd()
too early, and at the end we have executed exactly the same number of asyncStart()
and asyncEnd()
, and that balance is not achieved at any time before the end (because that would cause a spurious 'unittest-suite-success'). It is not a problem if we execute asyncEnd()
followed by asyncStart()
, as long as we maintain this parenthesis structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asyncEnd should never be called when there are more asyncStarts to be run in the program.
I do think 'async/error_timing_test.dart' will do this, and this test succeeds on 79 out of 83 configurations at this time. I think the actual requirement is that the parenthesis structure is maintained.
You're right, the rule I mentioned more strict than necessary. The parenthesis rule mostly makes sense to me, but
We can also have a structure that looks like ()()()()(),
Here the first ')' will just end the test. There needs to be an outer parens like (()()()()())
.
@osa1 wrote that @mkustermann said:
To clarify, I wasn't quoting Martin. I just wrote my understanding from our discussion. All mistakes are mine 😅 .
I think we just can't make the asyncStart
/asyncEnd
have the same syntactic and runtime order while still testing everything we want to test. The example in my previous comment:
main() {
asyncStart();
Future<int>.delayed(Duration(seconds: 1), () {
...
asyncEnd();
return 123;
});
Expect.equals(...);
}
I agree that it would be good if asyncStart
and end
s occured symmetrically and in the same lexical scope, but I'm not sure if that's possible in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@osa1 wrote, in response to a remark from me:
We can also have a structure that looks like ()()()()(),
Here the first ')' will just end the test. There needs to be an outer parens like (()()()()()).
Right, that's the reason why I said "looks like" and then continued to note that the actual sequencing in that test ('async/async_test.dart') is ((((()))))
, in spite of the fact that it looks like ()()()()()
syntactically. The point I was trying to make was that the syntax can look nice and still be misleading. In this case, 'async_test.dart' can succeed even though it doesn't syntactically have an extra pair of asyncStart()
/asyncEnd()
around the whole thing, but it just takes one await
to break that.
Actually, we can have a main
like this and get in trouble:
void main() {
asyncStart();
... // Perform the actions
asyncEnd();
}
This test will fail with an 'asyncStart() was called even though we are done with testing' error if all the actions are delayed, because we will then invoke asyncStart()
(the first one) at the beginning of main, then asyncEnd()
(the one that is considered to mark the end of testing) at the end of main
, and then a bunch of other asyncStart()
/asyncEnd()
pairs during the execution of various callbacks after the control has returned from main
—except that the test will fail soon after main
returns. This can easily happen, say because the actions are async
functions that are called in main
and whose returned futures are discarded.
I tend to think that the safe approach would be to aim for the following style, in all functions used by a test, with main
as just one example:
// One approach seen in language tests.
main() {
asyncStart();
test().then((_) { asyncEnd(); });
}
// Alternatively.
Future<T> test() async {
asyncStart();
await anotherTest();
asyncEnd();
}
So the idea is that asyncStart()
occurs "synchronously" at the top level, and the corresponding asyncEnd()
occurs in a callback to .then
; or, equivalently, asyncEnd()
occurs after code that await
s all futures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to have only the following two approaches
main() {
asyncStart();
test().then((_) { asyncEnd(); });
}
And if we have more checks
main() {
asyncMultyStart(2);
test().then((_) {
...
asyncEnd();
});
// More checks
asyncEnd();
}
This will ensure that async routine was called and will preserve from false-positive outcome in the case when we have additional checks after the first asyncEnd()
In the cases like
Future<T> test() async {
asyncStart();
await anotherTest();
asyncEnd();
}
and
void main() {
asyncStart();
await ...
asyncEnd();
}
What are the benefits of using asyncStart/End()
here? If, in the first case, test()
won't be called at all for some reasons, we'll never know about it. If anotherTest()
misteriously won't be called, pair of async guards won't catch it. For these two examples I prefer not use asyncStart/End()
at all. Am I missing anyhthing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the benefits of using
asyncStart/End()
here?
I would expect the test runner to see 'unittest-suite-wait-for-done' and expect 'unittest-suite-success', and it would be detected and reported as a test failure if we get the former and not the latter. This is needed in order to verify that we don't have portions of a test that are skipped entirely (such that there are some expected but missing asyncEnd()
invocations). Confirmed: the following test causes an test run failure and shows 'unittest-suite-wait-for-done' on stdout.
import 'package:async_helper/async_helper.dart';
void main() async => asyncStart();
There are many ways we could create a situation where some code isn't executed even though it's associated with a future: It could be deadlocked, the we could have done myFuture.ignore()
(or something with the same effect), etc. So I'd say that checking that we get enough invocations of asyncEnd()
does make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean comparing of
void main() async {
asyncStart();
await test();
asyncEnd();
}
vs
void main() async {
await test();
}
I still think that await
should discover all of the issues that may happen with test()
and async quards won't give much advantages. If test()
won't be executed, then shouldn't we expect a timeout in both cases (with and withoun guards)?
@@ -27,6 +28,7 @@ void isRuntimeTypeImplementsStream<T>(Object? o) async { | |||
List<T> list = await o.toList(); | |||
try { | |||
list.addAll(<T>[]); | |||
asyncEnd(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as my other comment, I think this should be the last line in main
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revisiting this after my other comment, it looks like this asyncEnd
is correct, because the part after the await
in this function will run after main
ends.
It would be helpful if someone else could confirm (maybe @lrhn or @mkustermann).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a correct change, but I'd write it differently (imho) somewhat more cleanly like this:
void isRuntimeTypeImplementsStream<T>(Object? o) async {
...
asyncStart();
List<T> list = await o.toList();
try {
list.addAll(<T>[]);
asyncEnd();
} catch (...) {
...
}
}
main() async {
asyncStart();
dynamic d = await foo();
FutureOr<Stream<int>> o = d;
isRuntimeTypeImplementsStream<int>(d);
asyncEnd();
}
This works because we're going to call asyncStart()
before any async operation. So the counter will go up to 2, then main
decreases it and then finally isRuntimeTypeImplementsStream
decreases it and reports success.
The code is easier to read as the asyncStart
and asyncEnd
are contained within functions and balanced.
@osa1 @mkustermann how can I reproduce the issue that we are going to fix? I did the following test import "dart:async";
import "../../Utils/expect.dart";
main() async {
asyncStart();
await Future<int>.delayed(Duration(seconds: 1), () => 42).then((value) {
print(value);
asyncEnd();
});
print("Check 2");
Expect.isTrue(false);
} This test has an assertion after
So, are we reaaly going to fix an existing problem? |
And just a note. We have main() async {
asyncMultyStart(2);
test1();
asyncEnd();
test2();
asyncEnd();
} The following also will work void isRuntimeTypeImplementsStream<T>(Object? o) async {
...
List<T> list = await o.toList();
try {
list.addAll(<T>[]);
asyncEnd();
} catch (...) {
...
}
}
main() async {
asyncMultiStart(2);
dynamic d = await foo();
FutureOr<Stream<int>> o = d;
isRuntimeTypeImplementsStream<int>(d);
asyncEnd();
} So, if the problem really exists the solution will be to use |
Ah... After more carefull look at import "dart:async";
import "../../Utils/expect.dart";
main() async {
asyncStart();
await Future<int>.delayed(Duration(seconds: 1), () => 42).then((value) {
print(value);
asyncEnd();
});
print("Wait...");
await Future.delayed(Duration(seconds: 2));
print("Wait done.");
Expect.isTrue(false);
} This test fails in VM (expected) but successes in Chrome. False-positive outcome. So, we are really have an issue here. @osa1, thank you for discovering this! But... @osa1 @eernstg @mkustermann isn't it a test controller issue? Shouldn't the test runner run the tests on VM and Web in the same way? |
The In the browser there's no "process" that can finish. In principle we could keep track of all the created microtasks in the browser and make the main function wait until they're all finished. I mentioned this a few days ago in dart-lang/sdk#52561 (comment). I don't know what guarantees we give today about the Dart main function compiled to Wasm or JS, but due to Hyrum's Law, there should be some users who depend on the current behavior of So it would be a rather significant change and take time and effort (assuming it's possible). (I will talk to the JS interop team to get their feedback on this) |
Ok. I'll update the tests and add appropriate comments to |
I still think it might make sense to remove the redundant |
Makes sense. Updated async tests in this PR to not give false-positive results on web platforms (yes, I know that io library is not supported on web, but, anyway, if we have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's still not quite right. I've added some inline comments.
Again for the third time, I think it makes sense to split this into two PRs, one with trivial changes (removing async
s) and one with the important changes. Currently we have to go through hundreds of files every time there's a revision.
LibTest/io/exit/exit_A01_t01.dart
Outdated
}); | ||
Expect.equals(1, called); | ||
asyncEnd(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need asyncStart
and end
in main
. When you call run_main
you create a future which creates a microtask. If the asyncMultiStart
line doesn't run synchronously (and runs in the microtask) then you'll potentially miss that print and conclude the test early. cc @mkustermann.
LibTest/io/exit/exit_A02_t01.dart
Outdated
}); | ||
if (called != 1) { | ||
throw new Exception("Called must be <1> but actually <$called>"); | ||
} | ||
Expect.equals(1, called); | ||
asyncEnd(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
}); | ||
Expect.equals(1, called); | ||
asyncEnd(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm with comments (that apply also to similar cases)
@@ -39,7 +41,9 @@ FutureOr<Stream<int>?> foo() async* { | |||
} | |||
|
|||
main() async { | |||
asyncMultiStart(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is somewhat confusing, as locally only reading main
one doesn't know where the second asyncEnd()
is. I'd really encourage you to follow my suggestion above: Any strait line code that triggers an async operation (e.g. await
, .then((_) {})
) should have an asyncStart
at the beginning and an asyncEnd
when the operations completes.
Usually there will be balanced asyncStart
and asyncEnd
in the same function, making it easily understandable.
@@ -22,7 +22,7 @@ main() async { | |||
await inSandbox(_main); | |||
} | |||
|
|||
_main(Directory sandbox) async { | |||
_main(Directory sandbox) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider explicitly adding void
to these functions.
@@ -22,7 +22,7 @@ main() async { | |||
await inSandbox(_main); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests are async tests, they will execute after _main
was invoked (the finally {}
clause in inSandbox
- which theoretically could throw)
@@ -20,14 +20,14 @@ Stream<List<int>> stream1 = new Stream<List<int>>.fromIterable( | |||
Stream<List<int>> stream2 = new Stream<List<int>>.fromIterable( | |||
[[1, 2, 3, 4, 5], [12], [3, 22]]); | |||
|
|||
test(Stdout sink, Stream<List<int>> stream) async { | |||
test(Stdout sink, Stream<List<int>> stream) { | |||
sink.addStream(stream).then((x) { | |||
new Future.delayed(new Duration(seconds: 3)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this line here? The test isn't using asyncStart()
so basically after main
returns we expect the test to be successfully done, so any delayed actions are ignored.
I think a slightly better test would be
Future test(Stdout sink) async {
final future = sink.addStream(Stream.fromIterable([[1, 2, 3, ]));
// While the `sink.addStream()` is in-progress we cannot touch the sink.
Expect.throws(() { sink.writeln(); }, (e) => e is StateError);
// After the `sink.addStream()` is done, we can write to it again.
await future;
sink.writeln();
}
main() async {
asyncStart();
await test(stdout);
await test(stderr);
asyncEnd();
}
void
All non-trivial changes reverted. They will be added separately (with respect to all comments made here). Now this PR contains only removing of excessive |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
@@ -11,7 +11,7 @@ | |||
import "dart:io"; | |||
import "../../../Utils/expect.dart"; | |||
|
|||
main() async { | |||
main() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's good, we shouldn't completely eliminate testing the case where main
does not have an explicit return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
I'm landing this PR. It changes the declaration of |
2023-12-01 49699333+dependabot[bot]@users.noreply.github.com Bump actions/setup-java from 3.13.0 to 4.0.0 (dart-lang/co19#2410) 2023-12-01 sgrekhov22@gmail.com dart-lang/co19#2398. Update async tests to avoid false-positive results on web. Language and LanguageFeatures tests (dart-lang/co19#2407) 2023-12-01 sgrekhov22@gmail.com Fixes dart-lang/co19#2408. Fix roll failures (dart-lang/co19#2409) 2023-11-30 sgrekhov22@gmail.com dart-lang/co19#2398. Update asyncStart/End() to correspond SDK version. Replace asyncMultiTest (dart-lang/co19#2406) 2023-11-30 sgrekhov22@gmail.com dart-lang/co19#2398. Remove excessive async. Add explicit `void` (dart-lang/co19#2400) 2023-11-28 sgrekhov22@gmail.com dart-lang/co19#2350. Update existing factory constructor tests. Part 1 (dart-lang/co19#2353) 2023-11-28 sgrekhov22@gmail.com Fixes dart-lang/co19#2390. Add expected error to static_analysis_extension_types_A30_t02.dart (dart-lang/co19#2401) 2023-11-28 sgrekhov22@gmail.com Fixes dart-lang/co19#2399. Update expected errors locations for CFE (dart-lang/co19#2402) 2023-11-24 sgrekhov22@gmail.com dart-lang/co19#2388. Rename and reorder static_analysis_member_invocation_A06_t* tests (dart-lang/co19#2397) Change-Id: Ie4b51caa12a9a0896c893cc02b099a07ef09fbd7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/339560 Reviewed-by: Alexander Thomas <athom@google.com> Reviewed-by: Erik Ernst <eernst@google.com> Commit-Queue: Erik Ernst <eernst@google.com>
No description provided.