Orcraft Test
A test is the smallest thing in Orcraft worth building on its own. It needs no group, no suite and no endpoint: a single Python file with a test class in it can be run from the command line, and the same file, unchanged, is what the controller loads later.
Every test is three classes - a config, a result and the test itself:
from __future__ import annotations
from orcraft import (
OrcraftTest,
OrcraftTestCase,
OrcraftTestConfig,
OrcraftTestResult,
OrcraftConfigField,
OrcraftResultField,
)
class MyTestConfig(OrcraftTestConfig):
name = OrcraftConfigField.Text(default="My Test")
description = OrcraftConfigField.Text(default="Verifies that the system under test responds correctly.")
payload_size = OrcraftConfigField.Numeric(default=64, label="Payload Size")
threshold = OrcraftConfigField.Numeric(default=100, label="Threshold")
class MyTestResult(OrcraftTestResult):
measured_value = OrcraftResultField.Numeric(default=0, label="Measured Value")
class MyTest(OrcraftTest):
def __init__(self, config: MyTestConfig | None = None) -> None:
super().__init__(config or MyTestConfig())
def _run_case(self, case: OrcraftTestCase) -> OrcraftTestResult:
case.result = MyTestResult()
measured = measure(payload_size=self.config.payload_size)
case.result.measured_value = measured
self.checkpoint(
measured <= self.config.threshold,
f"Measured {measured}, within the threshold of {self.config.threshold}.",
f"Measured {measured}, above the threshold of {self.config.threshold}.",
)
return case.result
def main() -> None:
MyTest().run()
if __name__ == "__main__":
main()
Config class
The config declares everything about the test that can be configured from outside it. Each field is a
class attribute built from the
OrcraftConfigField
namespace, and inside the test it reads as an ordinary attribute -
self.config.payload_size - holding either the configured value or the declared default.
Subclassing OrcraftTestConfig means the config starts out with fields it did not declare.
Overriding one, as name and description are overridden above, replaces its
default without changing what it is for.
Overridable in code
Set in your config class, fixed for the run.
| Field | Default | Meaning |
|---|---|---|
name |
"Test" |
Display name in the GUI, CLI and logs. Not editable from the GUI. The framework prefixes it with Test: in the log. |
description |
"" |
Free text describing what the test does. Not editable from the GUI. |
version |
"" |
Test version, recorded with the results. |
palette |
"gray" |
Log color palette applied to the test and everything it runs: default, classic, gray or brown. |
endpoints_file_path |
"" |
The controller's saved endpoints file: the machines this test's endpoint pools pick from when it runs outside the controller, from the command line, in a batch or in CI. A relative path is relative to the file of the config class that sets it. Only a pool reads it: endpoints named in the fields run as named, and localhost is never picked by a pool. Left empty, a test with a pool is refused before it runs, with a message naming the field. The controller never shows it and picks from its own endpoints. |
log_orientation |
HORIZONTAL |
Orientation of the logged config table. The framework's own tables beneath the test - test cycles, cases and case cycles - have no config class of their own and read the same way, and OFF leaves all of them out of the log, the matrix tables included. The result table has its own log_orientation. |
Redeclare a field with the same OrcraftConfigField type and a new default -
name = OrcraftConfigField.Text(default="My Test"). A bare assignment such as
name = "My Test" is not an override: it shadows the field with a plain attribute, and the
GUI, the log table and required validation no longer see it.
Overridable in code or GUI
Set in your config class, and changeable again from the run configuration panel before a run.
| Field | Default | Meaning |
|---|---|---|
test_endpoint |
"localhost" |
The endpoint this test executes against - see Orcraft Endpoints. |
matrix |
None |
The dimensions that expand into cases - see Orcraft Matrix. |
number_of_cycles |
1 |
How many times the whole test repeats - see Cycles. |
case_number_of_cycles |
1 |
How many times each case repeats - see Cycles. |
upload_test_package |
"if_missing" |
Whether the test package is uploaded to the endpoint before the run. |
update_endpoint_app |
"on_mismatch" |
Whether the endpoint application is updated before the run. |
Same rule as above: redeclare with the same OrcraftConfigField type and a new default,
never a bare value. The default you set here is what the run configuration panel shows until someone
changes it there.
Fields sitting at their default - a single cycle, an empty matrix - are hidden from the run panel rather than shown as clutter, and the endpoint policy fields only appear for a test that actually uses endpoints.
upload_test_package takes one of three values:
| Value | Meaning |
|---|---|
always |
Upload on every run. The safe choice while the suite is changing. |
if_missing |
Upload only when the endpoint does not already have the package. The default. |
never |
Never upload - the endpoint is expected to have the package already. |
update_endpoint_app takes one of three values:
| Value | Meaning |
|---|---|
always |
Update the endpoint application before every run. |
on_mismatch |
Update only when the endpoint application does not match the controller. The default for a test. |
never |
Leave the endpoint application as it is. |
Reserved
Written by the framework while a run is in progress.
| Field | Meaning |
|---|---|
start_time |
When the run started. Stamped by the framework at run time. |
log_tab |
Log indentation. Assigned by the framework at every level. |
A class of your own that declares one of these names fails as soon as it is defined, with an error naming the field, and an assignment to one at run time raises the same error. The framework alone writes them.
Result class
The result declares what the test reports. Its fields come from the
OrcraftResultField
namespace, and like the config it starts out with fields it did not declare. Unlike the config, most of
these are written by the framework, not by you:
overridable by user (optional) · written by the framework, may still be set directly · reserved
| Inherited field | Overridden by user (optional) | Reserved | Meaning |
|---|---|---|---|
status |
PASS, FAIL or ERROR. Assign it directly only for ERROR - see Setting a status. |
||
failure_description |
Why the system under test was judged incorrect. Setting it moves a passing result to FAIL. |
||
error_description |
Why the test could not complete. | ||
warning_description |
– | Something worth flagging that does not change the status. | |
finish_time |
– | When the component finished. | |
elapsed_time |
– | How long it took. | |
total_cases, done_cases, passed_cases, failed_cases, error_cases, warning_cases |
– | Per-case counters, carried up into the group and suite results. total_cases is the plan, the cases the run was going to execute, even when the test stopped before reaching them. done_cases is the cases that reached a verdict, passed or failed; an errored case is not done. Each counter after the total is logged with its share of the total, as 898 (99.56%). |
|
log_orientation |
– | Whether the result table is logged horizontally, vertically or not at all (OFF). Test results default to vertical because the counters make a horizontal row too wide. |
Fields you may set are assigned at run time - case.result.warning_description = "..." -
not redeclared. Redeclare only to change a default, and then with the same
OrcraftResultField type:
log_orientation = OrcraftResultField.Format(default=OrcraftResultField.LogTableOrientation.VERTICAL).
A bare value on the class shadows the field, and the log table and the controller no longer see it.
A class of your own that declares one of the reserved names fails as soon as it is defined, with an
error naming the field, and an assignment to one at run time raises the same error. Only
log_orientation is meant to be redeclared, and only to change its default.
Declaring a result class is optional. A test that has nothing of its own to report can leave
case.result alone - it is then a plain OrcraftTestResult, with the same
inherited and reserved fields - and still produce a complete pass or fail. Declaring one is what makes
measured values appear in the log tables, in the controller and in exported results.
Test body
_run_case is the only method a test must implement. The framework calls it once per case
per cycle, hands it the case, and
expects the case result back:
def _run_case(self, case: OrcraftTestCase) -> OrcraftTestResult:
case.result = MyTestResult()
...
return case.result
Assigning case.result to your own result subclass is what puts the extra result fields in
play - without it the case reports only the inherited ones. Everything else about the body is ordinary
Python: call into the system under test however you like, record what you measured on
case.result, and assert with
checkpoint.
A case body does not have to be small. When it grows past what reads well in one method, split it into steps and flows - but that is a decision to make when the test needs it, not up front.
Cycles
Repetition is configuration, not code. Two independent counters on the test config control it, and both are editable from the controller before a run:
| Field | Meaning |
|---|---|
number_of_cycles |
How many times the whole test - all of its cases - repeats. |
case_number_of_cycles |
How many times each individual case repeats before moving to the next one. |
The difference is the order of execution. Test cycles repeat the sequence: every case once, then every case again. Case cycles repeat in place: the first case three times, then the second case three times. For a flaky interaction, case cycles hammer the same conditions; for wear over time, test cycles walk the whole matrix repeatedly.
A test with four cases, two test cycles and three case cycles executes the case body twenty-four times. Both counters are hidden from the run panel while they sit at one, so a test that does not repeat does not advertise the fields.
Cycles are a test-level idea. Suites, groups, flows and steps have no cycle counter of their own: a suite repeats because the tests inside it do, and a flow repeats because the case that calls it does. Orcraft Setup, Teardown and Exit gives each cycle scope its own setup and teardown when per-cycle state has to be rebuilt.
Setup and teardown
Work that must happen around the cases rather than inside them - launching an application, opening a
connection, generating shared inputs - belongs in a lifecycle hook rather than at the top of
_run_case:
def _setup(self) -> None:
self.app = MyApplication(self.config.app_folder)
self.app.start()
def _teardown(self) -> None:
self.app.stop()
Every hook is optional. Orcraft Setup, Teardown and Exit covers all six of them and what happens when one fails.
Standalone execution
Giving the file a main() makes it directly executable, which is how a test is developed
before it is added to a group:
def main() -> None:
MyTest().run()
if __name__ == "__main__":
main()
run() executes the full lifecycle - setup, every case of every cycle, teardown - and logs
the config table, the per-case results and the test summary exactly as the controller would. There is
no separate packaging step and no alternate entry point: the file that runs from a terminal is the file
the controller loads.
Once the test does what you want, add it to a group to run it alongside its neighbors.