Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft

The platform for test development and orchestration
that lets you and your AI Agents run the entire testing operation

Watch the demo

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.
Override carefully

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.
Override carefully

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.
Refused by the framework

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.
Override carefully

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.

Refused by the framework

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.

Type to search the docs.

    ↑↓ navigate ↵ open Esc close