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

RFC006: proposal on unit-testing and property-based testing for TLA+ #741

Merged
merged 28 commits into from
Apr 27, 2021

Conversation

konnov
Copy link
Contributor

@konnov konnov commented Apr 15, 2021

This is an RFC to discuss unit testing and PBT for TLA+. Once we agreed on that internally, we should get the feedback from a wider audience.

Compiled version in pdf.

To see the code in the RFC, run the following:

git clone https://github.com/informalsystems/apalache -b igor/rfc-unit && cargo install mdbook \
  && cd apalache/docs && mdbook serve
  • Tests added for any new code
  • Ran make fmt-fix (or had formatting run automatically on all files edited)
  • Documentation added for any new functionality
  • Entry added to UNRELEASED.md for any new functionality

Copy link
Contributor

@vitorenesduarte vitorenesduarte left a comment

Choose a reason for hiding this comment

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

It's great to see this! I'll do another pass later, but here are some initial comments. I really like the idea of apalache-mc example!!

docs/src/adr/006rfc-unit-testing.md Outdated Show resolved Hide resolved
docs/src/adr/006rfc-unit-testing.md Outdated Show resolved Hide resolved
test/tla/ChangRobertsTyped_Test.tla Outdated Show resolved Hide resolved
Comment on lines +82 to +84
\* restrict the contents with TypeOK,
\* so we don't generate useless data structures
/\ TypeOK
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to have a way to define generators that are useful and then invoke them here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you elaborate? Do you want to have generators in TypeOK?

\* Note that succ(n) is not referring to state variables,
\* so we can test it in isolation.
\*
\* @testStateless
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we go with @test instead of @testStateless? Or do we predict that @test could be used in the future for something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know. I think this is actually a rare case, when we have a stateless operator. When I was writing this text, it was not even clear to me, why cannot we directly tag succ with @require and @ensure.

Copy link
Contributor

@vitorenesduarte vitorenesduarte Apr 16, 2021

Choose a reason for hiding this comment

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

In the MBT for IBC, we have a single module changing the state: IBC
The remaining modules (ICS02 and ICS03) only contain stateless operators that are then invoked by the IBC one.

I don't know if this is a common idiom, however.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's reasonable to expect that people would write larger specs like this. So far, I have not seen it often. It may also be a difference between people who come from imperative and functional languages.

(*
* A test setup for ChangRobertsTyped.
*)
EXTENDS Integers, Apalache
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to import Apalache?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are using Gen, which is defined in Apalache. If you are not using Gen, you would not need it.

konnov and others added 4 commits April 16, 2021 11:06
Co-authored-by: Vitor Enes <vitorenesduarte@gmail.com>
Co-authored-by: Vitor Enes <vitorenesduarte@gmail.com>
Co-authored-by: Vitor Enes <vitorenesduarte@gmail.com>
@konnov
Copy link
Contributor Author

konnov commented Apr 16, 2021

I have added a section on testing executions

Comment on lines 110 to 123
(*************************** EXECUTION TESTS **********************************)
\* Execute a sequence of 5 actions, similar to TestAction_n0.
\* We test a final state with Assert_n0.
\* Additionally, every state in an execution is tested for Correctness.
\*
\* @require("ConstInit")
\* @require("Prepare_n0")
\* @invariant("Correctness")
\* @ensure("Assert_noWinner")
\* @testExecution(5)
TestExec_n0_n1 ==
\* in this test, we only execute actions by processes 1 and 2
\E self \in { 1, 2 }:
n0(self) \/ n1(self)
Copy link
Contributor

Choose a reason for hiding this comment

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

I really like this idea!

I wonder if it would be possible to somehow write the following:

IfExec(trace) ==
   /\ trace[0].action.name = "create_client"
   /\ trace[0].action.id = 1
   /\ trace[0].action.height = 3
   /\ trace[1].action.name = "update_client"
   /\ trace[1].action.id = 1
   /\ trace[1].action.height = 2

ThenExec(trace) ==
   /\ trace[0].outcome = "ok" 
   /\ trace[1].outcome = "invalid_height"

\* @require("IfExec")
\* @ensure("ThenExec")
\* @testExecution(2, "Init", "Next")
\* the execution doesn't have to start at `Init`;
\* instead, the execution can start from any state reachable from `Init` by applying `Next`

I don't think I'm using @require and @ensure correctly (i.e. as in the other examples), but I believe that it conveys the idea.

If something like this was supported, it could probably also be used to generate MBT tests in a much nicer way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you like to refer to the intermediate states in a trace? The idea was the you can talk about the variables of the first state and the last state. Otherwise, we are quickly approaching temporal logic :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@andrey-kuprianov what is your take on that? How far is it from the tests you need in MBT?

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you like to refer to the intermediate states in a trace?

Yes. My example is bad in that sense. I should have used a trace with more than 2 states.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think @shonfeder and you are asking about more or less the same thing. I don't like the idea of having trace explicitly. I guess what you are asking for is a temporal property. Instead of giving an explicit index, we could write A /\ []B /\ <>C. This would be the syntax for advanced users :-)

Copy link

@lemmy lemmy May 4, 2021

Choose a reason for hiding this comment

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

Related: Trace operator in CommunityModules https://github.com/tlaplus/CommunityModules/blob/4337afb74653cedcc2a24107759bf1613812db34/modules/TLCExt.tla#L42-L56

Also seems related to state- and action-constraints.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. I don't like the idea of exposing the trace explicitly. Once you do that, the users will write arbitrary code involving traces, which makes it much harder to analyze for symbolic techniques.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am also not sure what to do about the primed and unprimed variables in this test. It would be nice to make it less of a hack.

Copy link

Choose a reason for hiding this comment

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

Yep. I don't like the idea of exposing the trace explicitly. Once you do that, the users will write arbitrary code involving traces, which makes it much harder to analyze for symbolic techniques.

Power to the People tools. ✊

Copy link
Contributor

@shonfeder shonfeder left a comment

Choose a reason for hiding this comment

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

Exciting stuff! Our discussion with @istoilkovska today seems to really confirm the utility of this stuff.

I posed some questions, just to help us consider different angles.

docs/src/adr/006rfc-unit-testing.md Outdated Show resolved Hide resolved
Comment on lines 97 to 103
\* @require("ConstInit")
\* @require("Prepare_n0")
\* @ensure("Assert_n0")
\* @testAction
TestAction_n0 ==
\E self \in Node:
n0(self)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it impossible to express this effectively without the require and ensure annotations? E.g., is it impossible for this to be expressed as something like?

TestAction_n0 ==
  /\ Require(ConstInit)
  /\ Require(Prepare_n0)
  /\  \E self \in Node:
          n0(self)
  /\ Ensure(Assert_n0)

I ask because, imo, it's generally not great when a parallel system of annotations starts to emerge, and preferable to express things within the language as much as possible. Of course, if it's really essential, then worth pursuing.

If it is essential, we should maybe work out a coherent syntax for the annotations. E.g., the annotations here don't really fit with the type annotations: here we're missing a trailing ; and we use a quoted argument, rather than a colon an unquoted term.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it is essential, we should maybe work out a coherent syntax for the annotations. E.g., the annotations here don't really fit with the type annotations: here we're missing a trailing ; and we use a quoted argument, rather than a colon an unquoted term.

We have two kinds of syntax for single-argument string annotations: https://apalache.informal.systems/docs/adr/004adr-annotations.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ask because, imo, it's generally not great when a parallel system of annotations starts to emerge, and preferable to express things within the language as much as possible. Of course, if it's really essential, then worth pursuing.

I know what you mean. Several times we tried to express a DSL in TLA+ itself, and it happened to be a disaster. I think there are two reasons for that: (1) TLA+ is not an imperative language, (2) formulas have a very flexible structure, so it is tempting to combine things in unexpected ways.

Another reason for having @require and @ensure in annotations is that you can see them like that in contracts for verification tools, e.g., see Dafny, Ivy, VeriFast. So I am just copying what works well :-)

Copy link
Contributor

Choose a reason for hiding this comment

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

I see! Thanks for explaining. This seems well motivated to me.

I'd be in favor of adding a section to the RFC to record the motivation for putting this in annotations instead of as a DSL.

\* @require("Prepare_n0")
\* @invariant("Correctness")
\* @ensure("Assert_noWinner")
\* @testExecution(5)
Copy link
Contributor

Choose a reason for hiding this comment

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

again, I think we can probably settle on either having to quote args or not? If ConstInit is an operator name, then it shouldn't be a string, right? Or else, what happens when we want to supply a string to one of these annotations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, annotations are another layer. Some languages support contracts right in the language, e.g., Scala has requires as part of the language. Maybe one day TLA+ will have it too, but for the moment annotations would work too.

Copy link
Contributor

@shonfeder shonfeder Apr 19, 2021

Choose a reason for hiding this comment

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

I think this reply may have meant for a different comment? (But I am convinced :))

\*
\* @require("ConstInit")
\* @require("Prepare_n0")
\* @invariant("Correctness")
Copy link
Contributor

Choose a reason for hiding this comment

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

With the invariants in particular, couldn't this just be in the body of the operator as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will add a discussion on that in the document. The problem is that all of these annotations can be written directly in TLA by writing temporal formulas. However, this often leads to confusion. Moreover, people start writing very powerful formulas right away. What we need in a test: (1) How to initialize the system, (2) what to execute, (3) what to check. Decomposing a formula into these three components is surprisingly hard, unless people are following a very rigid syntax, which nobody wants to follow.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good reason. Let's do record these motivations, along with #741 (comment) into the RFC>

\* @testExecution(5)
TestExec_n0_n1 ==
\* in this test, we only execute actions by processes 1 and 2
\E self \in { 1, 2 }:
Copy link
Contributor

@shonfeder shonfeder Apr 16, 2021

Choose a reason for hiding this comment

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

Here I'm wondering why we are not using Gen or aren't putting this constraint in a Prepare prerequisite. Not that those would be better, but it seems to me that we have like 3 different ways of accomplishing the same thing? If that's right, I think it'd be good to clarify which way is the preferred one and why. If not, it might help (at least a noob like me) to provide a paragraph justifying why we need these three different ways of stating constraints.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gen is a bit of a hack for testing. I just realized that it is not explained in the document at all.

[Property-based testing]: https://en.wikipedia.org/wiki/QuickCheck
[TLA+ examples]: https://github.com/tlaplus/Examples/
[LCR]: https://github.com/tlaplus/Examples/tree/master/specifications/chang_roberts
[ChangRobertsTyped_Test.tla]: ../../../test/tla/ChangRobertsTyped_Test.tla
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be a link to the file hosted by github, otherwise the link will be broken when this is served from the generated site.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I will have to update the link, once this PR is merged.

@konnov
Copy link
Contributor Author

konnov commented Apr 20, 2021

I have pushed more explanations. The discussions section is not finished yet. Also, added a pdf version of the file, so it is easy to read

@konnov konnov requested a review from romac April 20, 2021 16:24
@konnov
Copy link
Contributor Author

konnov commented Apr 21, 2021

Added discussions on the issues raised by @shonfeder and I guess also by @vitorenesduarte


In the rest of this section, we comment on the alternative approaches.

#### 3.5.1. But I can do all of that in TLA+
Copy link
Contributor

Choose a reason for hiding this comment

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

These sections make the particular feature set and approach very well motivated, IMO. Thanks for adding them!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was actually surprised how hard it was to express such a simple test in a dedicated operator, instead of creating a test in a separate file.

@konnov
Copy link
Contributor Author

konnov commented Apr 26, 2021

@andrey-kuprianov do you like to comment on this RFC? My plan was to post it on reddit, to see if TLA+ users would like to see a testing framework like that.

@Isaac-DeFrain
Copy link

@konnov you'll probably have better luck emailing the google group, I have not found many TLA+ people on Reddit

@konnov
Copy link
Contributor Author

konnov commented Apr 26, 2021

@Isaac-DeFrain, yeah, the tla mailing list too. I just want to do it in stages, so we don't get a storm of replies :)

@konnov konnov merged commit 4f73a46 into unstable Apr 27, 2021
@konnov konnov deleted the igor/rfc-unit branch April 27, 2021 17:46
@apalache-bot apalache-bot mentioned this pull request May 3, 2021
@konnov
Copy link
Contributor Author

konnov commented May 8, 2021

Let's continue the discussion in #817

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.

None yet

5 participants