瀏覽代碼

[cpp] Fix double free crash in SkeletonBinary::readSkeletonDataFile, add terminal logging utilities and style guide, add nostdcpp

Mario Zechner 1 月之前
父節點
當前提交
2036aa3e76

+ 3 - 0
.gitignore

@@ -254,3 +254,6 @@ spine-c/codegen/spine-cpp-types.json
 spine-flutter/example/devtools_options.yaml
 spine-glfw/.cache
 formatters/eclipse-formatter/format-diff.txt
+spine-cpp/build-debug
+spine-cpp/build-linux
+spine-cpp/build-release-debug

+ 93 - 0
formatters/logging/bash-colors.sh

@@ -0,0 +1,93 @@
+#!/bin/bash
+# Bash color and formatting utilities
+# Source this file in your bash scripts for colored output
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+BLUE='\033[0;34m'
+MAGENTA='\033[0;35m'
+CYAN='\033[0;36m'
+WHITE='\033[0;37m'
+GRAY='\033[0;90m'
+
+# Bright colors
+BRIGHT_RED='\033[0;91m'
+BRIGHT_GREEN='\033[0;92m'
+BRIGHT_YELLOW='\033[0;93m'
+BRIGHT_BLUE='\033[0;94m'
+BRIGHT_MAGENTA='\033[0;95m'
+BRIGHT_CYAN='\033[0;96m'
+BRIGHT_WHITE='\033[0;97m'
+
+# Background colors
+BG_RED='\033[41m'
+BG_GREEN='\033[42m'
+BG_YELLOW='\033[43m'
+BG_BLUE='\033[44m'
+BG_MAGENTA='\033[45m'
+BG_CYAN='\033[46m'
+BG_WHITE='\033[47m'
+
+# Text styles
+BOLD='\033[1m'
+DIM='\033[2m'
+UNDERLINE='\033[4m'
+BLINK='\033[5m'
+REVERSE='\033[7m'
+STRIKETHROUGH='\033[9m'
+
+# Reset
+NC='\033[0m' # No Color
+
+# Design principles:
+# 1. Minimal visual noise - use color sparingly for emphasis
+# 2. Clear hierarchy - different levels of information have different treatments  
+# 3. Consistent spacing - clean vertical rhythm
+# 4. Accessible - readable without colors
+
+# Main header for script/tool name
+log_title() {
+    echo ""
+    echo -e "${BOLD}$1${NC}"
+    echo ""
+}
+
+# Section headers for major phases
+log_section() {
+    echo -e "${BOLD}${BLUE}$1${NC}"
+}
+
+# Individual actions/steps
+log_action() {
+    echo -e "  $1..."
+}
+
+# Results - success/failure/info
+log_ok() {
+    echo -e "  ${GREEN}✓${NC} $1"
+}
+
+log_fail() {
+    echo -e "  ${RED}✗${NC} $1"
+}
+
+log_warn() {
+    echo -e "  ${YELLOW}!${NC} $1"
+}
+
+log_skip() {
+    echo -e "  ${GRAY}-${NC} $1"
+}
+
+# Final summary
+log_summary() {
+    echo ""
+    echo -e "${BOLD}$1${NC}"
+}
+
+# Detailed output (errors, etc.)
+log_detail() {
+    echo -e "${GRAY}$1${NC}"
+}

+ 153 - 0
formatters/logging/terminal-logging-guide.md

@@ -0,0 +1,153 @@
+# Terminal Logging Style Guide
+
+This guide defines the terminal output style for all bash scripts in the Spine Runtimes project.
+
+## Design Principles
+
+1. **Minimal visual noise** - Use color sparingly for emphasis, not decoration
+2. **Clear hierarchy** - Different levels of information have distinct visual treatments
+3. **Consistent spacing** - Clean vertical rhythm throughout output
+4. **Accessible** - Readable and meaningful even without colors
+5. **Scannable** - Easy to quickly identify successes, failures, and important information
+
+## Visual Hierarchy
+
+### 1. Title (`log_title`)
+- **Purpose**: Main script/tool name
+- **Style**: Bold with vertical spacing
+- **Usage**: Once at the beginning of script execution
+
+```bash
+log_title "Spine-C++ Test"
+```
+
+### 2. Section (`log_section`)
+- **Purpose**: Major phases or groups of operations
+- **Style**: Bold blue text, no extra spacing
+- **Usage**: Build, Test, Deploy, etc.
+
+```bash
+log_section "Build"
+log_section "Test"
+```
+
+### 3. Action (`log_action`)
+- **Purpose**: Individual operations in progress
+- **Style**: Indented, followed by "..."
+- **Usage**: Before starting an operation
+
+```bash
+log_action "Building all variants"
+log_action "Testing headless-test"
+```
+
+### 4. Results
+- **Purpose**: Outcome of operations
+- **Style**: Indented with colored symbols
+
+```bash
+log_ok "Build completed"          # Green ✓
+log_fail "Build failed"           # Red ✗  
+log_warn "Deprecated feature"     # Yellow !
+log_skip "Not supported on macOS" # Gray -
+```
+
+### 5. Detail (`log_detail`)
+- **Purpose**: Secondary information, error output, debug info
+- **Style**: Gray text, indented
+- **Usage**: Additional context, error messages
+
+```bash
+log_detail "Platform: Darwin"
+log_detail "$ERROR_OUTPUT"
+```
+
+### 6. Summary (`log_summary`)
+- **Purpose**: Final result or conclusion
+- **Style**: Bold with vertical spacing
+- **Usage**: End of script execution
+
+```bash
+log_summary "✓ All tests passed (5/5)"
+log_summary "✗ Tests failed (3/5)"
+```
+
+## Complete Example
+
+```bash
+#!/bin/bash
+source ../formatters/bash-colors.sh
+
+log_title "Spine-C++ Test"
+log_detail "Platform: $(uname)"
+
+log_section "Build"
+log_action "Building all variants"
+if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
+    log_ok "Build completed"
+else
+    log_fail "Build failed"
+    log_detail "$BUILD_OUTPUT"
+    exit 1
+fi
+
+log_section "Test"
+log_action "Testing headless-test"
+if test_result; then
+    log_ok "headless-test"
+else
+    log_fail "headless-test - execution failed"
+    log_detail "$error_output"
+fi
+
+log_summary "✓ All tests passed (2/2)"
+```
+
+## Output Preview
+
+```
+Spine-C++ Test
+
+Platform: Darwin
+
+Build
+  Building all variants...
+  ✓ Build completed
+
+Test
+  Testing headless-test...
+  ✓ headless-test
+  Testing headless-test-nostdcpp...
+  ✓ headless-test-nostdcpp
+
+✓ All tests passed (2/2)
+```
+
+## Error Handling Best Practices
+
+1. **Capture output**: Use `OUTPUT=$(command 2>&1)` to capture both stdout and stderr
+2. **Check exit codes**: Always check if critical operations succeeded
+3. **Show details on failure**: Use `log_detail` to show error output
+4. **Fail fast**: Exit immediately on critical failures
+5. **Clear error messages**: Make failure reasons obvious
+
+```bash
+if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
+    log_ok "Build completed"
+else
+    log_fail "Build failed"
+    log_detail "$BUILD_OUTPUT"
+    exit 1
+fi
+```
+
+## Usage
+
+1. Source the utilities in your script:
+```bash
+source ../formatters/logging/bash-colors.sh
+```
+
+2. Follow the hierarchy patterns shown above
+3. Use appropriate functions for each type of output
+4. Test output both with and without color support

+ 31 - 16
spine-cpp/CMakeLists.txt

@@ -7,7 +7,11 @@ option(SPINE_NO_FILE_IO "Disable file I/O operations" OFF)
 
 include_directories(include)
 file(GLOB INCLUDES "include/**/*.h")
-file(GLOB SOURCES "src/**/*.cpp")
+file(GLOB ALL_SOURCES "src/**/*.cpp")
+
+# Exclude nostdcpp.cpp from regular build
+list(FILTER ALL_SOURCES EXCLUDE REGEX "src/nostdcpp\\.cpp$")
+set(SOURCES ${ALL_SOURCES})
 
 add_library(spine-cpp STATIC ${SOURCES} ${INCLUDES})
 target_include_directories(spine-cpp PUBLIC include)
@@ -17,7 +21,7 @@ if(SPINE_NO_FILE_IO)
 endif()
 
 # nostdcpp variant (no C++ standard library)
-file(GLOB NOSTDCPP_SOURCES ${SOURCES} "src/nostdlib.cpp")
+set(NOSTDCPP_SOURCES ${SOURCES} "src/nostdcpp.cpp")
 add_library(spine-cpp-nostdcpp STATIC ${NOSTDCPP_SOURCES} ${INCLUDES})
 target_include_directories(spine-cpp-nostdcpp PUBLIC include)
 
@@ -35,28 +39,39 @@ export(
 if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
     add_executable(headless-test ${CMAKE_CURRENT_SOURCE_DIR}/tests/HeadlessTest.cpp)
     target_link_libraries(headless-test spine-cpp)
-    
+
     if(SPINE_NO_FILE_IO)
         target_compile_definitions(headless-test PRIVATE SPINE_NO_FILE_IO)
     endif()
-    
-    # nostdcpp test executable (no C++ stdlib)
+
+    # Configure nostdcpp linking for different platforms
     add_executable(headless-test-nostdcpp ${CMAKE_CURRENT_SOURCE_DIR}/tests/HeadlessTest.cpp)
-    
+    target_link_libraries(headless-test-nostdcpp spine-cpp-nostdcpp)
+
     if(MSVC)
-        # On Windows/MSVC, disable default libraries but keep C runtime
-        target_link_libraries(headless-test-nostdcpp spine-cpp-nostdcpp)
         target_link_options(headless-test-nostdcpp PRIVATE /NODEFAULTLIB)
         target_link_libraries(headless-test-nostdcpp msvcrt kernel32)
+    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+        target_link_options(headless-test-nostdcpp PRIVATE -nostdlib++ -lc)
     else()
-        # Unix/Linux: avoid linking libstdc++ automatically
-        if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
-            target_link_libraries(headless-test-nostdcpp spine-cpp-nostdcpp)
-            target_link_options(headless-test-nostdcpp PRIVATE -nostdlib++ -lc)
-        else()
-            # GCC: use -nodefaultlibs and link minimal libraries including libgcc for operator new/delete
-            target_link_options(headless-test-nostdcpp PRIVATE -nodefaultlibs)
-            target_link_libraries(headless-test-nostdcpp spine-cpp-nostdcpp -lm -lc -lgcc)
+        target_link_options(headless-test-nostdcpp PRIVATE -nodefaultlibs)
+        target_link_libraries(headless-test-nostdcpp -lm -lc -lgcc)
+    endif()
+
+    # Static variants (Linux only)
+    if(UNIX AND NOT APPLE)
+        add_executable(headless-test-static ${CMAKE_CURRENT_SOURCE_DIR}/tests/HeadlessTest.cpp)
+        target_link_libraries(headless-test-static spine-cpp)
+        target_link_options(headless-test-static PRIVATE -static)
+
+        add_executable(headless-test-nostdcpp-static ${CMAKE_CURRENT_SOURCE_DIR}/tests/HeadlessTest.cpp)
+        target_link_libraries(headless-test-nostdcpp-static spine-cpp-nostdcpp)
+        target_link_options(headless-test-nostdcpp-static PRIVATE -static -static-libgcc -Wl,--exclude-libs,libstdc++.a)
+        target_link_libraries(headless-test-nostdcpp-static -lm -lc)
+
+        if(SPINE_NO_FILE_IO)
+            target_compile_definitions(headless-test-static PRIVATE SPINE_NO_FILE_IO)
+            target_compile_definitions(headless-test-nostdcpp-static PRIVATE SPINE_NO_FILE_IO)
         endif()
     endif()
 endif()

+ 63 - 0
spine-cpp/src/nostdcpp.cpp

@@ -0,0 +1,63 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include <cstddef>
+
+// Stubs for C++ stdlib functions spine-cpp depends on. Used for nostdcpp builds.
+// These are weak symbols to allow overriding in custom builds, e.g. headless-test-nostdcpp
+// where the main app still requires C++.
+
+extern "C" {
+    void* malloc(size_t size);
+    void free(void* ptr);
+}
+
+__attribute__((weak)) void* operator new(size_t size) {
+    return malloc(size);
+}
+
+__attribute__((weak)) void operator delete(void* ptr) {
+    if (ptr) free(ptr);
+}
+
+extern "C" __attribute__((weak)) int __cxa_guard_acquire(char* guard) {
+    return *guard == 0 ? (*guard = 1, 1) : 0;
+}
+
+extern "C" __attribute__((weak)) void __cxa_guard_release(char* guard) {
+    // No-op for single-threaded
+    (void)guard;
+}
+
+extern "C" __attribute__((weak)) void __cxa_pure_virtual() {
+}
+
+extern "C" __attribute__((weak)) char __stack_chk_guard = 0;
+extern "C" __attribute__((weak)) void __stack_chk_fail() {
+}

+ 0 - 54
spine-cpp/src/nostdlib.cpp

@@ -1,54 +0,0 @@
-/*
- * Minimal runtime stubs for nostdlib spine-cpp build
- * 
- * Provides minimal implementations of C++ runtime symbols required
- * for spine-cpp to work without the standard library.
- */
-
-#include <cstddef>
-
-// ============================================================================
-// C++ Runtime Stubs
-// ============================================================================
-
-// Memory operators - basic malloc/free wrappers
-// Note: These require a C library that provides malloc/free
-extern "C" {
-    void* malloc(size_t size);
-    void free(void* ptr);
-}
-
-// Memory operators - use malloc/free but with careful implementation
-// Make them weak so system can override if needed
-__attribute__((weak)) void* operator new(size_t size) {
-    return malloc(size);
-}
-
-__attribute__((weak)) void operator delete(void* ptr) {
-    if (ptr) free(ptr);
-}
-
-// Static initialization guards (single-threaded stubs)
-// Make them weak so system can override if needed
-extern "C" __attribute__((weak)) int __cxa_guard_acquire(char* guard) {
-    return *guard == 0 ? (*guard = 1, 1) : 0;
-}
-
-extern "C" __attribute__((weak)) void __cxa_guard_release(char* guard) {
-    // No-op for single-threaded
-    (void)guard;
-}
-
-// Pure virtual function handler
-extern "C" __attribute__((weak)) void __cxa_pure_virtual() {
-    // In a real implementation, this would abort or throw
-    // For minimal stub, we'll just return (undefined behavior but won't crash)
-    // This should never be called in a correctly written program
-}
-
-// Stack protection (for GCC -fstack-protector)
-// Make it weak so libc can override it in static builds
-extern "C" __attribute__((weak)) char __stack_chk_guard = 0;
-extern "C" __attribute__((weak)) void __stack_chk_fail() {
-    // Could call abort() or be no-op for minimal runtime
-}

+ 1 - 1
spine-cpp/src/spine/SkeletonBinary.cpp

@@ -118,7 +118,7 @@ SkeletonData *SkeletonBinary::readSkeletonDataFile(const String &path) {
 		const char *lastDot = strrchr(nameWithoutExtension.buffer(), '.');
 		if (lastDot) {
 			int length = lastDot - nameWithoutExtension.buffer();
-			nameWithoutExtension = String(nameWithoutExtension.buffer(), length);
+			nameWithoutExtension = nameWithoutExtension.substring(0, length);
 		}
 		skeletonData->_name = nameWithoutExtension;
 	}

+ 0 - 82
spine-cpp/test-nostdlib.sh

@@ -1,82 +0,0 @@
-#!/bin/bash
-set -e
-
-cd "$(dirname "$0")"
-
-# Build or reuse Docker image
-IMAGE_NAME="spine-cpp-nostdcpp-test"
-if ! docker image inspect $IMAGE_NAME >/dev/null 2>&1; then
-    echo "Building Docker image (one-time setup)..."
-    docker build -t $IMAGE_NAME - <<'EOF'
-FROM ubuntu:22.04
-RUN apt-get update >/dev/null 2>&1 && \
-    apt-get install -y build-essential cmake ninja-build git file >/dev/null 2>&1 && \
-    rm -rf /var/lib/apt/lists/*
-EOF
-fi
-
-echo "Running spine-cpp nostdcpp test..."
-
-# Run Docker container with spine-runtimes directory mounted
-docker run --rm \
-    -v "$(pwd)/..:/workspace/spine-runtimes" \
-    -w /workspace/spine-runtimes/spine-cpp \
-    $IMAGE_NAME \
-    bash -c "
-        
-        # Build everything first
-        echo '=== Building all variants ==='
-        ./build.sh clean release >/dev/null 2>&1
-        
-        # Try to build static regular executable
-        echo 'Building static regular executable...'
-        g++ -static -o build/headless-test-static build/CMakeFiles/headless-test.dir/tests/HeadlessTest.cpp.o build/libspine-cpp.a >/dev/null 2>&1 || echo 'Static regular build failed'
-        
-        # Try to build static nostdcpp executable (multiple approaches)
-        echo 'Building static nostdcpp executable...'
-        
-        # Approach 1: Try with -static-libgcc and -static-libstdc++ but no libstdc++
-        if g++ -static -static-libgcc -Wl,--exclude-libs,libstdc++.a -o build/headless-test-nostdcpp-static build/CMakeFiles/headless-test-nostdcpp.dir/tests/HeadlessTest.cpp.o build/libspine-cpp-nostdcpp.a -lm -lc 2>/dev/null; then
-            echo 'SUCCESS: Static nostdcpp built (approach 1)'
-        # Approach 2: Try minimal static linking
-        elif g++ -static -o build/headless-test-nostdcpp-static-minimal build/CMakeFiles/headless-test-nostdcpp.dir/tests/HeadlessTest.cpp.o build/libspine-cpp-nostdcpp.a 2>/dev/null; then
-            echo 'SUCCESS: Static nostdcpp built (approach 2 - minimal)'
-        else
-            echo 'All static nostdcpp approaches failed - static linking may not be practical on this system'
-        fi
-        
-        echo ''
-        echo '=== FINAL RESULTS ==='
-        echo ''
-        echo 'File sizes:'
-        for exe in build/headless-test*; do
-            if [ -f \"\$exe\" ]; then
-                ls -lah \"\$exe\" | awk '{printf \"%-30s %s\\n\", \$9, \$5}'
-            fi
-        done
-        
-        echo ''
-        echo 'Dependencies:'
-        for exe in build/headless-test*; do
-            if [ -f \"\$exe\" ]; then
-                echo \"\$(basename \$exe):\"
-                ldd \"\$exe\" 2>/dev/null || echo \"  (statically linked)\"
-                echo ''
-            fi
-        done
-        
-        echo 'Functional test:'
-        if [ -f build/headless-test-nostdcpp ]; then
-            echo 'Testing headless-test-nostdcpp with spineboy...'
-            if OUTPUT=\$(./build/headless-test-nostdcpp ../examples/spineboy/export/spineboy-pro.skel ../examples/spineboy/export/spineboy-pma.atlas idle 2>&1); then
-                echo \"\$OUTPUT\" | head -10
-                echo '... (output truncated)'
-                echo 'SUCCESS: nostdcpp executable works!'
-            else
-                echo 'FAILED: nostdcpp executable failed to run'
-                echo \"Error: \$OUTPUT\"
-            fi
-        else
-            echo 'nostdcpp executable not found'
-        fi
-    "

+ 89 - 0
spine-cpp/tests/test.sh

@@ -0,0 +1,89 @@
+#!/bin/bash
+# Spine-C++ Smoke Test
+#
+# Tests all spine-cpp build variants with spineboy example data:
+# - headless-test (regular dynamic)
+# - headless-test-nostdcpp (nostdcpp dynamic)  
+# - headless-test-static (regular static, Linux only)
+# - headless-test-nostdcpp-static (nostdcpp static, Linux only)
+
+set -e
+
+# Change to spine-cpp root directory
+cd "$(dirname "$0")/.."
+
+# Source logging utilities
+source ../formatters/logging/bash-colors.sh
+
+# Test configuration - spineboy example files and animation
+SPINEBOY_SKEL="../examples/spineboy/export/spineboy-pro.skel"
+SPINEBOY_ATLAS="../examples/spineboy/export/spineboy-pma.atlas"
+SPINEBOY_ANIM="idle"
+
+# Expected output pattern - first 10 lines of skeleton JSON data
+EXPECTED_OUTPUT="=== SKELETON DATA ===
+{
+  \"type\": \"SkeletonData\",
+  \"bones\": [{
+      \"type\": \"BoneData\",
+      \"index\": 0,
+      \"parent\": null,
+      \"length\": 0,
+      \"color\": {
+        \"r\": 0.607843,"
+
+log_title "Spine-C++ Test"
+log_detail "Platform: $(uname)"
+
+log_section "Build"
+log_action "Building all variants"
+if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
+    log_ok "Build completed"
+else
+    log_fail "Build failed"
+    log_detail "$BUILD_OUTPUT"
+    exit 1
+fi
+
+log_section "Test"
+
+test_count=0
+pass_count=0
+
+for exe in build/headless-test*; do
+    if [ -f "$exe" ] && [ -x "$exe" ]; then
+        exe_name=$(basename "$exe")
+        log_action "Testing $exe_name"
+        
+        test_count=$((test_count + 1))
+        
+        if OUTPUT=$("$exe" $SPINEBOY_SKEL $SPINEBOY_ATLAS $SPINEBOY_ANIM 2>&1); then
+            actual_output=$(echo "$OUTPUT" | head -10)
+            
+            if [ "$actual_output" = "$EXPECTED_OUTPUT" ]; then
+                log_ok "$exe_name"
+                pass_count=$((pass_count + 1))
+            else
+                log_fail "$exe_name - output mismatch"
+                log_detail "Expected:"
+                log_detail "$EXPECTED_OUTPUT"
+                log_detail ""
+                log_detail "Actual:"
+                log_detail "$actual_output"
+                echo ""
+            fi
+        else
+            log_fail "$exe_name - execution failed"
+            log_detail "$OUTPUT"
+            echo ""
+        fi
+    fi
+done
+
+if [ $pass_count -eq $test_count ] && [ $test_count -gt 0 ]; then
+    log_summary "✓ All tests passed ($pass_count/$test_count)"
+    exit 0
+else
+    log_summary "✗ Tests failed ($pass_count/$test_count)"
+    exit 1
+fi