react: Express callback expectations precisely after each set_value#434
react: Express callback expectations precisely after each set_value#434petertseng merged 3 commits intoexercism:masterfrom petertseng:react-experiment
set_value#434Conversation
|
The possible outcomes that I expect from this discussion are the two that might immediately come to mind.
|
|
This tells me that this is possible in Rust, which allays my fears about making such tests canonical as I suggest in exercism/problem-specifications#1194 (comment) .I will probably decide it's better to be precise in the canonical data, even if the Rust track chooses not to do this. |
|
I need some more time to think this over; I do not yet have an opinion about this. I do, however, have a bit of dogshedding: it wasn't obvious to me at first what the distinction between struct Expector {
value: RefCell<Option<isize>>,
} |
petertseng
left a comment
There was a problem hiding this comment.
guess that worked, and opens up a few possibilities of wanting to save lines
| assert_eq!(*self.value.borrow(), v, "Callback was called with incorrect value"); | ||
| *self.have_value.borrow_mut() = false; | ||
| assert_ne!(*self.value.borrow(), None, "Callback was not called, but should have been"); | ||
| assert_eq!(*self.value.borrow(), Some(v), "Callback was called with incorrect value"); |
There was a problem hiding this comment.
I'd consider using https://doc.rust-lang.org/std/cell/struct.RefCell.html#method.replace in this line to remove the need for the line below it.
There was a problem hiding this comment.
Yes, I think that would be natural.
| assert!(*self.have_value.borrow(), "Callback was not called, but should have been"); | ||
| assert_eq!(*self.value.borrow(), v, "Callback was called with incorrect value"); | ||
| *self.have_value.borrow_mut() = false; | ||
| assert_ne!(*self.value.borrow(), None, "Callback was not called, but should have been"); |
There was a problem hiding this comment.
not necessary, given the below line, but maybe helpful for a better error message???
There was a problem hiding this comment.
Sure; to present a human-friendly error message in test code is definitely worth an extra line and a single repeated branch.
| assert!(!*self.have_value.borrow(), "Callback was called too many times"); | ||
| *self.value.borrow_mut() = v; | ||
| *self.have_value.borrow_mut() = true; | ||
| assert_eq!(*self.value.borrow(), None, "Callback was called too many times; can't be called with {}", v); |
There was a problem hiding this comment.
also possible use of https://doc.rust-lang.org/std/cell/struct.RefCell.html#method.replace
| } | ||
|
|
||
| use std::cell::RefCell; | ||
| struct Expector { |
There was a problem hiding this comment.
CallbackRecorder, maybe.
|
Ah, as much as I think this is a cool thing to have, I see that https://blog.rust-lang.org/2018/02/15/Rust-1.24.html tells me |
|
Now that I understand and https://blog.rust-lang.org/2017/04/27/Rust-1.17.html confirms the thought about Cell |
| } | ||
|
|
||
| struct Expector { | ||
| value: std::cell::Cell<Option<isize>>, |
There was a problem hiding this comment.
I'm only doing this because I thought If I just use cell here (with a use std::cell::Cell before, of course), what if a submitted implementation (which we use react::*) has a Cell ? will the conflicting names named in a use cause problems? I admit I never tried.
There was a problem hiding this comment.
It does not cause problems if Cell is specifically named:
mod m1 {
#[derive(Debug)]
pub struct A {
pub a: usize,
}
}
mod m2 {
#[derive(Debug)]
pub struct A {
pub a: usize,
}
}
use m1::*;
use m2::A;
fn main() {
println!("Hello, world! {:?}", A{a: 1});
}So, could go back to use std::cell::Cell, but perhaps I'll leave it this way for clarity in case someone doesn't know the rules. I didn't know off the top of my head.
|
I've finally had the chance to think about this for long enough to understand it. I think these changes improve the exercise:
I'm not approving in the Github sense largely because this PR is a demo, not yet complete. It doesn't apply to all tests. The most compelling objection is that this increases the cognitive load for students who want to understand how the tests work. My response to that is that this is a level 10 exercise, one of only three in the Rust track IIRC. Students who attempt this exercise are expected to be comfortable with advanced idioms, and they have the opportunity to learn something useful by studying this construction. Thank you, @petertseng, for coming up with this; it's very cool. I like it. |
My response here is that I hope the names of the functions should make this clear, but I also propose to add some comments to help. Commits coming that will use this for all the callback tests; I'll rely on GitHub's standard notifications for that. I do propose that I squash all these commits when merging. |
|
A note I've discovered: All So we'll have to live with the fact that the |
|
Feel free to ask for extra newlines to be added to make the tests easier to read. |
set_value
|
It is good manners for me to say that the improvement of more precisely saying which callbacks should be called for each |
|
After rebasing for #454 I believe that |
**Problem statement**: Consider a test with two `set_value` calls and which expects that a callback has, ultimately, been called with the two values, one for each `set_value`. The tests currently do not check that one value was added during each `set_value` call. For all we know, maybe an implementation: * magically manages to predict the future and calls the callback twice on the first `set_value` call, with the correct value. * calls the callback zero times on the first `set_value` call, but twice on the second `set_value` call. To more precisely define the `set_value` expectations, this commit uses a `Cell`-based implementation to test callbacks.
|
To avoid confusion on what I intend the final product's commit message to be (and to support another upcoming change to react), I've squashed down to one commit. If anyone wishes to see the old history, you may see at https://github.com/petertseng/exercism-rust/commits/e925ff28cba1abfc41956bb0382f265a886130ac. I do not believe any of that is worth keeping as separate commits since they were all exploratory. |
Maybe less confusion for students; they don't need to worry about callbacks until they actually run into those tests.
|
As you can see, I am proposing moving |
coriolinus
left a comment
There was a problem hiding this comment.
Notwithstanding my quibble about the documentation block, I think this looks good. Positioning the definition before its first use instead of before the first test improves usability for a student who is actually unlocking the tests one at a time.
| value: std::cell::Cell<Option<isize>>, | ||
| } | ||
|
|
||
| /// A CallbackRecorder helps tests whether callbacks get called correctly. |
There was a problem hiding this comment.
I do not know whether doccomments are parsed when they precede the impl block instead of the struct block. The documentation on documentation doesn't say. That said, my intuition is that this comment should probably move to the struct definition instead of the impl.
If it can be shown that cargo doc produces correct output when the documentation is attached to the impl block, I will rescind this comment.
|
I will not merge now, so as to allow potential movement of the documentation block. |
|
I am adding a commit to move the doc comment. I intend that these commits should all be squashed and will do so if the CI passes. |
|
Confirmed via pub struct hi {
a: usize,
}
/// hi is an impl
impl hi {
fn a() -> usize {
5
}
}
(but it does if you put |
|
correction: the index has no doc for The specific documentation page for For our case, it is better to put the doc on the struct. |
|
Although I did not rebase this, the stub has not changed and by pushing a new commit this had the effect of making Travis test the merge result (https://travis-ci.org/exercism/rust/jobs/362275367 tested commit 330f7d3 which merged 664236c, my commit, into ccc6f0fe471f68034ff6dd9f68401db027b2094dm, current master) |
| assert!(reactor.add_callback(output, |v| cb2.callback_called(v)).is_ok()); | ||
|
|
||
| assert!(reactor.set_value(input, 31).is_ok()); | ||
| cb1.expect_to_have_been_called_with(32); |
There was a problem hiding this comment.
Note as to how this applies to other languages:
Other languages would have been perfectly fine to use their language's equivalent of the old vector-based implementation, because they are free to check that the vector == [32] here. It was solely because I was unable to to do in Rust that I chose to use a Cell.
Other languages need not use their equivalent of this Cell-based implementation, but may still choose to if they feel it makes their tests more descriptive.
What all languages can indeed benefit from is testing that the callbacks were called with 32 at this point in the program (instead of waiting until the end), no matter how that is implemented.
react: Express callback expectations precisely after each
set_valueProblem statement:
Consider a test with two
set_valuecalls and which expects that acallback has, ultimately, been called with the two values, one for each
set_value.The tests currently do not check that one value was added during each
set_valuecall. For all we know, maybe an implementation:on the first
set_valuecall, with the correct value.set_valuecall, but twiceon the second
set_valuecall.To more precisely define the
set_valueexpectations, this commit usesa
Cell-based implementation to test callbacks.Discussion:
Is this clearer to students than the old vector-based approach? It is
certainly true that the expectations are closer to the line of code that
could cause the expectations to fail; each
set_valuenow shows exactlywhat callback calls are expected as a result of it. I would argue this
is does increase test clarity.
Some questions I will have:
implementation of the
Expectorclass? Any way to alleviate thatconfusion?
Expector. It was chosen arbitrarily simply becauseit is something that expects.
could not find a way.
It behooves me to explain why I could not. If the reason is already obvious to you because of your knowledge of Rust, you can probably skip this part, then.
If the callback mutably borrows something (say, to record what value it was called with), the rest of the test code cannot borrow that same thing to check that the values are correct. Same if we try to go vice-versa: If the test code mutably borrows something to signal to the callback "I am going to call set_value now so it is safe for you to let one more value through", the callback cannot have immutably borrowed it to check that signal. So it seems the only sharing the two can do is through both of them immutably borrowing something.
If they must both immutably borrow something, it seems that at least one of them will nevertheless need to mutate something.
Thus, it seems to be the case that we must achieve dynamic (runtime-checked, instead of compile-time-checked) borrowing, by using a RefCell.After experimentation, we can use a Cell, preferable because now there is no dynamic borrowing.