Die Schnittstelle ist ein Dokument, kein Protokoll.
Testläufe über Tags zusammenstellen und orchestrieren. Eine kuratierte Taxonomie legt sich über deinen Testkorpus, du stellst mit einer richtigen Filtersprache eine präzise Auswahl zusammen und frierst sie zu einer portablen Run Spec ein — einem versionierten Dokument, das sich überall ausführen lässt: von einem lokalen Process Pool, von deinem eigenen Agenten auf einer entfernten Maschine, von einem CI-Job. Die Ergebnisse finden auf beliebigem Weg zurück in eine Lauf-Historie, aus der die nächste Auswahl entsteht.
# boots a fictional web-shop corpus and runs the whole loop
pipx run --spec . runcomposer demo
robotframework.Jeder Testkorpus hat ab einer gewissen Größe dasselbe Problem: Alles laufen lassen geht, einen einzelnen Test laufen lassen auch. Umständlich wird es bei genau der richtigen Teilmenge — und erst recht, wenn du später noch belegen sollst, welche Teilmenge das war. Tags helfen, bis die Tag-Abfrage in einem Job-Parameter der CI steckt, den drei Wochen später niemand mehr nachvollziehen kann.
runcomposer teilt das Problem in zwei Hälften. Das Zusammenstellen wird zu einem eigenen Arbeitsschritt — einem, den man prüfen kann und der ein Dokument hinterlässt. Ausführen sollen andere, und zwar mit Absicht. Das Dokument ist die ganze Schnittstelle: Wer es ausführt, muss drei Felder verstehen, keine API.
Was es nicht ist: ein CI-System, ein Scheduler oder ein Test-Framework. Es gibt keinen Cron, keine instanzübergreifende Queue und keinen Anspruch, das abzulösen, was deine Builds heute schon laufen lässt. Zusammenstellen, dispatchen, einlesen, durchsehen — das ist das ganze Produkt.
Dieselbe Argumentation wie auf dem Rest der Seite, nur in Bewegung: wie umständlich es ist, präzise auszuwählen; was sich ändert, sobald die Auswahl ein Dokument ist; und wohin dieses Dokument danach reisen kann.
69 Sekunden, mit Sprecher
Viereinhalb Minuten über die Schulter, in der echten Oberfläche: die Tests für heute Nacht Bereich für Bereich zusammensuchen, die bekannt kaputten liegen lassen, den Verdicts beim Eintreffen zusehen, bis der Lauf sich abgeschlossen meldet — und am nächsten Morgen den einen Test, der fehlgeschlagen ist, noch einmal laufen lassen. Dazwischen ein Blick darauf, wie es darunter funktioniert, genau an der Stelle, an der die Geschichte ihn braucht.
4 Minuten 36 Sekunden, mit Sprecher · Untertitel
Sechs Schritte. Nach jedem einzelnen wird der Zustand des Laufs berechnet, nie geraten.
Eine Test Source zählt deine Tests als Items auf — je eine opake, stabile Id und die Tags dazu — und bildet über den Inhalt des Katalogs einen Hash. Die Robot-Framework-Source prägt ihre Ids aus Longnames; die Manifest-Source nimmt eine schlichte JSON-Liste, womit ein pytest-Korpus als Node-Ids hereinkommt.
Navigiere durch die
Taxonomie
— einen kuratierten Baum über Tag-Mustern, Daten statt Code —, bau einen Filter, und die
Vorschau kompiliert beim Tippen mit. Die Filtersprache ist klein und verlustfrei: Ein
bloßes Wort ist ein wörtlicher Tag, prefix:Checkout- ist nur syntaktischer
Zucker für einen verankerten Regex, regex: ist der Notausgang, und mehr als
AND, OR, NOT gibt es nicht.
Der Filter wird gegen den Katalog-Snapshot kompiliert, und was dabei herauskommt, wandert als Item-Liste in das Dokument. Dieser Schritt macht einen Lauf reproduzierbar: Der Filter bleibt zur Nachvollziehbarkeit stehen; ausgeführt wird die eingebettete Liste.
Übergib das Dokument an einen Runner — oder exportier es und gib es weiter, an was auch immer. Jede Übergabe prägt einen Dispatch; dieselbe Spec ein zweites Mal laufen zu lassen ergibt einen neuen Dispatch unter demselben Lauf.
Ergebnisse kommen als Bundle zurück, das einen Korrelations-Marker trägt. Schick es per HTTP, leg es in ein überwachtes Verzeichnis oder reich es auf der Kommandozeile durch. Von jeder Zustellung wird der Inhalt gehasht.
Sobald jeder deklarierte Shard geliefert hat, ist der Lauf abgeschlossen und seine Verdicts werden Historie — und genau dort fängt die nächste Auswahl an.
Versioniertes YAML, isomorph zu JSON und in beiden Formen akzeptiert; das
JSON Schema
ist veröffentlicht. Die Kernabschnitte sind generisch und abgeschlossen. Offen ist genau
einer, runner, und da schaut der Kern nie hinein.
runspec: "1.0"
run:
id: "01JZ9GQ2W8KJ3F6M4P5R7T9V" # minted at compose time; the correlation key
title: "Payments regression without quarantined tests"
created_at: "2026-07-06T09:14:03Z"
labels: # free-form provenance; stored, never interpreted
requested_by: "alex"
selection:
tag_filter: # kept for provenance — not re-compiled by executors
op: AND
items:
- op: OR
items: ["Payments", "prefix:Checkout-", "regex:^Cart(V2)?$"]
- not: "prefix:Quarantine-"
materialized: # THE authoritative executed set
item_ids: ["Tests.Payments.Visa Payment Succeeds", "Tests.Payments.Declined Card Shows Error", …]
count: 33
source:
provider: "robotframework" # this source mints ids from Robot longnames
snapshot: "sha256:21ee74fed1fcf5e6…" # integrity check — drift is detectable
results:
expect: [{ format: "robot-output-xml" }]
deliver: "api"
token: "rct_…" # per-run ingest token
runner: # the ONE open section — plugin's own vocabulary
robot-pool:
suite_root: "tests/"
partitions: ["env1", "env2"]
variables: { STAGE: "test" }
Die Item-Liste reist im Dokument mit, deshalb ist eine Spec unterwegs auf nichts weiter angewiesen: Die Maschine, die sie ausführt, braucht keinen Zugriff auf deinen Katalog, deine Datenbank oder dein Netz. Der Katalog-Snapshot reist ebenfalls mit — daran erkennt die ausführende Seite, ob sich der Korpus unter dem Plan bewegt hat.
Ein Consumer muss jede Spec derselben Major-Version annehmen: Unbekannte Felder in
bekannten Abschnitten werden ignoriert, unbekannte Abschnitte auf oberster Ebene ergeben
eine Warnung, eine höhere Major-Version lehnt er rundheraus ab.
runcomposer validate prüft ein Dokument gegen das Schema; mit
--for-dispatch verlangt es obendrein alles, was zum Ausführen nötig ist.
Diesen Teil zu verstehen lohnt sich, denn der ganze Entwurf existiert, um genau ihn möglich zu machen. runcomposer muss deine Testmaschinen nicht erreichen: keine Zugangsdaten dort, keine Netzwerkroute dorthin, kein Agent unter seiner Kontrolle. Es gibt ein Dokument heraus; irgendetwas auf der anderen Seite führt es aus und schickt ein Bundle zurück, wann immer es so weit ist.
runcomposer führt die Tests selbst auf einem Process Pool aus: Fan-out über Partitionen, laufzeitbalanciertes Chunking, und die Verdicts treffen schon ein, während der Lauf noch läuft.
Eine einzige Python-Datei, die du dir zu deinem eigenen Code kopierst, liest auf der Gegenseite die Spec, führt deinen Befehl gegen genau die aufgeführten Ids aus und legt einen Korrelations-Marker neben die Ausgabe. Ein vollständiges Adopter-Kit fährt die Schleife einmal von vorn bis hinten durch.
Stößt einen vorhandenen parametrisierten Job an und übergibt die Spec als Build-Parameter. Dessen eigene Stage führt genau dieselbe Datei aus und schickt das Bundle zurück — für Jenkins gibt es ein reproduzierbares Setup in Docker, das du selbst laufen lassen kannst.
Was auch immer die Spec ausführt — unser Runner, dein Skript, ein CI-Job, ein Kollege auf
seinem Laptop —, schuldet genau drei Dinge. Alles Übrige im Dokument darf es ignorieren,
den ganzen Abschnitt runner eingeschlossen.
Der Consumer ist mit Absicht winzig: eine einzige, in sich geschlossene Python-Datei, die
nur die Standardbibliothek benutzt. Auf der ausführenden Seite ist nichts zu installieren —
kopier sie zu deinem Code ins Repository, oder hol sie aus einem Release,
wo sie als eigenes Asset liegt. Gelesen wird die Spec als JSON; eine Maschine, auf der
nichts weiter als python3 liegt, kann sie ausführen.
# 1 — here: compose and freeze the plan
runcomposer spec 'Regression' --title "Nightly" \
--format json -o spec.json --export
# 2 — there: one vendored file, no install, your own runner command
python3 runcomposer_exec.py spec.json --out results \
--command "./run-tests.sh {ids_file} {out_dir}"
# → results/output.xml (whatever your tests produced)
# → results/runcomposer_run.json (run id + spec hash — the marker)
# 3 — here: the bundle comes back however you like, then
runcomposer ingest results
runcomposer runs
# RUN ID STATE RESULT
# 01JZ9GQ2W8KJ3F6M4P5R7T9V COMPLETE FAIL
Das „wie auch immer du willst“ aus Schritt 3 ist der eigentliche Punkt. Das Bundle ist ein
Verzeichnis. Committe es in einen Ergebnis-Branch, schieb es per rsync, leg es
auf ein Netzlaufwerk, häng es an einen Build — die Korrelation steckt im Marker, der
Transport ist deine Sache und nicht unsere.
Sicher macht das Ganze der Marker. Er trägt die Run-Id und einen Hash genau der Spec-Bytes, die herausgegeben wurden. Ein Bundle, dessen Hash nicht passt, oder eines, das sich auf einen Lauf beruft, den nie jemand losgeschickt hat, rutscht nicht still in deine Historie — es landet sichtbar in einer Quarantäne-Inbox, und ein Mensch entscheidet: zuordnen, übernehmen oder verwerfen.
Dispatch und Rückweg sind vollständig entkoppelt. Drei Transportwege münden in eine
Pipeline: ein HTTP-Push, abgesichert durch das eigene Ingest-Token des Laufs, ein
überwachtes File-Drop-Verzeichnis für Bundles, die per git oder über einen Air Gap gereist
sind, und schlicht runcomposer ingest auf der Kommandozeile.
Dass etwas zweimal zugestellt wird, ist kein Sonderfall — Poller pollen erneut, die CI wiederholt Webhooks, git zieht noch einmal. Deshalb sind die Regeln ausdrücklich: Ein Byte-gleiches Bundle ist ein No-op. Ein anderes Bundle für denselben Shard ersetzt die Verdicts dieses Shards, der letzte Schreiber gewinnt. Ein monotones Zusammenführen gibt es nicht: Eine Korrektur muss ein FAIL wieder zu einem PASS machen können.
Pro Item gibt es genau ein Verdict: PASS FAIL SKIP ERROR, jedes mit Dauer, Meldung, Artefakten und einem Versuchszähler. So lässt sich ein Retry innerhalb eines Laufs abbilden, und Flakiness kann man ausrechnen, statt sie als Vermutung abzulegen.
Sobald sich Läufe ansammeln, wird die Historie selbst zu einer Quelle für Auswahlen. Frag nach den Fehlschlägen des letzten abgeschlossenen Laufs: Beim Zusammenstellen lösen sie sich in eine ganz normale, statische, reproduzierbare Spec auf — samt Nachweis, wie die Liste zustande gekommen ist.
$ runcomposer spec --from-history 'failed@latest' --title "Rerun"
selection:
item_ids: ["Tests.Payments.Expired Card Is Rejected Loudly"]
derived_from:
- provider: "history"
query: { run: LATEST, verdicts: [FAIL] }
resolved_run_id: "01JZ8ZZ…"
„Letzter“ heißt letzter abgeschlossener — ein Lauf, der noch auf Ergebnisse wartet, gilt nicht stillschweigend als Antwort. Und frisch installiert bleiben diese Funktionen dunkel; das sagt das Werkzeug dann auch, statt selbstbewusst eine leere Liste zurückzugeben.
Der Kern kennt Items, Tags, Auswahlen, Specs, Läufe und Verdicts. Von Robot Framework,
pytest, Jenkins oder XML weiß er nichts. Das steckt in Plugins, die entweder über einen
Entry Point geladen werden oder über einen schlichten module:-Pfad in deiner
Konfiguration — keine Umgebungsvariablen, keine Magie, die von selbst etwas findet.
| Plugin | Art | Was es tut |
|---|---|---|
| manifestsource | Test Source | Ein JSON- oder YAML-Katalog, der nur id und tags
braucht. Der Einstieg ohne jede Abhängigkeit; bringt ein
pytest-Beispiel
mit, das Node-Ids benutzt. |
| robotframeworksource | Test Source | Geht .robot-Dateien durch, prägt Ids aus Longnames und nimmt jede
Eigenheit der Namensnormalisierung auf sich, damit der Kern sie nie kennen muss.
Vorgeführt an der
robot-shop-Suite. |
| robot-poolrunner | Runner | Ausführung im eigenen Prozess: Process Pool, Fan-out über Partitionen, laufzeitbalanciertes Chunking, live eintreffende Verdicts, Pre-Run-Hooks, deine eigenen Listener. |
| ci-triggerrunner | Runner | Steuert einen vorhandenen parametrisierten CI-Job. Der Abschluss kommt per Webhook — oder wird über die Build-API gepollt, wenn ein System nicht nach außen rufen kann. |
| robot-output-xmlparser | Result Parser | Robots output.xml → Verdicts. Entschärft: Dokumente mit Entity- oder
DTD-Deklarationen werden rundheraus abgelehnt. |
| junit-xmlparser | Result Parser | Die Lingua franca — pytest und so ziemlich alles andere, was Berichte schreibt. |
| sqlitestore | Run Store | Persistenz ohne Einrichtung für Läufe, Specs, Dispatches, Zustellungen und Verdicts. |
Die Grenze ist erzwungen, nicht bloß gut gemeint: Ein Test wacht darüber und lässt den Build scheitern, sobald Kern-Code auch nur das Vokabular eines Frameworks in den Mund nimmt. Native Ergebnisnamen laufen immer über die Source, der der Id-Raum gehört; so kann keine Eigenheit der Normalisierung nach innen sickern.
Selbst gehostet, eine einzige Konfigurationsdatei, sqlite als Voreinstellung. Die Web-UI liegt fertig gebaut im Wheel, auf Englisch und auf Deutsch, sodass du zum Ausprobieren keine Node-Toolchain brauchst.
git clone https://github.com/StochasticEntropy/runcomposer
cd runcomposer
# the guided demo — 60 tagged tests, a full compose → run → rerun loop,
# seeded into ./runcomposer-demo/ (rm -rf it to undo)
pipx run --spec . runcomposer demo
# the web UI and API on http://127.0.0.1:8100
pipx run --spec . runcomposer serve
# or in a container
docker build -t runcomposer . && docker run -p 8100:8100 runcomposer
# executing Robot Framework in-process needs one extra
pip install ".[robot]"
Ein veröffentlichtes Paket gibt es noch nicht. runcomposer 0.1.0 wird aus einem
Clone installiert — davon geht jeder Befehl auf dieser Seite aus. Auf PyPI oder npm liegt
nichts, pip install runcomposer findet es also nicht; wann sich das ändert,
siehst du bei den Releases.
Die CLI ist das ganze Produkt ohne Browser. Alle elf Befehle:
demo startet die Beispielwelt, catalog listet den Korpus und
seinen Snapshot, compile zeigt eine Auswahl vorab, spec stellt
eine zusammen, validate prüft ein Dokument gegen das Schema,
dispatch führt es aus, ingest nimmt Ergebnisse zurück,
runs blättert darin, export reicht Ergebnisse an ein anderes
Werkzeug weiter, gc hält alles in Grenzen, und serve liefert
Web-UI und API.
ADOPTING.md geht Schritt für Schritt durch, wie du das Ganze auf deinen eigenen Korpus und deine eigenen Maschinen richtest; DESIGN.md ist die Architektur und die Begründung für jede Entscheidung, die du hier siehst.
Alles, was oben steht, liegt in einem einzigen öffentlichen Repository. Es ist ein junges Projekt mit einem einzigen Maintainer, deshalb ist der Issue-Tracker die Eingangstür für alles, was hier nicht hineinpasst.
ADOPTING.md — an deinen eigenen Korpus, deinen eigenen Runner und deine
eigenen Maschinen anschließen, eine Entscheidung nach der anderen.
DESIGN.md — die Architektur, die Grenzen und ein Entscheidungsprotokoll,
das festhält, was verworfen wurde und warum.
examples/ — die Robot-Suite, der pytest-Korpus, das Adopter-Kit für den
entfernten Agenten und eine vollständige Run Spec.
Wheel, sdist und runcomposer_exec.py als eigener Download — die eine
Datei, die du dir zu deinem Code ins Repository legst.
Bugs, ein Korpus-Zuschnitt, der nicht passt, oder ein Plugin, das die Grenze erlauben sollte — dafür sind Issues da.
MIT. Benutzen, forken, ein Closed-Source-Plugin dafür schreiben und ausliefern — genau dazu ist die Plugin-Grenze da.