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

Bool assert idea #3828

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

Bool assert idea #3828

wants to merge 1 commit into from

Conversation

lpil
Copy link
Member

@lpil lpil commented Nov 14, 2024

What do you think?

rendered

@lpil lpil marked this pull request as ready for review November 14, 2024 12:10
@lpil
Copy link
Member Author

lpil commented Nov 14, 2024

@hayleigh-dot-dev and @giacomocavalieri have raised concerns with people using assert in libraries to check preconditions in functions.

I feel that introducing and documenting the feature purely in the context of testing would likely get us where we want to be, looking at Erlang and Elixir and Rust as examples of this succeeding elsewhere. I suspect most folks don't know you can even use their assertions outside of tests.

As additional encouragement we can also add another layer of nagging to gleam publish, and show when a package panics on the package index or some other documentation.


It was suggested we could ban panicking in published libraries, but there are lots of valid reasons to panic in libraries, even if typically it is discouraged and a bad idea. For example, OTP based code.


It was suggested that let assert True = could be upgraded to have all these features, but it's unwieldy to type and to read, and it fails the strangeness budget test for simple assertions, so it doesn't meet the goal of improving Gleam's test DX.

@giacomocavalieri
Copy link
Member

giacomocavalieri commented Nov 14, 2024

It was suggested we could ban panicking in published libraries, but there are lots of valid reasons to panic in libraries, even if typically it is discouraged and a bad idea. For example, OTP based code.

Do you have any examples? Looking at gleam_otp the only place where it's used is doing some let assert Ok(thing) = ... and couldn't really be replaced with anything else.

In the tests and examples there's a couple of places doing let assert True = ...:

- let assert True = process.is_alive(p1)
+ assert process.is_alive(p1)

And there's also a couple of other assertions that could be rewritten but I'm not sure I like it:

- let assert Error(Nil) = process.receive(subject, 10)
+ assert result.is_error(process.receive(subject, 10))
- let assert Error(Timeout) = task.try_await(t3, 35)
+ assert Error(Timeout) == task.try_await(t3, 35)

What would become the preferred style in those cases? If we're not making let assert carry around the same kind of info assert does I can see people gravitating towards the latter just because it would produce more detailed error messages.
This is just my opinion but I think it's a net negative to favour bool returning functions instead of pattern matching and let assert.

@lpil
Copy link
Member Author

lpil commented Nov 14, 2024

I don't have examples to hand, but it would be any case there a precondition is checked.

The error checking example you have there should be rewritten like this:

assert process.receive(subject, 10) == Error(Timeout)

Now == is always used for equality rather than you choosing between different approaches depending on what error message you want and how much boilerplate you are willing to tolerate.

@giacomocavalieri
Copy link
Member

But then if I want to change it in the future and assert that it's just an error do you think it would be easier to go this way:

- assert process.receive(subject, 10) == Error(Timeout) 
+ let assert Error(_) = process.receive(subject, 10)

Or this way:

- assert process.receive(subject, 10) == Error(Timeout) 
+ assert process.receive(subject, 10) |> result.is_error

To me the second one feels encouraged.

And it's a bit unexpected that writing let assert Error(Nil) = wibble will produce a different and worse error message than assert Error(Nil) == wibble.
What should happen to all let asserts that check a pattern that could be rewritten as an assert?

@lpil
Copy link
Member Author

lpil commented Nov 14, 2024

To me the second one feels encouraged.

Why do you think that? To use equality rather than a helper function is most common way to write that test both in Gleam and the other languages I'm familiar with.

And it's a bit unexpected that writing let assert Error(Nil) = wibble will produce a different and worse error message than assert Error(Nil) == wibble.

Perhaps unintuitive, but easy to learn, and it's the norm and doesn't cause problems for any of the other languages referenced. Equality is the simpler and more common feature so I don't think people would default to skipping over it in favour of the more complex partial pattern matching.

@jhillyerd
Copy link

jhillyerd commented Nov 14, 2024

I'm in favor of this. I think the existing gleeunit/should module is clunky to use, and find it frustrating that it does not accept custom error/context messages. This proposal allows for concise, readable, and flexible test assertions. (edit: and I like that the assertions are just gleam, rather than a DSL)

I don't think it needs to be blocked from use in non-test code, but I do expect it will lead to folks requesting a "disable assertions in production builds" feature.

@bentomas
Copy link

To me the second one feels encouraged.

I think you can discourage the second one by having whatever updated test runner that replaces the current gleeunit use the left hand side as the expected value in messaging about failures.

@lpil lpil marked this pull request as draft November 14, 2024 19:48
@lpil lpil added the discussion The approach has not yet been decided label Nov 14, 2024
@sbergen
Copy link
Contributor

sbergen commented Nov 24, 2024

Would it be possible to pull in the argument names, instead of 1, 2, 3 in this case?

error: Lock required for user
test/myapp/user_test.gleam:45

    assert lock_user(username, 10, role)

1. "lucystarfish"
2. 10
3. Admin

@lpil
Copy link
Member Author

lpil commented Nov 24, 2024

Argument names are not part of the public API of a function.

@sbergen
Copy link
Contributor

sbergen commented Nov 24, 2024

Argument names are not part of the public API of a function.

How about labelled arguments?

@lpil
Copy link
Member Author

lpil commented Nov 24, 2024

What would you propose the exception data structure be in the presence of labels?

@sbergen
Copy link
Contributor

sbergen commented Nov 24, 2024

I don't really know enough about the internals of Gleam to gauge the feasibility of this at any level, but in addition to value and kind could there be an optional label in the expression map?

Copy link
Member

@GearsDatapacks GearsDatapacks left a 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 a very good idea, and the design is pretty near what we will eventually implement. I've left a couple of small notes to potentially consider inline.

This does have the file and line information, but has no message or information
about the values. We are planning to add support for a custom message to `let
assert`, at which point is could have the same amount of information as `case` +
`panic` approach, but with a slightly more concise syntax.
Copy link
Member

Choose a reason for hiding this comment

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

We have now added a custom panic message for let assert, so it might be worth rewording this paragraph with that in mind

| Key | Type | Value |
| --- | ---- | ----- |
| value | t1 | the value the expression evaluated to at runtime |
| kind | Atom | `literal` or `expression` |
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to include the original source code of the expression in here? So we can display stuff like age + 1: 27
I guess it may not be necessary if the entire asserted expression is displayed above, but something to potentially consider.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think the code would be ideal as it can be arbitrarily large, which could result in code size bloating.

It could have byte indexes perhaps? Currently it contains the source line. That with a parser would be enough to get the source code, but what's best I am unsure. Perhaps my suggestion below of including the actual code isn't ideal.

Reading this again I don't understand why there's kind: expression | literal. What does the programmer do with this information?

| gleam_error | Atom | See individual errors |
| message | String | See individual errors |
| module | String | The module the error occured in |
| function | String | The function the error occured in |
Copy link
Member

Choose a reason for hiding this comment

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

These tables seem to be written from the perspective of the representation on the Erlang target (e.g. Atom doesn't exist in JS). Is it worth clarifying how these will be represented on JS, or is it just assumed that we'll use the nearest JS equivalent of the Erlang-specific stuff?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup on JS it's the same but atom -> string

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion The approach has not yet been decided
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants