In-place testing with Scribble
A lot of projects come with their own set of tests. Tests are a useful quick check for your properties - if the properties you have in mind don't hold for the user tests, then something might be wrong with them. However in order to build an arbitrary project and run its test suite, Scribble needs to instrument the files in-place, without changing the file/directory structure, or contracts' external interfaces.
In this tutorial we will learn how to instrument a simple project in-place with scribble, and check whether its tests still pass after instrumentation. A failing test may indicate either a bug in your specifications or a bug in your actual program.
Prerequisites
You should have the following installed:
git
node version 12.x
Setting-up
You need to first install scribble
:
Next you need to clone the sample project repo:
And run npm install
in the directory containing the tutorial:
This will install truffle
and ganache-cli
and any remaining dependencies.
The project
In the scribble-getting-started/in-place-testing
directory of the repository you should find a simple Truffle project. Under contracts/
there are 2 contracts of interest - a base contract Managed
that contains some logic for adding and removing administrators and a test contract Test
that extendsManaged
.
Managed.sol
Test.sol
One property we may want to add to Managed.sol
, is that only current administrators may call the addAdminInternal
function. The contract Test
violates that property - Test.addAdmin
calls addAdminInternal
without first checking if the caller is an admin.
As a first try we can add the following annotation in Managed.sol
(extra brownie points if you see something weird already ;)):
The added annotation is a 'function annotation`. It consists of 3 parts:
#if_succeeds
specifies that the predicate should only be checked if theaddAdminInternal
function succeeds.{:msg "only owner can add admins"}
provides a human-readable message to help remind you what this property is checking. As shorthand you can skip the{:msg }
and only specify"only owner can add admins"
.admins[msg.sender]
is the actual property. Its a valid boolean Solidity expression. In this case we are saying that the value ofadmin[msg.sender]
must be true upon successful execution ofaddAdminInternal
.
This project has a test test/tests.js
in which a random user tries to add himself as an admin.
If we run the test suite without instrumentation this test would actually succeed. We would expect that if we instrumented the contract, and ran the test, the test would fail, showing that the contract doesn't respect the specification.
Instrumenting contracts in-place manually
To instrument the contracts we can run:
The output tells us that scribble
created a .instrumented
version for Managed.sol
. Indeed we should see the new under contracts
:
There is a 2nd new file there as well - __scribble_ReentrancyUtils.sol
. This is a contract for internal use that scribble
always emits.
Now we want to swap the .instrumented
file with its original counterpart and re-run the tests. Since we don't want to loose our original files, lets first copy the originals to the side:
Then copy the instrumented file in-place of the original:
Now you should be able to run the test suite and hit the failure. To do so in another shell window you must start ganache-cli
by running:
Back in the original shell you can run the tests with:
But wait! why did that test succeed? Shouldn't it have failed since the sender wasn't an administrator?
The key here is when the admin[msg.sender]
predicate was evaluated - it was evaluated after the function had successfully executed, at which point the sender had already added themselves to the admin
map. To fix this, we want to talk about the value of admin[msg.sender]
before the function started executing. We can do this using the old()
operator, which specifies that scribble
should compute the value of a given expression before the function is called.
So lets fix this. First lets move the original annotated file back:
Now lets change the annotation to look like this:
Now if we repeated the whole process of instrumenting, swapping the files, and running tests we should hit an error. But that process is so laborious! There must be a better way! And there is - introducing --arm
and --disarm
.
Scribble --arm and --disarm
Copying files back and forth, keeping track of .sol.instrumented
and .sol.original
files is annoying. So scribble
provides you with two options that take care of this for you.
If you add the --arm
option when instrumenting, then scribble
will automatically create .sol.original
copies of the original files, and swap the instrumented files in-place, so you can start testing immediately. So now that we have fixed our annotation, lets use --arm
and --disarm
.
First we will instrument the contracts and swap them in-place with a single command:
Note that the new output shows which files were copied where. At this point if we run the tests, we can verify that the fixed annotation fails:
And afterwards we can revert the workspace back by using the--disarm
option:
Note that the additional .original
and .instrumented
files, as well as other auxilary files emitted by scribble are all removed:
If you want to keep the .instrumented
files for debugging purposes, add the --keep-instrumented
option when disarming.
Testing with and without asserts
When we ran the instrumented tests, we hit an assertion and the test fails. However if we had multiple properties, we wouldn't know WHICH property fails. It would be nice to get a message telling us which property failed. To do so, lets re-instrument the code with the --no-assert
option:
The --no-assert
option tells scribble to not emit an explicit assert(false)
on failure, and instead just emit an event. As a result, the tests may not fail anymore, but if you run them with--show-events
you will see all the failing properties:
Now we see the AssertionFailed
event. Its text contains the human-readable label from the original annotation.
Last updated