Skip to content
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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

ameeratgithub
Copy link
Collaborator

closes #289

Copy link

codecov bot commented Jun 20, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 86.54%. Comparing base (13792d0) to head (35f129f).
Report is 2 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.


use crate::{random_expressions, PropValue, TypePrinter};

const DEFINE_PRIVATE_READONLY: [&str; 2] = ["define-private", "define-read-only"];
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

Copy link
Collaborator

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(
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

Copy link
Collaborator

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.

Comment on lines 20 to 21
let expr=format!("(begin {expr})");
let snippet=&format!(r#"({function} (func (a {})) {expr}) (func {v})"#,v.type_string());
Copy link
Collaborator

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.

Copy link
Collaborator Author

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

Copy link
Collaborator

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.

Comment on lines 46 to 47
let expr=format!("(begin {expr})");
let snippet=&format!(r#"(define-public (func (a {})) {expr}) (func {v})"#,v.type_string());
Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above

Copy link
Collaborator

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 {
Copy link
Collaborator

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.

  1. Why last? There is only one argument here that is a Value, why should we specify it's the last of something?
  2. If the second argument is false, the function does nothing! You didn't ensure anything here.
  3. I actually had the idea before reading this function that true would create a ok value and false an err, but not at all. There is no way to create an err.

Copy link
Collaborator Author

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(
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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 {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

Copy link
Collaborator

@Acaccia Acaccia Jun 20, 2024

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
);
}
}
Copy link
Collaborator

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.

@ameeratgithub
Copy link
Collaborator Author

@Acaccia could you take a look at it again?

@smcclellan smcclellan requested review from Acaccia and csgui June 20, 2024 17:41
Copy link
Collaborator

@Acaccia Acaccia left a 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();
Copy link
Collaborator

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

Copy link
Collaborator Author

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}"));
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

Comment on lines 8 to 11
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| {
Copy link
Collaborator

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

Copy link
Collaborator Author

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?

Copy link
Collaborator

@Acaccia Acaccia Jun 21, 2024

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()
}

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

Comment on lines 58 to 88
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)) })))
);
}
}
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. A little misunderstanding.

Comment on lines 505 to 506
s.push(':');
s.push(' '); // Space required after colon in tuple as function argument
Copy link
Collaborator

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@ameeratgithub
Copy link
Collaborator Author

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).

I'm not sure I understood this. Consider following example

>> (define-public (func) 1)
<stdin>:1:1: error: public functions must return an expression of type 'response', found 'int'

@Acaccia
Copy link
Collaborator

Acaccia commented Jun 21, 2024

@ameeratgithub

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.
-> This test would prove that the function can take any number of arguments of any type and use them all in the function body.

I would also make a function that can take also any number of random arguments and returns a randomly generated value.
-> This test would prove that a function can return any type.

For the public function, you can generate a random response value like this:
response in (prop_signature(), prop_signature()).prop_flat_map(|(ok_ty, err_ty)| PropValue::from_type(TypeSignature::ResponseType(Box::new((ok_ty, err_ty)))))

@ameeratgithub
Copy link
Collaborator Author

@Acaccia thanks for the details. Hopefully it will be good now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Property testing for Binding related functions
2 participants