Skip to content

Commit 0aaae0a

Browse files
vzakaznikovclaude
andcommitted
Examples for TestFlows Machine
Programs that hold a concurrency bug with a known shape, two stress programs, a hello world, and Compose projects, published under Apache 2.0. - hello/hello-world.c: the smallest disk that boots and runs a program. - races/, starvation/, stress/: the programs. Plans in their comments use the syntax at testflows.com/docs/machine/steering-programs.md and name /examples/<name>, the path in the testflows/machine-examples image. - compose/: two-services, clickhouse and clickhouse-regression. The regression suite is used at a pinned commit of its main, with prepare-env.patch, Altinity PR 170, applied. - make builds every program static and x86_64 into bin/; make check proves it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJu3r5JhoqtUHitojYU7xh
1 parent 4fe23b4 commit 0aaae0a

22 files changed

Lines changed: 2837 additions & 1 deletion

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin/
2+
.*.swp

‎LICENSE‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright [yyyy] [name of copyright owner]
189+
Copyright 2026 Katteli Inc.
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

‎Makefile‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Builds every example program as a static x86_64 binary in bin/, which is what
2+
# `machine disks build --binary` takes, and what the testflows/machine-examples
3+
# image carries.
4+
#
5+
# Copyright 2026 Katteli Inc.
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
CC = gcc
20+
# -static because `--binary` refuses anything asking for an interpreter: the
21+
# image it writes has no loader and no libraries. -pthread because most of
22+
# these programs are about what threads do to each other.
23+
CFLAGS = -O2 -static -pthread
24+
25+
# One directory per kind of example. compose/ holds projects, not programs.
26+
GROUPS = hello races starvation stress
27+
28+
SOURCES = $(wildcard $(addsuffix /*.c,$(GROUPS)))
29+
BINARIES = $(patsubst %.c,bin/%,$(notdir $(SOURCES)))
30+
31+
.PHONY: all clean check list
32+
33+
all: $(BINARIES)
34+
@echo "Built: $(BINARIES)"
35+
36+
# One rule per group, because the sources are in several directories and the
37+
# binaries land in one.
38+
define build_group
39+
bin/%: $(1)/%.c | bin
40+
$$(CC) $$(CFLAGS) $$< -o $$@
41+
endef
42+
$(foreach group,$(GROUPS),$(eval $(call build_group,$(group))))
43+
44+
bin:
45+
mkdir -p bin
46+
47+
# What `--binary` checks before it packs anything, asked here so a binary that
48+
# would be refused is caught by the build that made it rather than by the
49+
# upload that carries it.
50+
check: all
51+
@for b in $(BINARIES); do \
52+
file -b "$$b" | grep -q "statically linked" \
53+
|| { echo "$$b: not static"; exit 1; }; \
54+
file -b "$$b" | grep -q "x86-64" \
55+
|| { echo "$$b: not x86_64"; exit 1; }; \
56+
echo "$$b: static x86_64"; \
57+
done
58+
59+
list:
60+
@echo $(SOURCES) | tr ' ' '\n'
61+
62+
clean:
63+
rm -rf bin

‎README.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# TestFlows Machine Examples
2+
3+
Programs and Compose projects to run on [TestFlows Machine](https://testflows.com/machine/).
4+
5+
## ✨ Getting Started
6+
7+
```bash
8+
git clone https://github.com/testflows/Machine-Examples.git
9+
cd Machine-Examples
10+
```
11+
12+
## 🧪 Programs
13+
14+
The programs are small and written to be run many times. Most hold a
15+
concurrency bug with a known shape, and what they print depends on how their
16+
threads interleaved, which is the thing a machine decides, records and can
17+
replay. The two in `stress/` hold no bug at all: they drive the kernel hard
18+
enough that a replay either matches or does not.
19+
20+
| Example | What it does | The bug |
21+
|---|---|---|
22+
| [hello/hello-world.c](hello/hello-world.c) | Prints a greeting and the kernel it runs on | None. The smallest disk that boots and runs a program |
23+
| [races/data-race.c](races/data-race.c) | N threads increment one shared counter with no lock | Lost update. The read-add-write is three steps and another thread lands between two of them, so the total comes out short |
24+
| [starvation/timeout-starve.c](starvation/timeout-starve.c) | Main arms a timeout and waits; the worker must publish before it fires | The worker is starved for the whole wait and the timeout expires with nothing published. It is the ONLY runnable thread while main sleeps, so no pick-next policy finds this |
25+
| [starvation/pipe-wakeup.c](starvation/pipe-wakeup.c) | A reader blocks on a pipe, main writes one byte then waits | The reader is starved after becoming runnable and misses the timeout. Both threads are runnable after the write, so a scheduler can pick the reader |
26+
| [starvation/futex-wakeup.c](starvation/futex-wakeup.c) | A waiter blocks on a locked mutex, main unlocks then waits | The waiter is starved after the unlock. Same shape as pipe-wakeup with a futex instead of a pipe |
27+
| [starvation/narrow-window.c](starvation/narrow-window.c) | A producer raises a flag for a few iterations, then lowers it for a long gap | The consumer is starved through every window and never sees the flag up. Needs frequent short delays, not long ones |
28+
| [starvation/missed-wakeup.c](starvation/missed-wakeup.c) | A waiter checks a predicate, then waits on it, with no loop around the check | Missed wakeup. Held between the check and the wait, the signaler signals into nobody and the waiter sleeps through it. The timeout expires with the predicate already true |
29+
| [starvation/lease.c](starvation/lease.c) | Main waits on a lease long enough for the worker's fixed work | The lease expires before the worker publishes. Nobody is starved: virtual time jumps while the work is still running, so the timer fires early |
30+
| [stress/memstress.c](stress/memstress.c) | Allocates close to the VM's memory limit across threads, with madvise, fork/COW and fault cycles | Not a bug of its own. It drives the kernel's memory management hard, which is the work a replay has to reproduce exactly |
31+
| [stress/memswapstress.c](stress/memswapstress.c) | Allocates 150% of RAM against a swap file, forcing swap-out, swap-in and swap cache | Not a bug of its own. Same purpose one layer down, through swap |
32+
33+
## 🚀 Run One From the Image
34+
35+
Every program is in one image, `testflows/machine-examples`, at
36+
`/examples/<name>`. Pull it, then build a disk with `--entrypoint` naming the
37+
program; arguments go after `--`. A machine is x86_64, so ask for
38+
`linux/amd64` even on an ARM computer:
39+
40+
```bash
41+
docker pull --platform linux/amd64 testflows/machine-examples
42+
machine disks build --image testflows/machine-examples --entrypoint /examples/hello-world hello
43+
machine disks build --image testflows/machine-examples --entrypoint /examples/data-race race -- 8
44+
machine create --disk race
45+
```
46+
47+
Each program's arguments and defaults are at the top of its source.
48+
49+
## 🔨 Build Them Yourself
50+
51+
`make` compiles every program static and x86_64, which is what a disk built
52+
from a binary requires:
53+
54+
```bash
55+
make # every program, into bin/
56+
make check # and prove each one is static x86_64
57+
make list # what would be built
58+
```
59+
60+
```bash
61+
machine disks build --binary bin/data-race race -- 8
62+
machine create --disk race
63+
```
64+
65+
## 🐳 Compose Environments
66+
67+
`compose/` holds whole environments rather than programs: a docker compose
68+
project that `machine disks build --compose` turns into a disk. The machine
69+
runs the project with `--abort-on-container-exit`, so it ends when a service
70+
exits.
71+
72+
| Environment | What it does | What it shows |
73+
|---|---|---|
74+
| [compose/two-services](compose/two-services/docker-compose.yml) | A busybox server and a client that fetches from it by service name | Services reach each other by name, over the network Docker gives the project, the same as on any host |
75+
| [compose/clickhouse](compose/clickhouse/docker-compose.yml) | A ClickHouse server and a client that queries it | A real database server in a machine. `config.xml` replaces the image's: IPv4 only, because the machine's kernel has no IPv6, and every background pool capped, because the defaults hang startup in a small VM without an error |
76+
| [compose/clickhouse-regression](compose/clickhouse-regression/README.md) | Altinity's ClickHouse regression suite, which brings up its own Compose project inside the machine | A test suite that drives Docker Compose itself. The runner binds the machine's Docker socket, the sources sit at one path both sides of it agree on, and `scale: 0` carries the images the suite starts but no service here names |
77+
78+
```
79+
machine disks build --compose compose/clickhouse ch
80+
machine create --disk ch
81+
```
82+
83+
Each project is also runnable on the host, which is how to fix one without
84+
waiting for a boot:
85+
86+
```
87+
cd compose/clickhouse && docker compose up --abort-on-container-exit
88+
```
89+
90+
## 📚 Learn More
91+
92+
- 🌐 **Website:** [testflows.com/machine](https://testflows.com/machine/)
93+
- 📖 **Documentation:** [testflows.com/docs/machine](https://testflows.com/docs/machine/)
94+
- ⬇️ **Download:** [testflows.com/machine/download](https://testflows.com/machine/download/)
95+
96+
## 📜 License
97+
98+
Apache 2.0; see [LICENSE](LICENSE).
99+
100+
---
101+
102+
Learn to write test programs, not just tests 👽
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# The suite is someone else's code, copied in and never tracked.
2+
suite/
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# clickhouse-regression
2+
3+
Altinity's [ClickHouse regression suite](https://github.com/Altinity/clickhouse-regression)
4+
in a machine. It is a test suite that drives Docker Compose itself:
5+
`regression.py` brings up its own project inside the machine, runs its tests
6+
against it and tears it down. So the disk holds two projects: this one, which
7+
the machine starts at boot, and the suite's, which the suite starts.
8+
9+
Only this project is here. The suite is copied into `suite/`, which
10+
`.gitignore` keeps untracked.
11+
12+
## 📦 1. The Sources
13+
14+
The suite is used at a pinned commit of its `main`, with
15+
[prepare-env.patch](prepare-env.patch) applied. The patch is Altinity
16+
[PR 170](https://github.com/Altinity/clickhouse-regression/pull/170), which
17+
adds `--prepare-env`; when it is merged, the patch goes.
18+
19+
```bash
20+
cd compose/clickhouse-regression
21+
git clone https://github.com/Altinity/clickhouse-regression.git /tmp/clickhouse-regression
22+
git -C /tmp/clickhouse-regression checkout 5f7decbac4a3896931be5cfe1882ab1ad1e72fab
23+
git -C /tmp/clickhouse-regression apply "$PWD/prepare-env.patch"
24+
mkdir -p suite
25+
cp -a /tmp/clickhouse-regression/helpers \
26+
/tmp/clickhouse-regression/docker-compose \
27+
/tmp/clickhouse-regression/requirements.txt \
28+
/tmp/clickhouse-regression/example \
29+
suite/
30+
```
31+
32+
Four entries, not the repository: `disks build` packs the whole project
33+
directory, and that repository is 2.1GB of tracked files. A suite needs
34+
`helpers/`, the shared `docker-compose/`, `requirements.txt` and its own
35+
directory, which is about 1MB. `helpers` has to stay a sibling of the suite,
36+
because `regression.py` finds it at `..`.
37+
38+
## 🐳 2. The Runner Image
39+
40+
```bash
41+
docker build -t regression-runner:local runner/
42+
```
43+
44+
## 🗂️ 3. The Images the Suite Starts
45+
46+
A machine reaches no registry, so everything the suite will run has to be in
47+
the local Docker under the names it asks for. `--prepare-env` builds and pulls
48+
them and runs no tests:
49+
50+
```bash
51+
docker run --rm \
52+
-v /var/run/docker.sock:/var/run/docker.sock \
53+
-v "$PWD/suite:$PWD/suite" -w "$PWD/suite" \
54+
regression-runner:local \
55+
python3 example/regression.py \
56+
--clickhouse docker://clickhouse/clickhouse-server:24.8.14.39-alpine \
57+
--prepare-env
58+
```
59+
60+
The `carried_*` services in `docker-compose.yml` name those images so the build
61+
packs them. They never start.
62+
63+
## 💾 4. The Disk
64+
65+
```bash
66+
machine disks build --compose . chreg --dry-run
67+
machine disks build --compose . chreg --size 6144
68+
machine sessions create --cpus 6 --mem 24G
69+
machine create --disk chreg --mem 8192 --cpus 2 --daemon
70+
until machine --timeout 0 wait <run> --for halted; do
71+
machine run <run> --iters 200000000 --mode free
72+
done
73+
machine console <run> -f
74+
```
75+
76+
`--dry-run` reports what the disk holds and builds nothing:
77+
78+
```
79+
content
80+
rootfs 285MB
81+
images (tar) 422MB
82+
images (loaded) 896MB
83+
project 1MB
84+
size
85+
used 1604MB
86+
capacity 3072MB
87+
```
88+
89+
Leaving `--size` out takes `used` plus a gigabyte, rounded up to a whole GB; a
90+
smaller `--size` is refused before anything uploads. The 6144 here is for what
91+
the suite itself writes, which no footprint predicts: it copies the 600MB
92+
clickhouse binary out of its own container.
93+
94+
The 8192MB machine needs a session with at least 24GB. `run` returns several
95+
times while the machine boots, so the loop keeps it running until the machine
96+
halts. The machine runs the project with `--abort-on-container-exit`, so it
97+
powers off when the suite exits and the run lands in `halted`.
98+
99+
## ➕ Another Suite
100+
101+
Copy that suite's directory into `suite/` as well and change `command:`. A
102+
suite whose environment names the same two images needs nothing else; one that
103+
starts ZooKeeper, Keeper or MinIO needs another `carried_*` entry and more
104+
memory. Run it on your own computer first: a suite that fails there fails in a
105+
machine for the same reason.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2026 Katteli Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# A test suite that drives docker compose ITSELF. `regression.py` brings up its
16+
# own project inside the guest, so the runner needs this machine's docker, and
17+
# the sources at the SAME path the daemon sees: compose resolves a bind source
18+
# on the daemon's filesystem, not in the container that asked for it.
19+
services:
20+
regression:
21+
image: regression-runner:local
22+
working_dir: /opt/compose
23+
volumes:
24+
- /var/run/docker.sock:/var/run/docker.sock
25+
- ./:/opt/compose
26+
command:
27+
- python3
28+
- suite/example/regression.py
29+
- --clickhouse
30+
- docker://clickhouse/clickhouse-server:24.8.14.39-alpine
31+
32+
# Carried, never started. The images the suite starts are named by no service
33+
# here, so `scale: 0` is how they reach the disk: the build packs every image
34+
# the project names, and compose starts none of these. Another suite that
35+
# needs ZooKeeper or MinIO adds another entry.
36+
carried_prepared:
37+
image: clickhouse-regression/clickhouse/clickhouse-server:24.8.14.39-alpine
38+
scale: 0
39+
carried_server:
40+
image: clickhouse/clickhouse-server:24.8.14.39-alpine
41+
scale: 0
42+
carried_ready:
43+
image: hello-world:latest
44+
scale: 0

0 commit comments

Comments
 (0)