-
Notifications
You must be signed in to change notification settings - Fork 218
Return a Future from expect(). #529
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
Conversation
This also adds a (private) AsyncMatcher class that makes it easier to define asynchronous matchers, and generally improves the formatting of those matchers.
buffer.writeln(indent(prettyPrint(expected), first: 'Expected: ')); | ||
buffer.writeln(indent(prettyPrint(actual), first: ' Actual: ')); | ||
if (which.isNotEmpty) buffer.writeln(indent(which, first: ' Which: ')); | ||
if (reason != null) buffer.writeln(reason); |
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.
did you want to indent the "reason" line too?
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, the extra whitespace is just to visually verify that the first
prefixes are all the same width.
Invoker.current.addOutstandingCallback(); | ||
// Avoid async/await so we synchronously start listening to [item]. | ||
/*FutureOr<String>*/ matchAsync(item) { | ||
if (item is! Future) return "was not a Future"; |
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.
why this string here but null below?
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.
A string means failure, null
means success. It's a failure if the item isn't the right type, but if _matcher
is null that just means that this came from completes
and doesn't care about the actual completion value.
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.
gotcha. can you add that to the comments?
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 in the AsyncMatcher.matchAsync()
docs—is it really worth duplicating in the subclasses?
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.
oh. right. I can read. Carry on then!
/// [size] defaults to `first.length`. Otherwise, [size] defaults to 2. | ||
String indent(String string, {int size, String first}) { | ||
size ??= first == null ? 2 : first.length; | ||
return prefixLines(string, " " * size, first: first); |
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.
hey I didn't know we could do the " " * n thing in dart, too! Cool!
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.
Yeah, it's so handy it seems out-of-place 😝.
lib/src/utils.dart
Outdated
if (lines.length == 1) return "$single$text"; | ||
|
||
var buffer = new StringBuffer("$first${lines.first}\n"); | ||
for (var line in lines.skip(1).take(lines.length - 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 line with the skip and take len - 2 is a little complex. Consider adding a comment describing why it's like this
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.
Done.
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.
approve after one comment addition
Invoker.current.addOutstandingCallback(); | ||
// Avoid async/await so we synchronously start listening to [item]. | ||
/*FutureOr<String>*/ matchAsync(item) { | ||
if (item is! Future) return "was not a Future"; |
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.
gotcha. can you add that to the comments?
This interacts really poorly with http://dart-lang.github.io/linter/lints/unawaited_futures.html When should/shouldn't a caller |
@nex3 Thoughts on this? I saw many things break. Could this be a generic method maybe? Then matchers can opt to have a return value or –– or something... |
It seems like a flaw in the lint if it can't handle Futures that don't need to be listened to.
It returns a
Do you mean dart-lang/sdk#57503? That's just a bug that I'll fix. This should be a fully backwards-compatible change in terms of Dart semantics.
This would be a breaking change both for |
If we ignore the lint this seems like a usability problem - We've had folks ask in gitter whether they should be |
It's not just lints.
```
void expectFoo() => expect('foo', isNotNull);
```
Is a non-lint error because you're (now) trying to return a Future from a
void function.
…On Mon, Feb 13, 2017 at 1:45 PM, Nate Bosch ***@***.***> wrote:
If we ignore the lint this seems like a usability problem - We've had
folks ask in gitter whether they should be awaiting the Future. Unless
we're prepared to tell people "Every expect should have an await" there
is going to be a lot of confusion.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#529 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AABCinriwq5cPSFgWWggaObQbpWFwhF7ks5rcM73gaJpZM4L0XfK>
.
|
What is a case where you do want to await? |
+1. This is going to make practically every internal user unhappy. I'm not comfortable with this landing. |
I'm happy to update the method documentation if you have suggestions. The API is necessary, though—stream matchers aren't usable without it.
I've always considered adding a return type to a previously I've filed #28763 to track the broader issue.
Any time you want to wait until the matcher is complete. For example, you might await |
Given that we likely can't address the SDK issue in the short term what is our plan? |
My suggestion is to address dart-lang/sdk#28763 ASAP in Google3 so we can start rolling |
We could back out this change – or make |
The SDK issue is very unlikely to be solvable in the short term - we're not going to find a solution very many people are happy with. If we consider ourselves blocked on that I suspect it's going to be a long time before we can roll test internally. |
Both backing out the change and adding a generic method annotation would themselves be backwards-incompatible. Making This is also a key aspect of test's support for testing asynchronous APIs. This is the right API for doing so, and if I had to increment the major version to make this change I'd do it. We need stream matchers, and we need them to integrate well with
Why? What's to be unhappy about with loosening the restriction on using If fixing the language issue isn't workable in the short term, we should find a way to auto-refactor the Google3 codebase to change |
What are your thoughts on a separate |
That seems pretty confusing, since the various asynchronous matchers are totally usable with |
We're already backwards incompatible in the external world for any code using the pattern mentioned right? I think we should separately consider the API we want to end up with and the path we take to get there - apologies that I've been threading those two conversations throughout. Are there examples of code that are mixing and matching between async and non-async matchers? I'd be curious to play around with what it would look like if we forced them to be explicitly chosen. It'd be good to get more data on what is more confusing - picking the right We should also experiment with a generic method... though I suspect the current flexibility of taking a value or a matcher is very likely to rule that out. |
Tragically yes, but that was unintentional and we've already paid much of the (external) cost for it. This would be an intentional backwards incompatibility. Also, because
I'm confident that the API in the most recent release is the API we should end up with. The documentation could probably use a little work, though.
All the code I know of that would use this pattern hasn't been ported away from |
What data are you using? It's come up externally that the question of whether to
Is that evidence that little code will be impacted by the breaking change of rolling this back? |
Usage of the |
Some data so we can decide if we want to ignore the unawaited_futures lint: Internally ~35% of projects using linting are using unawaited_futures, ~15% of projects with an On github it's harder to tell, ~15% of projects using linting have "unawaited_futures" in their analysis_options but some fraction of that is commented out to disable the lint. |
I'm using my years of highly successful API design experience. I don't believe there's an effective way to empirically measure quality design: it's much more of an art than a science. I'm happy to listen to feedback though—in this case, I plan to improve the documentation to explain more clearly when an |
I find the |
+1. It's caught real bugs in our codebase in the past, but with the update to |
I'm confident we can get the linter to work well with this change soon. |
It looks like this may have caused one of the fake_async tests to start failing in quiver. Wondering how broadly this'll affect users of that or similarly zone-reliant tests. |
If this interacts any differently with zones than the old version, that's a bug. Let me know if you can find a reproduction. |
Confirmed that it seems to be interacting with zones differently. Similar to the fake_async failures, I discovered that this change causes Flutter tests to fail because they make expectations about the number of pending microtasks in the queue -- and now |
Repro:
Result:
Then edit pubspec.yaml to use package test 0.12.18, pub get, and re-run:
It's entirely possibly this is a bug in FakeAsync, or if not, that we could put special-case logic in for |
@tvolkert That's not a safe assertion to make in general—microtasks are an implementation detail, and any library (including core libraries) could queue more or less at any time. The event loop should be considered a black box. @cbracken I can chase that down if I need to, but it would be really helpful if you could find a standalone reproduction. |
Here, we're trying to test the implementation, and since we are the Dart VM embedder, such details are very valid to test. |
I suppose, but you're testing something very contingent on the implementations of every library those tests touch, which means you'll need to be prepared to update those assertions when you get new upstream versions. |
The problem is that we are mostly trying to test whether a microtask got queued at all or not. If we start having to verify whether a single microtask got queued or not that gets much harder to maintain. |
Would it be possible to have an We're already wrapping |
@nex3 reduced repro: import 'dart:async';
import 'package:test/test.dart';
class FakeAsync {
final _microtasks = [];
Zone _zone;
int get microtaskCount => _microtasks.length;
run(callback(FakeAsync self)) {
if (_zone == null) {
_zone = Zone.current.fork(specification: new ZoneSpecification(
scheduleMicrotask: (_, __, ___, Function microtask) {
_microtasks.add(microtask);
}));
}
return _zone.runGuarded(() => callback(this));
}
}
main() {
test('count pending tasks', () {
new FakeAsync().run((fakeAsync) {
expect(fakeAsync.microtaskCount, 0);
scheduleMicrotask(() => null);
expect(fakeAsync.microtaskCount, 1);
// passes under test 0.12.18
// fails under test 0.12.19: expected 1, actual 2
});
});
} Under the assumption that In particular, there may be some test frameworks that test that try to blast through the set of pending async tasks until there are none, then test that certain conditions are met. If the test expectations are also queued up in there, this could be problematic. |
One thing to note is that Flutter can't actually await the Future that's returned by This is quite intentional, because we want to test code that uses futures but we want to very carefully step them through state machines in controlled ways (so we can verify different potential race conditions, for example). We also use the lack of any microtasks as a signal that we're not leaking asynchronous work in certain cases; the code that verifies that doesn't know if you've called |
That's kind of the position you're in if you're writing a test like that... making assertions about implementation details is never going to be easy to maintain. I know the core Dart SDK platform tests avoid the That said, in this case in particular since we're just returning @cbracken If the issue is just with a microtask counter, the answer's the same as for Flutter: these tests are relying on implementation details of the libraries they're using and the runtime, and should expect to be broken occasionally. |
No description provided.