-
Notifications
You must be signed in to change notification settings - Fork 9
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
Property testing for define-functions #426
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #426 +/- ##
=======================================
Coverage 86.54% 86.54%
=======================================
Files 43 43
Lines 19192 19192
Branches 19192 19192
=======================================
Hits 16609 16609
Misses 1118 1118
Partials 1465 1465 ☔ View full report in Codecov by Sentry. |
|
||
use crate::{random_expressions, PropValue, TypePrinter}; | ||
|
||
const DEFINE_PRIVATE_READONLY: [&str; 2] = ["define-private", "define-read-only"]; |
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 constant?
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.
How is it different from 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.
I had no idea this existed and yes, this is as bad.
#![proptest_config(super::runtime_config())] | ||
|
||
#[test] | ||
fn define_private_readonly( |
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 would you test two separate Clarity functions in the same test? If the test breaks, how are we suppose to know which of the two functions is wrong?
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 this is similar
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, it is as bad. No idea what would happen if the test failed.
let expr=format!("(begin {expr})"); | ||
let snippet=&format!(r#"({function} (func (a {})) {expr}) (func {v})"#,v.type_string()); |
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.
You did the opposite of what should have been tested for a function declaration: you should have generated any number of argument and a simple expression for the body. Your goal is to test function definitions, not begin
.
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.
Do you suggest that I should Ignore begin? I mean we should test intermediary expressions though. I'll take care of arguments
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.
You don't test intermediary expressions: begin
is an expression. You test a single expression which is a begin
.
let expr=format!("(begin {expr})"); | ||
let snippet=&format!(r#"(define-public (func (a {})) {expr}) (func {v})"#,v.type_string()); |
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.
Exact same issues as the previous test.
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.
See comment 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.
see reply above
} | ||
} | ||
|
||
fn ensure_last_response(last_value: PropValue, is_public_function: bool) -> PropValue { |
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.
Imagine being someone who didn't write this function. You read it's name, and the function tells you that it ensures that the last expression is a response.
- Why
last
? There is only one argument here that is a Value, why should we specify it's the last of something? - If the second argument is false, the function does nothing! You didn't ensure anything here.
- I actually had the idea before reading this function that
true
would create aok
value andfalse
anerr
, but not at all. There is no way to create anerr
.
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.
Good point. Will fix it
} | ||
} | ||
|
||
fn random_expressions( |
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.
Here is the same, imagine being someone who didn't write this. What does random_expression
even mean? Why are there 3 return values? limit
is limit of what? Why a random expression should care if it is a public function?
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.
Okay there are two approaches. Increase the duplication a bit and or pass some vars to remove duplication. limit
is maximum number of expressions being generated.
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.
Don't tell me what limit
is: write a comment or make it more explicit!
@@ -381,6 +382,63 @@ fn qualified_principal() -> impl Strategy<Value = Value> { | |||
}) | |||
} | |||
|
|||
fn value_to_string(value: &PropValue, to_response: bool) -> String { |
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.
Imagine being someone who didn't write this function. Why would we have this function when we know that a PropValue
implements Display
? Why converting a Value
to a String
would care if it's a response or not?
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 to ensure that last value is always "stringified" response when function is public. Public functions require last value to be response
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.
Where in the name do you understand that the stringified response is a response for a public function?
expected_val | ||
); | ||
} | ||
} |
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.
(Optional - I hope there is a unit test about it, bud didn't check)
Public functions have another property: if it returns an err
, a modification to a datamap should be discarded. This property could also be tested.
@Acaccia could you take a look at it again? |
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.
The tests for the functions definitions don't test at all if the arguments are accessible. They should be used in the function body.
There are no tests to prove that any type could be returned from private and read-only functions.
Same for public function, it could show more kind of return types than (response int).
@@ -24,16 +24,12 @@ fn begin_strategy() -> impl Strategy<Value = (String, PropValue, bool)> { | |||
expressions.push(' '); | |||
} | |||
|
|||
expressions.push_str(&s); | |||
expressions.push_str(&format!("{v}")); | |||
} | |||
|
|||
let last_value = values.last().unwrap().clone(); |
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.
You could simplify this function a lot with the usage of split_last
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.
Simplify code a bit, but repetition of 2 statements. Fixed though
@@ -24,16 +24,12 @@ fn begin_strategy() -> impl Strategy<Value = (String, PropValue, bool)> { | |||
expressions.push(' '); | |||
} | |||
|
|||
expressions.push_str(&s); | |||
expressions.push_str(&format!("{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.
No need for a macro here, that's a to_string
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 thought macros are faster.
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 of course not! Macros add steps in the compilation process to create the code that will be placed here, and then this code still has to be executed in the end.
fn generate_begin_expressions( | ||
maximum_expressions: usize, | ||
) -> impl Strategy<Value = (String, PropValue, bool)> { | ||
prop::collection::vec(PropValue::any(), 1..=maximum_expressions).prop_map(move |values| { |
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.
If this is used only for begin
, shouldn't this function be inlined in the begin test? That's what I said in the previous PR here
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 got the idea that you're only interested in name change. I'm not sure what do you think of lengthy functions and separation of concerns. What I thought that building expressions isn't the test's job. I know it's not being used anywhere else but aren't they only tradeoffs?
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 separation of concern are you talking about here? It's literally
fn _foo() {
// super
// long
// body
}
pub fn foo() {
_foo()
}
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.
Alright. How do you make a longer function readable? I mean there are examples which splits a complex function into smaller functions just to make code readable. Those functions aren't used anywhere. There is one problem with this though. Users debugging the code might lose context if they are too many.
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 20 lines function should normally be readable, nothing special. You can always add comments to describe each sections of your functions if you feel like it could make the reading easier.
proptest! { | ||
#![proptest_config(super::runtime_config())] | ||
|
||
#[test] | ||
fn define_public_ok( | ||
(arguments, parameters) in generate_random_function_arguments(20) | ||
) { | ||
let snippet=&format!("(define-public (func {arguments}) (ok 1)) (func {parameters})"); | ||
|
||
crosscheck( | ||
snippet, | ||
Ok(Some(Value::Response(ResponseData { committed: true, data: Box::new(Value::Int(1)) }))) | ||
); | ||
} | ||
} | ||
|
||
proptest! { | ||
#![proptest_config(super::runtime_config())] | ||
|
||
#[test] | ||
fn define_public_err( | ||
(arguments, parameters) in generate_random_function_arguments(20) | ||
) { | ||
let snippet=&format!("(define-public (func {arguments}) (err 1)) (func {parameters})"); | ||
|
||
crosscheck( | ||
snippet, | ||
Ok(Some(Value::Response(ResponseData { committed: false, data: Box::new(Value::Int(1)) }))) | ||
); | ||
} | ||
} |
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.
For me, those tests are completely identical: you define a public function and return a response type.
I fail to see a reason why one of those tests could succeed and one could fail.
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.
Then I failed to understand this
(Optional - I hope there is a unit test about it, bud didn't check)
Public functions have another property: if it returns an err, a modification to a datamap should be discarded. This property could also be tested.
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.
You don't make any datamap modification here. I didn't even realize that was suppose to address this comment.
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.
Alright. A little misunderstanding.
s.push(':'); | ||
s.push(' '); // Space required after colon in tuple as function argument |
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.
should be merged into a push_str
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.
fixed
I'm not sure I understood this. Consider following example
|
Your tests should make sure that all the function's arguments are accessible. I would personally write a function that takes any number of arguments and returns a tuple which uses them all. I would also make a function that can take also any number of random arguments and returns a randomly generated value. For the public function, you can generate a random response value like this: |
@Acaccia thanks for the details. Hopefully it will be good now. |
closes #289