-
Notifications
You must be signed in to change notification settings - Fork 544
react: Express callback expectations precisely after each set_value
#434
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
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>>,
} |
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.
guess that worked, and opens up a few possibilities of wanting to save lines
exercises/react/tests/react.rs
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think that would be natural.
exercises/react/tests/react.rs
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessary, given the below line, but maybe helpful for a better error message???
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.
Sure; to present a human-friendly error message in test code is definitely worth an extra line and a single repeated branch.
exercises/react/tests/react.rs
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also possible use of https://doc.rust-lang.org/std/cell/struct.RefCell.html#method.replace
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.
Agreed.
exercises/react/tests/react.rs
Outdated
@@ -99,20 +99,42 @@ fn error_setting_a_compute_cell() { | |||
assert!(reactor.set_value(output, 3).is_err()); | |||
} | |||
|
|||
use std::cell::RefCell; | |||
struct Expector { |
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.
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 |
exercises/react/tests/react.rs
Outdated
@@ -99,20 +99,41 @@ fn error_setting_a_compute_cell() { | |||
assert!(reactor.set_value(output, 3).is_err()); | |||
} | |||
|
|||
struct Expector { | |||
value: std::cell::Cell<Option<isize>>, |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
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.
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.
exercises/react/tests/react.rs
Outdated
value: std::cell::Cell<Option<isize>>, | ||
} | ||
|
||
/// A CallbackRecorder helps tests whether callbacks get called correctly. |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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_value
Problem statement:
Consider a test with two
set_value
calls 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_value
call. For all we know, maybe an implementation:on the first
set_value
call, with the correct value.set_value
call, but twiceon the second
set_value
call.To more precisely define the
set_value
expectations, 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_value
now 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
Expector
class? 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.