#!/bin/bash # test_runner.sh - 中核実行器(強制使用) # スモークテストv2の核心ライブラリ # set -eは使わない(個々のテストが失敗しても全体を続行するため) set -uo pipefail # ルート/バイナリ検出(CWDに依存しない実行を保証) if [ -z "${NYASH_ROOT:-}" ]; then export NYASH_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" fi # Prefer hakorune binary if exists; fallback to nyash for compatibility if [ -z "${NYASH_BIN:-}" ]; then if [ -x "$NYASH_ROOT/target/release/hakorune" ]; then export NYASH_BIN="$NYASH_ROOT/target/release/hakorune" else export NYASH_BIN="$NYASH_ROOT/target/release/nyash" fi fi # グローバル変数 export SMOKES_V2_LIB_LOADED=1 export SMOKES_START_TIME=$(date +%s.%N) export SMOKES_TEST_COUNT=0 export SMOKES_PASS_COUNT=0 export SMOKES_FAIL_COUNT=0 # 色定義(重複回避) if [ -z "${RED:-}" ]; then readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color fi # ログ関数 log_info() { echo -e "${BLUE}[INFO]${NC} $*" >&2 } log_success() { echo -e "${GREEN}[PASS]${NC} $*" >&2 } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2 } log_error() { echo -e "${RED}[FAIL]${NC} $*" >&2 } # 共通ノイズフィルタ(VM実行時の出力整形) filter_noise() { # プラグイン初期化やメタログ、動的ローダの案内等を除去 grep -v "^\[UnifiedBoxRegistry\]" \ | grep -v "^\[FileBox\]" \ | grep -v "^Net plugin:" \ | grep -v "^\[.*\] Plugin" \ | grep -v "Using builtin StringBox" \ | grep -v "Using builtin ArrayBox" \ | grep -v "Using builtin MapBox" \ | grep -v "^\[using\]" \ | grep -v "^\[using/resolve\]" \ | grep -v "^\[builder\]" \ | grep -v "^\\[vm-trace\\]" \ | grep -v "^\[DEBUG\]" \ | grep -v '^\{"ev":' \ | grep -v '^\[warn\] dev fallback: user instance BoxCall' \ | sed -E 's/^❌ VM fallback error: *//' \ | grep -v '^\[warn\] dev verify: NewBox ' \ | grep -v '^\[warn\] dev verify: NewBox→birth invariant warnings:' \ | grep -v '^\[ny-compiler\]' \ | grep -v '^\[using/cache\]' \ | grep -v "plugins/nyash-array-plugin" \ | grep -v "plugins/nyash-map-plugin" \ | grep -v "Phase 15.5: Everything is Plugin" \ | grep -v "cargo build -p nyash-string-plugin" \ | grep -v "^\[plugin-loader\] backend=" \ | grep -v "^\[using\] ctx:" \ | grep -v "^🔌 plugin host initialized" \ | grep -v "^✅ plugin host fully configured" \ | grep -v "Failed to load nyash.toml - plugins disabled" \ | grep -v "^🚀 Nyash VM Backend - Executing file:" \ | grep -v "^🚀 Hakorune VM Backend - Executing file:" } # 環境チェック(必須) require_env() { local required_tools=("cargo" "grep" "jq") local missing_tools=() for tool in "${required_tools[@]}"; do if ! command -v "$tool" &> /dev/null; then missing_tools+=("$tool") fi done if [ ${#missing_tools[@]} -ne 0 ]; then log_error "Required tools missing: ${missing_tools[*]}" log_error "Please install missing tools and try again" return 1 fi # Nyash実行ファイル確認 if [ ! -f "$NYASH_BIN" ]; then log_error "Nyash executable not found at $NYASH_BIN" log_error "Please run 'cargo build --release' first (in $NYASH_ROOT)" return 1 fi log_info "Environment check passed" return 0 } # プラグイン整合性チェック(必須) preflight_plugins() { # プラグインマネージャーが存在する場合は実行 if [ -f "$(dirname "${BASH_SOURCE[0]}")/plugin_manager.sh" ]; then source "$(dirname "${BASH_SOURCE[0]}")/plugin_manager.sh" check_plugin_integrity || return 1 else log_warn "Plugin manager not found, skipping plugin checks" fi return 0 } # テスト実行関数 run_test() { local test_name="$1" local test_func="$2" ((SMOKES_TEST_COUNT++)) local start_time=$(date +%s.%N) log_info "Running test: $test_name" if $test_func; then local end_time=$(date +%s.%N) local duration=$(echo "$end_time - $start_time" | bc -l) log_success "$test_name (${duration}s)" ((SMOKES_PASS_COUNT++)) return 0 else local end_time=$(date +%s.%N) local duration=$(echo "$end_time - $start_time" | bc -l) log_error "$test_name (${duration}s)" ((SMOKES_FAIL_COUNT++)) return 1 fi } # Nyash実行ヘルパー(Rust VM) run_nyash_vm() { local program="$1" shift local USE_PYVM="${SMOKES_USE_PYVM:-0}" local EXTRA_ARGS=() if [ "${SMOKES_USE_DEV:-0}" = "1" ]; then EXTRA_ARGS+=("--dev") fi # Optional env sanitization between rapid invocations (default OFF) # Enable with: SMOKES_CLEAN_ENV=1 local ENV_PREFIX=( ) if [ "${SMOKES_CLEAN_ENV:-0}" = "1" ]; then ENV_PREFIX=(env -u NYASH_DEBUG_ENABLE -u NYASH_DEBUG_KINDS -u NYASH_DEBUG_SINK \ -u NYASH_RESOLVE_FIX_BRACES -u NYASH_USING_AST \ -u NYASH_VM_TRACE -u NYASH_VM_VERIFY_MIR -u NYASH_VM_TOLERATE_VOID \ -u NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN) fi # -c オプションの場合は一時ファイル経由で実行 if [ "$program" = "-c" ]; then local code="$1" shift local tmpfile="/tmp/nyash_test_$$.nyash" echo "$code" > "$tmpfile" # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ]; then sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$tmpfile" || true fi # プラグイン初期化メッセージを除外 # Optional preinclude for include-based code local runfile="$tmpfile" if [ "${NYASH_PREINCLUDE:-0}" = "1" ] || [ "${HAKO_PREINCLUDE:-0}" = "1" ]; then local prefile="/tmp/nyash_pre_$$.nyash" "$NYASH_ROOT/tools/dev/hako_preinclude.sh" "$tmpfile" "$prefile" >/dev/null || true runfile="$prefile" fi # Optional hint for include lines when preinclude is OFF if grep -q '^include\s\"' "$tmpfile" 2>/dev/null && [ "${NYASH_PREINCLUDE:-0}" != "1" ] && [ "${HAKO_PREINCLUDE:-0}" != "1" ]; then echo "[WARN] VM backend does not support include. Prefer using+alias, or set NYASH_PREINCLUDE=1 for dev." >&2 fi NYASH_VM_USE_PY="$USE_PYVM" NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 \ NYASH_DISABLE_NY_COMPILER=1 HAKO_DISABLE_NY_COMPILER=1 \ NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 NYASH_PARSER_ALLOW_SEMICOLON=1 \ "${ENV_PREFIX[@]}" \ "$NYASH_BIN" --backend vm "$runfile" "${EXTRA_ARGS[@]}" "$@" 2>&1 | filter_noise local exit_code=${PIPESTATUS[0]} # prefile may be unset when preinclude is OFF; use default expansion to avoid set -u errors rm -f "$tmpfile" "${prefile:-}" 2>/dev/null || true return $exit_code else # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ] && [ -f "$program" ]; then sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$program" || true fi # プラグイン初期化メッセージを除外 # Optional preinclude local runfile2="$program" if [ "${NYASH_PREINCLUDE:-0}" = "1" ] || [ "${HAKO_PREINCLUDE:-0}" = "1" ]; then local prefile2="/tmp/nyash_pre_$$.nyash" "$NYASH_ROOT/tools/dev/hako_preinclude.sh" "$program" "$prefile2" >/dev/null || true runfile2="$prefile2" fi # Optional hint for include lines when preinclude is OFF if [ -f "$program" ] && grep -q '^include\s\"' "$program" 2>/dev/null && [ "${NYASH_PREINCLUDE:-0}" != "1" ] && [ "${HAKO_PREINCLUDE:-0}" != "1" ]; then echo "[WARN] VM backend does not support include. Prefer using+alias, or set NYASH_PREINCLUDE=1 for dev." >&2 fi NYASH_VM_USE_PY="$USE_PYVM" NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 \ NYASH_DISABLE_NY_COMPILER=1 HAKO_DISABLE_NY_COMPILER=1 \ NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 NYASH_PARSER_ALLOW_SEMICOLON=1 \ "${ENV_PREFIX[@]}" \ "$NYASH_BIN" --backend vm "$runfile2" "${EXTRA_ARGS[@]}" "$@" 2>&1 | filter_noise local exit_code=${PIPESTATUS[0]} # prefile2 may be unset when preinclude is OFF rm -f "${prefile2:-}" 2>/dev/null || true return $exit_code fi } # Verify MIR JSON rc using selected primary (Core or Hakorune VM) verify_mir_rc() { local json_path="$1" local primary="${HAKO_VERIFY_PRIMARY:-core}" if [ "$primary" = "hakovm" ]; then # Build a tiny driver to call MiniVmEntryBox.run_min with JSON literal embedded if [ ! -f "$json_path" ]; then echo "[FAIL] verify_mir_rc: json not found: $json_path" >&2 return 2 fi # Escape JSON as a single string literal via jq -Rs (preserves newlines) local json_literal json_literal="$(jq -Rs . < "$json_path")" build_and_run_driver_alias() { local header="$1" local code=$(cat </dev/null | tr -d '\r' | tail -n 1 } build_and_run_driver_include() { local inc_path="$1" local code=$(cat </dev/null | tr -d '\r' | tail -n 1 } # Try alias header first; fallback to dev-file header; final fallback: include+preinclude local out out="$(build_and_run_driver_alias 'using selfhost.vm.entry as MiniVmEntryBox')" if ! [[ "$out" =~ ^-?[0-9]+$ ]]; then out="$(build_and_run_driver_alias 'using "lang/src/vm/boxes/mini_vm_entry.hako" as MiniVmEntryBox')" fi if ! [[ "$out" =~ ^-?[0-9]+$ ]]; then out="$(build_and_run_driver_include 'lang/src/vm/boxes/mini_vm_entry.hako')" fi if [[ "$out" =~ ^-?[0-9]+$ ]]; then local n=$out # normalize into [0,255] if [ $n -lt 0 ]; then n=$(( (n % 256 + 256) % 256 )); else n=$(( n % 256 )); fi return $n fi # Fallback: core primary when MiniVM resolution is unavailable NYASH_GATE_C_CORE=1 HAKO_GATE_C_CORE=1 "$NYASH_BIN" --json-file "$json_path" >/dev/null 2>&1 return $? else NYASH_GATE_C_CORE=1 HAKO_GATE_C_CORE=1 "$NYASH_BIN" --json-file "$json_path" >/dev/null 2>&1 return $? fi } # Nyash実行ヘルパー(LLVM) run_nyash_llvm() { local program="$1" shift # Allow developer to force LLVM run (env guarantees availability) if [ "${SMOKES_FORCE_LLVM:-0}" != "1" ]; then # Skip gracefully when LLVM backend is not available in this build # Primary check: version string advertises features if ! "$NYASH_BIN" --version 2>/dev/null | grep -q "features.*llvm"; then # Fallback check: binary contains LLVM harness symbols (ny-llvmc / NYASH_LLVM_USE_HARNESS) if ! strings "$NYASH_BIN" 2>/dev/null | grep -E -q 'ny-llvmc|NYASH_LLVM_USE_HARNESS'; then log_warn "LLVM backend not available in this build; skipping LLVM run" log_info "Hint: build ny-llvmc + enable harness: cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm" return 0 fi fi fi # -c オプションの場合は一時ファイル経由で実行 if [ "$program" = "-c" ]; then local code="$1" shift local tmpfile="/tmp/nyash_test_$$.nyash" echo "$code" > "$tmpfile" # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ]; then sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$tmpfile" || true fi # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ] && [ -f "$program" ]; then sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$program" || true fi # プラグイン初期化メッセージを除外 PYTHONPATH="${PYTHONPATH:-$NYASH_ROOT}" NYASH_NY_LLVM_COMPILER="$NYASH_ROOT/target/release/ny-llvmc" NYASH_LLVM_USE_HARNESS=1 NYASH_EMIT_EXE_NYRT="$NYASH_ROOT/target/release" NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 NYASH_PARSER_ALLOW_SEMICOLON=1 NYASH_DISABLE_NY_COMPILER=1 HAKO_DISABLE_NY_COMPILER=1 "$NYASH_BIN" --backend llvm "$tmpfile" "$@" 2>&1 | \ grep -v "^\[UnifiedBoxRegistry\]" | grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \ grep -v '^\[plugin-loader\] backend=' | \ grep -v '^🔌 plugin host initialized' | grep -v '^✅ plugin host fully configured' | \ grep -v '^⚡ Hakorune LLVM Backend' | \ grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' | grep -v 'JSON Parse Errors:' | grep -v 'Parsing errors' | grep -v 'No parsing errors' | grep -v 'Error at line ' | \ grep -v '^\[using\]' | grep -v '^\[using/resolve\]' | grep -v '^\[using/cache\]' | \ grep -v '^\[ny-llvmc\]' | grep -v '^\[harness\]' | grep -v '^Compiled to ' | grep -v '^/usr/bin/ld:' local exit_code=${PIPESTATUS[0]} rm -f "$tmpfile" return $exit_code else # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ] && [ -f "$program" ]; then sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$program" || true fi # プラグイン初期化メッセージを除外 PYTHONPATH="${PYTHONPATH:-$NYASH_ROOT}" NYASH_NY_LLVM_COMPILER="$NYASH_ROOT/target/release/ny-llvmc" NYASH_LLVM_USE_HARNESS=1 NYASH_EMIT_EXE_NYRT="$NYASH_ROOT/target/release" NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 NYASH_PARSER_ALLOW_SEMICOLON=1 NYASH_DISABLE_NY_COMPILER=1 HAKO_DISABLE_NY_COMPILER=1 "$NYASH_BIN" --backend llvm "$program" "$@" 2>&1 | \ grep -v "^\[UnifiedBoxRegistry\]" | grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \ grep -v '^\[plugin-loader\] backend=' | \ grep -v '^🔌 plugin host initialized' | grep -v '^✅ plugin host fully configured' | \ grep -v '^⚡ Hakorune LLVM Backend' | \ grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' | grep -v 'JSON Parse Errors:' | grep -v 'Parsing errors' | grep -v 'No parsing errors' | grep -v 'Error at line ' | \ grep -v '^\[using\]' | grep -v '^\[using/resolve\]' | grep -v '^\[using/cache\]' | \ grep -v '^\[ny-llvmc\]' | grep -v '^\[harness\]' | grep -v '^Compiled to ' | grep -v '^/usr/bin/ld:' return ${PIPESTATUS[0]} fi } # シンプルテスト補助(スクリプト互換) test_pass() { log_success "$1"; return 0; } test_fail() { log_error "$1 ${2:-}"; return 1; } test_skip() { log_warn "SKIP $1 ${2:-}"; return 0; } # 出力比較ヘルパー compare_outputs() { local expected="$1" local actual="$2" local test_name="$3" if [ "$expected" = "$actual" ]; then return 0 else log_error "$test_name output mismatch:" log_error " Expected: $expected" log_error " Actual: $actual" return 1 fi } # 結果サマリー出力 print_summary() { local end_time=$(date +%s.%N) local total_duration=$(echo "$end_time - $SMOKES_START_TIME" | bc -l) echo "" echo "===============================================" echo "Smoke Test Summary" echo "===============================================" echo "Total tests: $SMOKES_TEST_COUNT" echo "Passed: $SMOKES_PASS_COUNT" echo "Failed: $SMOKES_FAIL_COUNT" echo "Duration: ${total_duration}s" echo "" if [ $SMOKES_FAIL_COUNT -eq 0 ]; then log_success "All tests passed! ✨" return 0 else log_error "$SMOKES_FAIL_COUNT test(s) failed" return 1 fi } # JSON出力関数 output_json() { local profile="${1:-unknown}" local end_time=$(date +%s.%N) local total_duration=$(echo "$end_time - $SMOKES_START_TIME" | bc -l) cat << EOF { "profile": "$profile", "total": $SMOKES_TEST_COUNT, "passed": $SMOKES_PASS_COUNT, "failed": $SMOKES_FAIL_COUNT, "duration": $total_duration, "timestamp": "$(date -Iseconds)", "success": $([ $SMOKES_FAIL_COUNT -eq 0 ] && echo "true" || echo "false") } EOF } # JUnit XML出力関数 output_junit() { local profile="${1:-unknown}" local end_time=$(date +%s.%N) local total_duration=$(echo "$end_time - $SMOKES_START_TIME" | bc -l) cat << EOF EOF }