From 14ca5b35d0fc477ba30f10b80f937b523fd7e930 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Sat, 21 Mar 2026 21:43:22 +0100 Subject: Interprocess pipe support (for stdout/stderr capture) (#866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **RAII pipe handles for child process stdout/stderr capture**: `StdoutPipeHandles` is now a proper RAII type with automatic cleanup, move semantics, and partial close support. This makes it safe to use pipes for capturing child process output without risking handle/fd leaks. - **Optional separate stderr pipe**: `CreateProcOptions` now accepts a `StderrPipe` field so callers can capture stdout and stderr independently. When null (default), stderr shares the stdout pipe as before. - **LogStreamListener with pluggable handler**: The TCP log stream listener accepts connections from remote processes and delivers parsed log lines through a `LogStreamHandler` interface, set dynamically via `SetHandler()`. This allows any client to receive log messages without depending on a specific console implementation. - **TcpLogStreamSink for zen::logging**: A logging sink that forwards log messages to a `LogStreamListener` over TCP, using the native `zen::logging::Sink` infrastructure with proper thread-safe synchronization. - **Reliable child process exit codes on Linux**: `waitpid` result handling is fixed so `ProcessHandle::GetExitCode()` returns the real exit code. `ProcessHandle::Reset()` reaps zombies directly, replacing the global `IgnoreChildSignals()` which prevented exit code collection entirely. Also fixes a TOCTOU race in `ProcessHandle::Wait()` on Linux/Mac. - **Pipe capture test suite**: Tests covering stdout/stderr capture via pipes (both shared and separate modes), RAII cleanup, move semantics, and exit code propagation using `zentest-appstub` as the child process. - **Service command integration tests**: Shell-based integration tests for `zen service` covering the full lifecycle (install, status, start, stop, uninstall) on all three platforms — Linux (systemd), macOS (launchd), and Windows (SCM via PowerShell). - **Test script reorganization**: Platform-specific test scripts moved from `scripts/test_scripts/` into `scripts/test_linux/`, `test_mac/`, and `test_windows/`. --- scripts/test_mac/block-clone-test.sh | 43 ++++ scripts/test_mac/service-test.sh | 368 +++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100755 scripts/test_mac/block-clone-test.sh create mode 100755 scripts/test_mac/service-test.sh (limited to 'scripts/test_mac') diff --git a/scripts/test_mac/block-clone-test.sh b/scripts/test_mac/block-clone-test.sh new file mode 100755 index 000000000..1a9575dcf --- /dev/null +++ b/scripts/test_mac/block-clone-test.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Test block-clone functionality on macOS (APFS). +# +# APFS is the default filesystem on modern Macs and natively supports +# clonefile(), so no special setup is needed — just run the tests. +# +# Usage: +# ./scripts/test_mac/block-clone-test.sh [path-to-zencore-test] +# +# If no path is given, defaults to build/macosx//debug/zencore-test +# relative to the repository root. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +ARCH="$(uname -m)" +TEST_BINARY="${1:-$REPO_ROOT/build/macosx/$ARCH/debug/zencore-test}" + +if [ ! -x "$TEST_BINARY" ]; then + echo "error: test binary not found or not executable: $TEST_BINARY" >&2 + echo "hint: build with 'xmake config -m debug && xmake build zencore-test'" >&2 + exit 1 +fi + +# Verify we're on APFS +BINARY_DIR="$(dirname "$TEST_BINARY")" +FS_TYPE="$(diskutil info "$(df "$BINARY_DIR" | tail -1 | awk '{print $1}')" 2>/dev/null | grep "Type (Bundle)" | awk '{print $NF}' || true)" + +if [ "$FS_TYPE" != "apfs" ]; then + echo "warning: filesystem does not appear to be APFS (got: ${FS_TYPE:-unknown}), clone tests may skip" >&2 +fi + +TEST_CASES="TryCloneFile,CopyFile.Clone,SupportsBlockRefCounting,CloneQueryInterface" + +echo "Running block-clone tests ..." +echo "---" +"$TEST_BINARY" \ + --test-suite="core.filesystem" \ + --test-case="$TEST_CASES" +echo "---" +echo "All block-clone tests passed." diff --git a/scripts/test_mac/service-test.sh b/scripts/test_mac/service-test.sh new file mode 100755 index 000000000..da4a966e7 --- /dev/null +++ b/scripts/test_mac/service-test.sh @@ -0,0 +1,368 @@ +#!/usr/bin/env bash +# Test zen service command lifecycle on macOS (launchd). +# +# Does NOT require sudo — macOS service commands run as the current user +# via ~/Library/LaunchAgents. +# +# Usage: +# ./scripts/test_mac/service-test.sh [path-to-zen] +# +# If no path is given, defaults to build/macosx//debug/zen +# relative to the repository root. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +ARCH="$(uname -m)" +ZEN_BINARY="${1:-$REPO_ROOT/build/macosx/$ARCH/debug/zen}" +ZENSERVER_BINARY="$(dirname "$ZEN_BINARY")/zenserver" +SERVICE_NAME="ZenServerTest-$$" +DAEMON_NAME="com.epicgames.unreal.${SERVICE_NAME}" +PLIST_FILE="$HOME/Library/LaunchAgents/${DAEMON_NAME}.plist" + +PASSED=0 +FAILED=0 +TESTS_RUN=0 + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +cleanup() { + local exit_code=$? + set +e + + echo "" + echo "--- Cleanup ---" + + # Bootout (stop) the agent if loaded + if launchctl list "$DAEMON_NAME" &>/dev/null; then + echo "Stopping test service..." + launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true + fi + + # Remove plist file + if [ -f "$PLIST_FILE" ]; then + echo "Removing test plist file..." + rm -f "$PLIST_FILE" + fi + + echo "" + echo "==============================" + printf " Tests run: %d\n" "$TESTS_RUN" + printf " ${GREEN}Passed: %d${NC}\n" "$PASSED" + if [ "$FAILED" -gt 0 ]; then + printf " ${RED}Failed: %d${NC}\n" "$FAILED" + else + printf " Failed: %d\n" "$FAILED" + fi + echo "==============================" + + if [ "$FAILED" -gt 0 ]; then + exit 1 + fi + exit "$exit_code" +} + +trap cleanup EXIT + +pass() { + TESTS_RUN=$((TESTS_RUN + 1)) + PASSED=$((PASSED + 1)) + printf " ${GREEN}PASS${NC}: %s\n" "$1" +} + +fail() { + TESTS_RUN=$((TESTS_RUN + 1)) + FAILED=$((FAILED + 1)) + printf " ${RED}FAIL${NC}: %s\n" "$1" + if [ -n "${2:-}" ]; then + printf " %s\n" "$2" + fi +} + +# ── Preflight checks ────────────────────────────────────────────── + +if [ "$(uname)" != "Darwin" ]; then + echo "Error: this test is for macOS only." + exit 1 +fi + +if ! command -v launchctl &>/dev/null; then + echo "Error: launchctl not found." + exit 1 +fi + +if [ ! -x "$ZEN_BINARY" ]; then + echo "Error: zen binary not found at '$ZEN_BINARY'" + echo "Build with: xmake config -m debug && xmake build zen" + exit 1 +fi + +if [ ! -x "$ZENSERVER_BINARY" ]; then + echo "Error: zenserver binary not found at '$ZENSERVER_BINARY'" + echo "Build with: xmake config -m debug && xmake build zenserver" + exit 1 +fi + +echo "zen binary: $ZEN_BINARY" +echo "zenserver binary: $ZENSERVER_BINARY" +echo "service name: $SERVICE_NAME" +echo "daemon name: $DAEMON_NAME" +echo "plist path: $PLIST_FILE" +echo "" + +# ── Test: status before install (should fail) ───────────────────── + +echo "--- Test: status before install ---" + +OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true) +if echo "$OUTPUT" | grep -qi "not installed"; then + pass "status reports 'not installed' for non-existent service" +else + fail "status should report 'not installed'" "got: $OUTPUT" +fi + +# ── Test: install ───────────────────────────────────────────────── + +echo "--- Test: install ---" + +OUTPUT=$("$ZEN_BINARY" service install "$ZENSERVER_BINARY" "$SERVICE_NAME" 2>&1) +EXIT_CODE=$? + +if [ "$EXIT_CODE" -eq 0 ]; then + pass "install exits with code 0" +else + fail "install exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT" +fi + +if [ -f "$PLIST_FILE" ]; then + pass "plist file created at $PLIST_FILE" +else + fail "plist file created at $PLIST_FILE" +fi + +# Verify plist contents +if [ -f "$PLIST_FILE" ]; then + if grep -q "$ZENSERVER_BINARY" "$PLIST_FILE"; then + pass "plist contains zenserver executable path" + else + fail "plist contains zenserver executable path" "$(cat "$PLIST_FILE")" + fi + + if grep -q "Label" "$PLIST_FILE"; then + pass "plist has Label key" + else + fail "plist has Label key" + fi + + if grep -q "$DAEMON_NAME" "$PLIST_FILE"; then + pass "plist contains correct daemon name" + else + fail "plist contains correct daemon name" + fi + + if grep -q "ProgramArguments" "$PLIST_FILE"; then + pass "plist has ProgramArguments" + else + fail "plist has ProgramArguments" + fi +fi + +# ── Test: install again (already installed, no --full) ──────────── + +echo "--- Test: install again (idempotent) ---" + +OUTPUT=$("$ZEN_BINARY" service install "$ZENSERVER_BINARY" "$SERVICE_NAME" 2>&1) +EXIT_CODE=$? + +if [ "$EXIT_CODE" -eq 0 ]; then + pass "re-install exits with code 0" +else + fail "re-install exits with code 0" "got exit code: $EXIT_CODE" +fi + +if echo "$OUTPUT" | grep -qi "already installed"; then + pass "re-install reports service already installed" +else + fail "re-install reports service already installed" "got: $OUTPUT" +fi + +# ── Test: status after install (not yet started) ────────────────── + +echo "--- Test: status after install (stopped) ---" + +OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true) +if echo "$OUTPUT" | grep -qi "not running"; then + pass "status reports 'not running' for stopped service" +else + fail "status reports 'not running' for stopped service" "got: $OUTPUT" +fi + +# ── Test: start ─────────────────────────────────────────────────── + +echo "--- Test: start ---" + +OUTPUT=$("$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1) +EXIT_CODE=$? + +if [ "$EXIT_CODE" -eq 0 ]; then + pass "start exits with code 0" +else + fail "start exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT" +fi + +# Give the service a moment to start +sleep 2 + +if launchctl list "$DAEMON_NAME" &>/dev/null; then + pass "service is loaded after start" + + # ── Test: status while running ──────────────────────────────── + + echo "--- Test: status (running) ---" + + OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true) + if echo "$OUTPUT" | grep -qi "Running"; then + pass "status reports 'Running'" + else + fail "status reports 'Running'" "got: $OUTPUT" + fi + + if echo "$OUTPUT" | grep -qi "zenserver"; then + pass "status shows executable path" + else + fail "status shows executable path" "got: $OUTPUT" + fi + + # ── Test: start again (already running) ─────────────────────── + + echo "--- Test: start again (already running) ---" + + OUTPUT=$("$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1) + if echo "$OUTPUT" | grep -qi "already running"; then + pass "start reports service already running" + else + fail "start reports service already running" "got: $OUTPUT" + fi + + # ── Test: stop ──────────────────────────────────────────────── + + echo "--- Test: stop ---" + + OUTPUT=$("$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1) + EXIT_CODE=$? + + if [ "$EXIT_CODE" -eq 0 ]; then + pass "stop exits with code 0" + else + fail "stop exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT" + fi + + sleep 1 + + if ! launchctl list "$DAEMON_NAME" &>/dev/null; then + pass "service is unloaded after stop" + else + fail "service is unloaded after stop" + fi +else + fail "service is loaded after start" "(skipping start-dependent tests)" +fi + +# ── Test: stop when already stopped ─────────────────────────────── + +echo "--- Test: stop when already stopped ---" + +# Make sure it's stopped first +launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true +sleep 1 + +OUTPUT=$("$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1 || true) +if echo "$OUTPUT" | grep -qi "not running"; then + pass "stop reports 'not running' when already stopped" +else + fail "stop reports 'not running' when already stopped" "got: $OUTPUT" +fi + +# ── Test: uninstall while running (should fail) ─────────────────── + +echo "--- Test: uninstall while running (should fail) ---" + +# Start the service so we can test uninstall-while-running +"$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1 || true +sleep 2 + +if launchctl list "$DAEMON_NAME" &>/dev/null; then + OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1 || true) + EXIT_CODE=$? + + if [ "$EXIT_CODE" -ne 0 ] || echo "$OUTPUT" | grep -qi "running.*stop"; then + pass "uninstall refuses while service is running" + else + fail "uninstall refuses while service is running" "got: exit=$EXIT_CODE, output: $OUTPUT" + fi + + # Stop it for the real uninstall test + "$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1 || true + launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true + sleep 1 +else + echo " (skipped: could not start service for this test)" +fi + +# ── Test: uninstall ─────────────────────────────────────────────── + +echo "--- Test: uninstall ---" + +# Ensure stopped +launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true +sleep 1 + +OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1) +EXIT_CODE=$? + +if [ "$EXIT_CODE" -eq 0 ]; then + pass "uninstall exits with code 0" +else + fail "uninstall exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT" +fi + +if [ ! -f "$PLIST_FILE" ]; then + pass "plist file removed after uninstall" +else + fail "plist file removed after uninstall" +fi + +# ── Test: status after uninstall ────────────────────────────────── + +echo "--- Test: status after uninstall ---" + +OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true) +if echo "$OUTPUT" | grep -qi "not installed"; then + pass "status reports 'not installed' after uninstall" +else + fail "status reports 'not installed' after uninstall" "got: $OUTPUT" +fi + +# ── Test: uninstall when not installed (idempotent) ─────────────── + +echo "--- Test: uninstall when not installed ---" + +OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1) +EXIT_CODE=$? + +if [ "$EXIT_CODE" -eq 0 ]; then + pass "uninstall of non-existent service exits with code 0" +else + fail "uninstall of non-existent service exits with code 0" "got exit code: $EXIT_CODE" +fi + +if echo "$OUTPUT" | grep -qi "not installed"; then + pass "uninstall reports service not installed" +else + fail "uninstall reports service not installed" "got: $OUTPUT" +fi -- cgit v1.2.3