From 17ae59ea48f58e6d119476a5739d5d3756d38911 Mon Sep 17 00:00:00 2001 From: pajjilykk Date: Tue, 12 May 2026 18:37:09 +0700 Subject: [PATCH] add hella vibecoded 3 --- 3/Makefile | 40 +++++++ 3/README.md | 31 +++++ 3/benchmark.py | 200 ++++++++++++++++++++++++++++++++ 3/exporter.py | 86 ++++++++++++++ 3/main.cpp | 301 +++++++++++++++++++++++++++++++++++++++++++++++++ 3/test_lab3.py | 126 +++++++++++++++++++++ 6 files changed, 784 insertions(+) create mode 100644 3/Makefile create mode 100644 3/README.md create mode 100644 3/benchmark.py create mode 100644 3/exporter.py create mode 100644 3/main.cpp create mode 100644 3/test_lab3.py diff --git a/3/Makefile b/3/Makefile new file mode 100644 index 0000000..b704bd0 --- /dev/null +++ b/3/Makefile @@ -0,0 +1,40 @@ +CXX := g++ +CXXFLAGS := -O2 -std=c++17 -Wall -Wextra -pedantic +TARGET := lab3 +OUT_DIR := out + +.PHONY: all clean run test bench timelines pack + +all: $(TARGET) + +$(TARGET): main.cpp + $(CXX) $(CXXFLAGS) main.cpp -o $(TARGET) + +run: $(TARGET) + ./$(TARGET) --size 100000 --depth 2 --min-size 4096 + +test: $(TARGET) + python3 test_lab3.py + ./$(TARGET) --size 0 --depth 3 --min-size 1 >/dev/null + ./$(TARGET) --size 1 --depth 3 --min-size 1 >/dev/null + ./$(TARGET) --size 10000 --depth 0 --min-size 1 >/dev/null + ./$(TARGET) --size 10000 --depth 2 --min-size 128 >/dev/null + ./$(TARGET) --size 10000 --depth 3 --min-size 256 --seed 2026 >/dev/null + ./$(TARGET) --size 12345 --depth 4 --min-size 257 --seed 777 >/dev/null + +bench: $(TARGET) + python3 benchmark.py + +timelines: $(TARGET) + mkdir -p $(OUT_DIR)/logs $(OUT_DIR)/pics + ./$(TARGET) --size 2048 --depth 2 --min-size 64 --log > $(OUT_DIR)/logs/depth2.log 2>$(OUT_DIR)/logs/depth2.stat + ./$(TARGET) --size 4096 --depth 3 --min-size 64 --log > $(OUT_DIR)/logs/depth3.log 2>$(OUT_DIR)/logs/depth3.stat + python3 exporter.py $(OUT_DIR)/logs/depth2.log $(OUT_DIR)/pics + python3 exporter.py $(OUT_DIR)/logs/depth3.log $(OUT_DIR)/pics + +pack: clean + zip -r lab3_process_pipes.zip main.cpp Makefile benchmark.py exporter.py test_lab3.py README.md + +clean: + rm -f $(TARGET) lab3_process_pipes.zip + rm -rf $(OUT_DIR) __pycache__ diff --git a/3/README.md b/3/README.md new file mode 100644 index 0000000..35c7cb9 --- /dev/null +++ b/3/README.md @@ -0,0 +1,31 @@ +# Лабораторная работа 3. Процессы. Неименованные каналы + +**Вариант 13: 72-01. Сортировка массива рекурсивным разделением.** + +Программа реализует сортировку массива рекурсивным разделением с построением дерева процессов. Каждый процесс получает часть массива, при разрешенной глубине рекурсии делит ее на две части, создает двух потомков через `fork()` и обменивается с ними данными через неименованные каналы `pipe()`. + +Входные данные во всех экспериментах считаются **полностью случайными**. Отдельного режима `mode` нет. Для повторяемости используется только параметр `--seed`. + +## Алгоритм + +1. Родитель генерирует массив случайных целых чисел. +2. На каждом рекурсивном шаге массив делится на левую и правую половины. +3. Для каждой половины создается дочерний процесс. +4. Родитель передает дочернему процессу данные в формате: + - `uint64_t count`; + - `count` значений типа `int32_t`. +5. Потомок сортирует полученную часть тем же алгоритмом. +6. При достижении `max_depth` или `min_size` сортировка выполняется локально без новых процессов. +7. Потомок возвращает результат родителю в формате: + - `uint64_t count`; + - `count` отсортированных значений; + - `uint64_t processes` — число процессов в поддереве. +8. Родитель выполняет слияние двух отсортированных частей. + +Для одного потомка используются два канала: родитель → потомок и потомок → родитель. Так как потомков два, на рекурсивном узле создается четыре канала. + +## Сборка и запуск + +```bash +make +./lab3 --size 100000 --depth 2 --min-size 4096 diff --git a/3/benchmark.py b/3/benchmark.py new file mode 100644 index 0000000..213f02c --- /dev/null +++ b/3/benchmark.py @@ -0,0 +1,200 @@ +import csv +import os +import re +import subprocess + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +BIN = "./lab3" +OUT = "out" +STAT_RE = re.compile( + r"STAT:.*size=(\d+).*depth=(\d+).*min_size=(\d+).*processes=(\d+).*valid=(\d+).*time=([\d.]+)" +) + + +def run_once(size, depth, min_size, seed): + cmd = [ + BIN, + "--size", + str(size), + "--depth", + str(depth), + "--min-size", + str(min_size), + "--seed", + str(seed), + ] + p = subprocess.run( + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True + ) + if p.returncode != 0: + raise RuntimeError(p.stderr) + m = STAT_RE.search(p.stderr) + if not m: + raise RuntimeError(f"STAT not found: {p.stderr}") + if m.group(5) != "1": + raise RuntimeError(f"sort validation failed: {p.stderr}") + return { + "size": int(m.group(1)), + "depth": int(m.group(2)), + "min_size": int(m.group(3)), + "processes": int(m.group(4)), + "time": float(m.group(6)), + } + + +def seed_for(size, depth, min_size, salt): + # Для каждой точки используется свой seed, поэтому вход всегда случайный, + # но результаты можно воспроизвести. + return 2026 + salt * 1_000_003 + size * 17 + depth * 1009 + min_size * 31 + + +def save_csv(path, rows, header): + with open(path, "w", encoding="utf-8", newline="") as f: + w = csv.DictWriter(f, fieldnames=header) + w.writeheader() + w.writerows(rows) + + +def plot_depth_scaling(): + os.makedirs(f"{OUT}/pics", exist_ok=True) + + # ВАЖНО: на графиках по глубине ровно 30 точек по оси X: 0..29. + # + # depth идет до 29, но реально число процессов не взорвется бесконечно, + # потому что дальнейшее деление останавливает min_size. + depths = list(range(30)) + + # Несколько размеров дают несколько линий на одном графике. + sizes = [50_000, 100_000, 200_000] + min_size = 4096 + + rows = [] + for size in sizes: + for d in depths: + seed = seed_for(size, d, min_size, salt=1) + r = run_once(size, d, min_size, seed) + row = {**r, "seed": seed} + rows.append(row) + print( + f"size={size} depth={d} min_size={min_size} " + f"seed={seed} processes={r['processes']} time={r['time']:.6f}", + flush=True, + ) + + plt.figure(figsize=(12, 6)) + for size in sizes: + cur = [r for r in rows if r["size"] == size] + plt.plot( + [r["depth"] for r in cur], + [r["time"] for r in cur], + marker="o", + label=f"N={size}", + ) + plt.xlabel("Глубина порождения процессов") + plt.ylabel("Время, сек") + plt.title("Зависимость времени сортировки от глубины fork-рекурсии") + plt.grid(True) + plt.legend() + plt.tight_layout() + plt.savefig(f"{OUT}/pics/time_by_depth.png") + plt.close() + + plt.figure(figsize=(12, 6)) + for size in sizes: + cur = [r for r in rows if r["size"] == size] + base_time = cur[0]["time"] + speedup = [base_time / r["time"] if r["time"] > 0 else 0 for r in cur] + plt.plot( + [r["depth"] for r in cur], + speedup, + marker="s", + label=f"N={size}", + ) + plt.xlabel("Глубина порождения процессов") + plt.ylabel("Ускорение относительно depth=0") + plt.title("Ускорение при использовании процессов") + plt.grid(True) + plt.legend() + plt.tight_layout() + plt.savefig(f"{OUT}/pics/speedup_by_depth.png") + plt.close() + + plt.figure(figsize=(12, 6)) + for size in sizes: + cur = [r for r in rows if r["size"] == size] + plt.plot( + [r["depth"] for r in cur], + [r["processes"] for r in cur], + marker="^", + label=f"N={size}", + ) + plt.xlabel("Глубина порождения процессов") + plt.ylabel("Количество процессов") + plt.title("Размер дерева процессов") + plt.grid(True) + plt.legend() + plt.tight_layout() + plt.savefig(f"{OUT}/pics/process_count_by_depth.png") + plt.close() + + save_csv( + f"{OUT}/benchmark_depth.csv", + rows, + ["size", "depth", "min_size", "seed", "processes", "time"], + ) + return rows + + +def plot_threshold_effect(): + os.makedirs(f"{OUT}/pics", exist_ok=True) + + # ВАЖНО: на графике по min_size ровно 30 точек по оси X. + size = 200_000 + depth = 5 + + # 30 значений порога от 128 до 131072, примерно равномерно по log2-шкале. + min_sizes = [round(2 ** (7 + i * (10 / 29))) for i in range(30)] + + rows = [] + for m in min_sizes: + seed = seed_for(size, depth, m, salt=2) + r = run_once(size, depth, m, seed) + row = {**r, "seed": seed} + rows.append(row) + print( + f"size={size} depth={depth} min_size={m} " + f"seed={seed} processes={r['processes']} time={r['time']:.6f}", + flush=True, + ) + + plt.figure(figsize=(12, 6)) + plt.plot( + [r["min_size"] for r in rows], + [r["time"] for r in rows], + marker="o", + ) + plt.xscale("log", base=2) + plt.xlabel("Минимальный размер части для fork") + plt.ylabel("Время, сек") + plt.title("Влияние порога min_size на производительность") + plt.grid(True) + plt.tight_layout() + plt.savefig(f"{OUT}/pics/time_by_min_size.png") + plt.close() + + save_csv( + f"{OUT}/benchmark_min_size.csv", + rows, + ["size", "depth", "min_size", "seed", "processes", "time"], + ) + return rows + + +if __name__ == "__main__": + plot_depth_scaling() + plot_threshold_effect() + print(f"Графики сохранены в {OUT}/pics. На каждом графике ровно 30 точек по оси X.") diff --git a/3/exporter.py b/3/exporter.py new file mode 100644 index 0000000..2a704da --- /dev/null +++ b/3/exporter.py @@ -0,0 +1,86 @@ +import os +import re +import sys +from collections import defaultdict + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +if len(sys.argv) < 3: + print("Использование: python3 exporter.py ") + sys.exit(1) + +logfile = sys.argv[1] +out_dir = sys.argv[2] +os.makedirs(out_dir, exist_ok=True) +base = os.path.splitext(os.path.basename(logfile))[0] + +pattern = re.compile( + r"(START|END) PID=(\d+) PPID=(\d+) depth=(\d+) size=(\d+) time=([\d.]+)" +) +events = defaultdict(dict) + +with open(logfile, encoding="utf-8") as f: + for line in f: + m = pattern.search(line) + if not m: + continue + typ, pid, ppid, depth, size, t = m.groups() + key = (int(pid), int(depth), int(size)) + events[key][typ] = float(t) + events[key]["pid"] = int(pid) + events[key]["ppid"] = int(ppid) + events[key]["depth"] = int(depth) + events[key]["size"] = int(size) + +rows = [] +for v in events.values(): + if "START" in v and "END" in v: + rows.append(v) + +if not rows: + print("В логе нет полных START/END событий") + sys.exit(1) + +rows.sort(key=lambda r: (r["START"], r["depth"], r["pid"])) +t0 = min(r["START"] for r in rows) + +# 1. Временная диаграмма: видно параллельность и время жизни каждого процесса. +plt.figure(figsize=(12, max(5, len(rows) * 0.35))) +for y, r in enumerate(rows): + start = r["START"] - t0 + end = r["END"] - t0 + plt.plot([start, end], [y, y], linewidth=5) + plt.text( + end, + y, + f" pid={r['pid']} d={r['depth']} n={r['size']}", + va="center", + fontsize=8, + ) + +plt.xlabel("Время от начала, сек") +plt.ylabel("Процессы/задачи сортировки") +plt.title(f"Временная диаграмма процессов: {base}") +plt.grid(True) +plt.tight_layout() +plt.savefig(os.path.join(out_dir, f"{base}_timeline.png")) +plt.close() + +# 2. Гистограмма глубин: проверка, что дерево дошло до нужной глубины. +by_depth = defaultdict(int) +for r in rows: + by_depth[r["depth"]] += 1 + +plt.figure(figsize=(8, 5)) +xs = sorted(by_depth) +plt.bar(xs, [by_depth[x] for x in xs]) +plt.xlabel("Глубина рекурсии") +plt.ylabel("Количество процессов") +plt.title(f"Распределение процессов по глубине: {base}") +plt.grid(True, axis="y") +plt.tight_layout() +plt.savefig(os.path.join(out_dir, f"{base}_depth_hist.png")) +plt.close() diff --git a/3/main.cpp b/3/main.cpp new file mode 100644 index 0000000..9b1f466 --- /dev/null +++ b/3/main.cpp @@ -0,0 +1,301 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Лабораторная работа 3. Процессы. Неименованные каналы. +// Вариант 13: 72-01. Сортировка массива рекурсивным разделением. +// +// Идея: родительский процесс делит массив на две части, создает двух потомков, +// передает им части массива через pipe, потомки сортируют свои части тем же +// алгоритмом до заданной глубины. При достижении max_depth или min_size сортировка +// выполняется локально в одном процессе. Обратно потомки возвращают отсортированные +// массивы тоже через pipe. Родитель выполняет слияние. + +using i32 = int32_t; +using u64 = uint64_t; + +struct Options { + size_t size = 100000; + int max_depth = 2; + size_t min_size = 4096; + unsigned seed = 1337; + bool print = false; + bool log = false; +}; + +struct SortResult { + std::vector data; + u64 processes = 1; // текущий процесс тоже считается +}; + +static double now_seconds() { + using clock = std::chrono::steady_clock; + static const auto start = clock::now(); + auto t = clock::now() - start; + return std::chrono::duration(t).count(); +} + +static void log_event(const char* type, int depth, size_t n) { + // Одна строка короче PIPE_BUF, поэтому при выводе в общий файл обычно не рвется. + std::ostringstream ss; + ss << type + << " PID=" << static_cast(getpid()) + << " PPID=" << static_cast(getppid()) + << " depth=" << depth + << " size=" << n + << " time=" << now_seconds() + << "\n"; + const std::string s = ss.str(); + (void)!write(STDOUT_FILENO, s.data(), s.size()); +} + +[[noreturn]] static void die_child(const std::string& msg) { + std::cerr << "CHILD_ERROR pid=" << getpid() << " " << msg << "\n"; + _exit(2); +} + +static void throw_errno(const std::string& what) { + throw std::runtime_error(what + ": " + std::strerror(errno)); +} + +static void write_all(int fd, const void* ptr, size_t bytes) { + const char* p = static_cast(ptr); + while (bytes > 0) { + ssize_t w = write(fd, p, bytes); + if (w < 0) { + if (errno == EINTR) continue; + throw_errno("write"); + } + if (w == 0) throw std::runtime_error("write returned 0"); + p += w; + bytes -= static_cast(w); + } +} + +static void read_all(int fd, void* ptr, size_t bytes) { + char* p = static_cast(ptr); + while (bytes > 0) { + ssize_t r = read(fd, p, bytes); + if (r < 0) { + if (errno == EINTR) continue; + throw_errno("read"); + } + if (r == 0) throw std::runtime_error("unexpected EOF in pipe"); + p += r; + bytes -= static_cast(r); + } +} + +static void close_checked(int fd) { + if (fd >= 0) { + while (close(fd) < 0 && errno == EINTR) {} + } +} + +static void send_vector(int fd, const std::vector& a) { + u64 n = static_cast(a.size()); + write_all(fd, &n, sizeof(n)); + if (!a.empty()) write_all(fd, a.data(), a.size() * sizeof(i32)); +} + +static std::vector recv_vector(int fd) { + u64 n = 0; + read_all(fd, &n, sizeof(n)); + if (n > static_cast(SIZE_MAX / sizeof(i32))) { + throw std::runtime_error("too large vector in pipe"); + } + std::vector a(static_cast(n)); + if (!a.empty()) read_all(fd, a.data(), a.size() * sizeof(i32)); + return a; +} + +static void send_result(int fd, const SortResult& result) { + send_vector(fd, result.data); + write_all(fd, &result.processes, sizeof(result.processes)); +} + +static SortResult recv_result(int fd) { + SortResult r; + r.data = recv_vector(fd); + read_all(fd, &r.processes, sizeof(r.processes)); + return r; +} + +static std::vector merge_sorted(const std::vector& left, const std::vector& right) { + std::vector out; + out.reserve(left.size() + right.size()); + size_t i = 0, j = 0; + while (i < left.size() && j < right.size()) { + if (left[i] <= right[j]) out.push_back(left[i++]); + else out.push_back(right[j++]); + } + out.insert(out.end(), left.begin() + static_cast(i), left.end()); + out.insert(out.end(), right.begin() + static_cast(j), right.end()); + return out; +} + +static void sequential_recursive_sort(std::vector& a) { + if (a.size() < 2) return; + const size_t mid = a.size() / 2; + std::vector left(a.begin(), a.begin() + static_cast(mid)); + std::vector right(a.begin() + static_cast(mid), a.end()); + sequential_recursive_sort(left); + sequential_recursive_sort(right); + a = merge_sorted(left, right); +} + +static SortResult process_recursive_sort(std::vector a, int depth, const Options& opt); + +static pid_t spawn_sort_child(const std::vector& part, + int child_depth, + const Options& opt, + int& result_read_fd) { + int to_child[2] = {-1, -1}; + int from_child[2] = {-1, -1}; + if (pipe(to_child) < 0) throw_errno("pipe to_child"); + if (pipe(from_child) < 0) throw_errno("pipe from_child"); + + pid_t pid = fork(); + if (pid < 0) throw_errno("fork"); + + if (pid == 0) { + try { + close_checked(to_child[1]); + close_checked(from_child[0]); + std::vector input = recv_vector(to_child[0]); + close_checked(to_child[0]); + SortResult result = process_recursive_sort(std::move(input), child_depth, opt); + send_result(from_child[1], result); + close_checked(from_child[1]); + _exit(0); + } catch (const std::exception& e) { + die_child(e.what()); + } + } + + close_checked(to_child[0]); + close_checked(from_child[1]); + send_vector(to_child[1], part); + close_checked(to_child[1]); + result_read_fd = from_child[0]; + return pid; +} + +static SortResult process_recursive_sort(std::vector a, int depth, const Options& opt) { + if (opt.log) log_event("START", depth, a.size()); + + if (a.size() < 2 || depth >= opt.max_depth || a.size() <= opt.min_size) { + sequential_recursive_sort(a); + if (opt.log) log_event("END", depth, a.size()); + return {std::move(a), 1}; + } + + const size_t mid = a.size() / 2; + std::vector left(a.begin(), a.begin() + static_cast(mid)); + std::vector right(a.begin() + static_cast(mid), a.end()); + + int left_fd = -1, right_fd = -1; + pid_t left_pid = spawn_sort_child(left, depth + 1, opt, left_fd); + pid_t right_pid = spawn_sort_child(right, depth + 1, opt, right_fd); + + SortResult left_result = recv_result(left_fd); + SortResult right_result = recv_result(right_fd); + close_checked(left_fd); + close_checked(right_fd); + + int status_left = 0, status_right = 0; + while (waitpid(left_pid, &status_left, 0) < 0 && errno == EINTR) {} + while (waitpid(right_pid, &status_right, 0) < 0 && errno == EINTR) {} + if (!WIFEXITED(status_left) || WEXITSTATUS(status_left) != 0) { + throw std::runtime_error("left child failed"); + } + if (!WIFEXITED(status_right) || WEXITSTATUS(status_right) != 0) { + throw std::runtime_error("right child failed"); + } + + SortResult result; + result.data = merge_sorted(left_result.data, right_result.data); + result.processes = 1 + left_result.processes + right_result.processes; + + if (opt.log) log_event("END", depth, result.data.size()); + return result; +} + +static Options parse_args(int argc, char** argv) { + Options opt; + for (int i = 1; i < argc; ++i) { + std::string s = argv[i]; + auto need_value = [&](const std::string& name) -> std::string { + if (i + 1 >= argc) throw std::runtime_error("missing value for " + name); + return argv[++i]; + }; + if (s == "--size" || s == "-n") opt.size = std::stoull(need_value(s)); + else if (s == "--depth" || s == "-d") opt.max_depth = std::stoi(need_value(s)); + else if (s == "--min-size" || s == "-m") opt.min_size = std::stoull(need_value(s)); + else if (s == "--seed") opt.seed = static_cast(std::stoul(need_value(s))); + else if (s == "--print") opt.print = true; + else if (s == "--log") opt.log = true; + else if (s == "--help" || s == "-h") { + std::cout << "Usage: ./lab3 [--size N] [--depth D] [--min-size M] [--seed S] " + << "[--print] [--log]\n"; + std::exit(0); + } else { + throw std::runtime_error("unknown argument: " + s); + } + } + if (opt.max_depth < 0) throw std::runtime_error("depth must be non-negative"); + return opt; +} + +static std::vector generate_data(const Options& opt) { + // По условию этой версии лабораторной входные данные всегда считаются + // полностью случайными. Для повторяемости экспериментов используется --seed. + std::vector a(opt.size); + std::mt19937 rng(opt.seed); + std::uniform_int_distribution dist(-100000000, 100000000); + for (auto& x : a) x = dist(rng); + return a; +} + +int main(int argc, char** argv) { + try { + Options opt = parse_args(argc, argv); + std::vector data = generate_data(opt); + + const auto t1 = std::chrono::steady_clock::now(); + SortResult result = process_recursive_sort(std::move(data), 0, opt); + const auto t2 = std::chrono::steady_clock::now(); + const double elapsed = std::chrono::duration(t2 - t1).count(); + const bool ok = std::is_sorted(result.data.begin(), result.data.end()); + + if (opt.print) { + for (size_t i = 0; i < result.data.size(); ++i) { + if (i) std::cout << ' '; + std::cout << result.data[i]; + } + std::cout << '\n'; + } + + std::cerr << "STAT: size=" << opt.size + << " depth=" << opt.max_depth + << " min_size=" << opt.min_size + << " processes=" << result.processes + << " valid=" << (ok ? 1 : 0) + << " time=" << elapsed << " sec\n"; + return ok ? 0 : 3; + } catch (const std::exception& e) { + std::cerr << "ERROR: " << e.what() << "\n"; + return 1; + } +} diff --git a/3/test_lab3.py b/3/test_lab3.py new file mode 100644 index 0000000..585c97e --- /dev/null +++ b/3/test_lab3.py @@ -0,0 +1,126 @@ +import re +import subprocess + +BIN = "./lab3" +STAT_RE = re.compile( + r"STAT:.*size=(\d+).*depth=(\d+).*min_size=(\d+).*processes=(\d+).*valid=(\d+).*time=([\d.]+)" +) + + +def run(args): + p = subprocess.run([BIN, *args], text=True, capture_output=True) + if p.returncode != 0: + print("STDOUT:\n", p.stdout) + print("STDERR:\n", p.stderr) + raise AssertionError(f"command failed: {args}") + m = STAT_RE.search(p.stderr) + assert m and m.group(5) == "1", f"bad stat: {p.stderr}" + return p, m + + +def ints(s): + return [int(x) for x in s.split()] if s.strip() else [] + + +def test_01_printed_small_random_array_sorted(): + p, _ = run(["--size", "97", "--depth", "3", "--min-size", "8", "--print"]) + a = ints(p.stdout) + assert len(a) == 97 + assert a == sorted(a) + + +def test_02_seed_makes_random_input_reproducible(): + a = run( + ["--size", "128", "--depth", "2", "--min-size", "8", "--seed", "42", "--print"] + )[0].stdout + b = run( + ["--size", "128", "--depth", "2", "--min-size", "8", "--seed", "42", "--print"] + )[0].stdout + c = run( + ["--size", "128", "--depth", "2", "--min-size", "8", "--seed", "43", "--print"] + )[0].stdout + assert a == b + assert a != c + + +def test_03_zero_depth_sequential_fallback(): + _, m = run(["--size", "1000", "--depth", "0", "--min-size", "1"]) + assert m.group(4) == "1" + + +def test_04_depth_two_full_tree_process_count(): + _, m = run(["--size", "4096", "--depth", "2", "--min-size", "16"]) + assert m.group(4) == "7", m.group(0) + + +def test_05_depth_three_full_tree_process_count(): + _, m = run(["--size", "8192", "--depth", "3", "--min-size", "16"]) + assert m.group(4) == "15", m.group(0) + + +def test_06_min_size_stops_forking(): + _, m = run(["--size", "1000", "--depth", "5", "--min-size", "1000"]) + assert m.group(4) == "1", m.group(0) + + +def test_07_odd_size_sorted_correctly(): + p, _ = run( + ["--size", "999", "--depth", "4", "--min-size", "17", "--seed", "77", "--print"] + ) + a = ints(p.stdout) + assert len(a) == 999 + assert a == sorted(a) + + +def test_08_single_element_array(): + p, m = run(["--size", "1", "--depth", "5", "--min-size", "1", "--print"]) + a = ints(p.stdout) + assert len(a) == 1 + assert m.group(4) == "1" + + +def test_09_empty_array(): + p, m = run(["--size", "0", "--depth", "5", "--min-size", "1", "--print"]) + assert p.stdout.strip() == "" + assert m.group(4) == "1" + + +def test_10_timeline_log_has_events_and_pid_fields(): + p, _ = run(["--size", "128", "--depth", "2", "--min-size", "8", "--log"]) + assert "START PID=" in p.stdout + assert "END PID=" in p.stdout + assert "PPID=" in p.stdout + assert "depth=" in p.stdout + + +def test_11_help_has_no_mode_argument(): + p = subprocess.run([BIN, "--help"], text=True, capture_output=True) + assert p.returncode == 0 + assert "--mode" not in p.stdout + assert "--seed" in p.stdout + + +def test_12_unknown_mode_is_rejected(): + p = subprocess.run([BIN, "--mode", "random"], text=True, capture_output=True) + assert p.returncode != 0 + assert "unknown argument" in p.stderr + + +if __name__ == "__main__": + tests = [ + test_01_printed_small_random_array_sorted, + test_02_seed_makes_random_input_reproducible, + test_03_zero_depth_sequential_fallback, + test_04_depth_two_full_tree_process_count, + test_05_depth_three_full_tree_process_count, + test_06_min_size_stops_forking, + test_07_odd_size_sorted_correctly, + test_08_single_element_array, + test_09_empty_array, + test_10_timeline_log_has_events_and_pid_fields, + test_11_help_has_no_mode_argument, + test_12_unknown_mode_is_rejected, + ] + for t in tests: + t() + print(f"OK {t.__name__}")