Conform Tutorial
In this tutorial we will write and evaluate a specification for an example system from the Modality tracing.rs library (opens new window).
# Before You Start
This tutorial builds off of the Modality tutorial. (opens new window) Specifically, we will write and evaluate specifications against the data generated in that tutorial. If you haven't yet, follow the instructions in the Modality tutorial, then return here.
# Setup
First, install Conform. Make sure you complete the steps in the installation guide to tell the Conform CLI where it can access the modalityd API and to make an auth token available to the CLI. For simplicity, in this tutorial we will assume you are installing conform on the same machine where you have installed modality and modalityd and followed the Modality tutorial, since this is the easiest way to proceed.
$ sudo apt-get install conform
# A Simple Specification
We will start by writing a simple specification.
# Components of a Specification
Specifications in Conform use a syntax which naturally lends itself to describing system behavior and requirements. Each specification is made up of one or more behavior
s, and each behavior
contains one or more case
s, as well as a single optional when
block.
There are 3 types of case
:
nominal case
blocks describe how your system should behave.recovery case
blocks describe how your system is expected to deal with adverse conditions.prohibited case
blocks describe things your system should never do.
The when
block changes the semantics of the behavior so that its case
s must match every time the when
block is matched, rather than only needing to satisfy the case
s a single time.
# The Example System
The example system is a simplified simulation of a system with 3 components. There is a producer
component which takes some measurements. It then sends these measurements to a consumer
component, which acknowledges their receipt. In addition, both the producer
and consumer
regularly send heartbeat messages to a monitor
component.
For this tutorial we will focus on the messages that go between the producer
and consumer
. To start, let's write a specification to verify that every message sent by the producer
is received by the consumer
. We will put the following specification in a file named tutorial.speqtr
:
behavior "Messages should be received"
when "The producer sends a message"
"Producer sending measurement message"@producer AS SentMessage
end
nominal case "The consumer receives the message"
SentMessage FOLLOWED BY
"Received measurement message"@consumer AS ReceivedMessage
AND SentMessage.sample = ReceivedMessage.sample
end
end
This specification has a when
block to describe a precondition and a nominal case
block to describe the system's expected behavior. In plain English, the specification says "Every time I see the producer send a message, I should then see the consumer receive a message with the same sample value".
SpeQTr
For more information on how to write specifications and all the different ways you can describe system behavior check out the SpeQTr documentation. (opens new window)
# Creating and Evaluating Specifications
Now that we have a specification written in a file, let's create the specification so Conform can track its revisions and evaluation results.
$ conform spec create tutorial --file tutorial.speqtr
Created spec 'tutorial' at version 'cf318f03-ba70-4555-a1a2-fea61f09dffd'
The output tells us we've successfully created the specification and gives us the current version ID (more on that later). Now that we have a specification, let's evaluate it!
Note
We're using example data that we generated during the Modality tutorial. (opens new window) If you haven't yet, follow the steps in that tutorial to generate data to evaluate this specification against.
Conform and Modality ship with a default setup to automatically select the set of data from your most recent run, so we will just use that default setup to evaluate this specification against our run of the example system.
$ conform spec eval --name tutorial
No explicit workspace provided. Using the default workspace.
evaluation of spec "tutorial"
[ ✔ ] segment "Run 0e60118a-50f0-4326-85b4-ba8eb802fd60"
[ ✔ ] behavior "Messages should be received"
All Evaluations Passed
Behaviors: [1: Passed] [0: Inconclusive] [0: Failed] (stored as d3ca18f3-71fc-442e-a025-64eff17dea1b)
Great news, we've confirmed that every single message sent by the producer
was received by the consumer
. The output also shows us the evaluation result ID at the end, which will allow us to recall these results if we need them later.
# Updating a Specification
Now, let's make our specification just a bit more demanding. It turns out our system requirement isn't just for every message to be received eventually, but that we need the consumer
to receive messages within 850 microseconds. We can easily add this constraint to our specification by updating the relationship connective from FOLLOWED BY
to FOLLOWED BY WITHIN 850us
. We'll put the new version of the specification in a file named updated-tutorial.speqtr
. It looks like this:
behavior "Messages should be received in a timely manner"
when "The producer sends a message"
"Producer sending measurement message"@producer AS SentMessage
end
nominal case "The consumer receives the message within allowable time"
SentMessage FOLLOWED BY WITHIN 850us
"Received measurement message"@consumer AS ReceivedMessage
AND SentMessage.sample = ReceivedMessage.sample
end
end
We can now update our tutorial
specification to reflect this new constraint:
$ conform spec update tutorial --file updated-tutorial.speqtr
Updated spec 'tutorial' to version 'a659500e-2c6d-49b4-8137-cf2704246765'
To confirm our specification is updated let's inspect tutorial
to see its version history:
$ conform spec inspect tutorial
Spec: tutorial
Updated By: tutorial
Updated At: 2022-08-25 01:45:35 UTC
Current Version: a659500e-2c6d-49b4-8137-cf2704246765
Other Versions:
* a659500e-2c6d-49b4-8137-cf2704246765 Created at 2022-08-25 01:45:35 UTC by tutorial
■ cf318f03-ba70-4555-a1a2-fea61f09dffd Created at 2022-08-25 01:35:29 UTC by tutorial
Spec Evaluations: 0
This shows us that we have two versions of the tutorial
spec, and that the current one has never been evaluated. Let's evaluate it and see what we get:
$ conform spec eval --name tutorial
No explicit workspace provided. Using the default workspace.
evaluation of spec "tutorial"
[ ✘ ] segment "Run 0e60118a-50f0-4326-85b4-ba8eb802fd60"
[ ✘ ] behavior "Messages should be received"
Failures Detected
Behaviors: [0: Passed] [0: Inconclusive] [1: Failed] (stored as 61f0854a-4822-43e9-a7a3-7bab137c15b2)
|
1 | behavior "Messages should be received"
|
6 | nominal case "The consumer receives the message"
7 | SentMessage FOLLOWED BY WITHIN 850us "Received measurement message"@consumer AS ReceivedMessage AND
| ^^^^^^^^^^^^ Did not match
8 | SentMessage.sample = ReceivedMessage.sample
9 | end
|
= error: When evaluating the behavior 'Messages should be received', no expected
cases matched. The case 'The consumer receives the message' partially
matched, but an event matching the subexpression 'WITHIN 850us' was
not found.
Note
Depending on how long you ran the example system it is possible that this specification will pass for you. If you would like to explore what happens with a failing specification try running the example system for longer and it is likely to produce failing data.
This time our specification fails. Conform's logical analysis helpfully indicates that the violation is because of the time constraint—it can find a "received message" event for every "sent message" event, but not always within the time limit.
# Adding Recovery Behavior
So far we have described our system's expected behavior in the nominal case. SpeQTr also lets us use a recovery case
to say how our system is expected to handle situations outside of standard operation. Let's add a grace period for messages that may be sent when the system is shutting down. We will put our new spec in a file named recovery-tutorial.speqtr
:
behavior "Messages should be received"
when "The producer sends a message"
"Producer sending measurement message"@producer AS SentMessage
end
nominal case "The consumer receives the message within allowable time"
SentMessage FOLLOWED BY WITHIN 850us
"Received measurement message"@consumer AS ReceivedMessage
AND SentMessage.sample = ReceivedMessage.sample
end
recovery case "The pipeline is shutting down, message receipt is not guaranteed"
SentMessage FOLLOWED BY WITHIN 5s "Shutting down"@producer
end
end
Now, let's update our specification and evaluate it again:
$ conform spec update tutorial --file recovery-tutorial.speqtr
Updated spec 'tutorial' to version '4ef43db4-4cac-4f5a-9103-e7e66cd133b8'
$ conform spec eval --name tutorial
No explicit workspace provided. Using the default workspace.
evaluation of spec "tutorial"
[ ✘ ] segment "Run 0e60118a-50f0-4326-85b4-ba8eb802fd60"
[ ✘ ] behavior "Messages should be received"
Failures Detected
Behaviors: [0: Passed] [0: Inconclusive] [1: Failed] (stored as 20701255-a919-4bc8-b921-e5e35acab1fd)
|
1 | behavior "Messages should be received"
|
6 | nominal case "The consumer receives the message within allowable time"
7 | SentMessage FOLLOWED BY WITHIN 850us "Received measurement message"@consumer AS ReceivedMessage AND
| ^^^^^^^^^^^^ Did not match
8 | SentMessage.sample = ReceivedMessage.sample
9 | end
|
= error: When evaluating the behavior 'Messages should be received', no
expected cases matched. The case 'The consumer receives the message
within allowable time' partially matched, but an event matching the
subexpression 'WITHIN 850us' was not found.
Our updated specification still fails. This confirms that the problem was not just system shutdown and that there is in fact a bug in the system.