diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml new file mode 100644 index 0000000..15003ba --- /dev/null +++ b/.github/workflows/ci-linux.yml @@ -0,0 +1,53 @@ +name: Linux + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-test: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: "Linux GCC" + compiler: gcc + cmake-generator: 'Ninja' + cmake-options: '-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++' + + - name: "Linux Clang" + compiler: clang + cmake-generator: 'Ninja' + cmake-options: '-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Install ninja-build + run: | + sudo apt-get update + sudo apt-get install -y ninja-build + shell: bash + + - name: Configure CMake + run: | + cmake -B build -G "${{ matrix.cmake-generator }}" ${{ matrix.cmake-options }} + shell: bash + + - name: Build + run: | + cmake --build build --config Release + shell: bash + + - name: Run tests + working-directory: build + run: | + ./tests/testcoe_tests + shell: bash \ No newline at end of file diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml new file mode 100644 index 0000000..e305e3a --- /dev/null +++ b/.github/workflows/ci-macos.yml @@ -0,0 +1,47 @@ +name: macOS + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-test: + name: ${{ matrix.name }} + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + include: + - name: "macOS Clang" + compiler: clang + cmake-generator: 'Ninja' + cmake-options: '' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Install ninja-build + run: | + brew install ninja + shell: bash + + - name: Configure CMake + run: | + cmake -B build -G "${{ matrix.cmake-generator }}" ${{ matrix.cmake-options }} + shell: bash + + - name: Build + run: | + cmake --build build --config Release + shell: bash + + - name: Run tests + working-directory: build/tests + run: | + ./testcoe_tests + shell: bash \ No newline at end of file diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..e881d07 --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,86 @@ +name: Windows + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-test: + name: ${{ matrix.name }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - name: "Windows MSVC" + compiler: msvc + cmake-generator: 'Visual Studio 17 2022' + cmake-options: '' + + - name: "Windows MinGW" + compiler: gcc + cmake-generator: 'MinGW Makefiles' + cmake-options: '-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Install Windows build tools + if: matrix.compiler != 'msvc' + run: | + choco install mingw + shell: bash + + - name: Configure CMake + run: | + cmake -B build -G "${{ matrix.cmake-generator }}" ${{ matrix.cmake-options }} + shell: bash + + - name: Build + run: | + cmake --build build --config Release + shell: bash + + - name: Copy MinGW DLLs + if: matrix.compiler == 'gcc' + run: | + # Find GCC directory + GCC_DIR=$(dirname $(which gcc)) + echo "GCC directory: $GCC_DIR" + + # Copy DLLs to test directory + echo "Copying DLLs to tests directory" + cp "$GCC_DIR"/libgcc*.dll build/tests/ 2>/dev/null || echo "No libgcc DLLs found" + cp "$GCC_DIR"/libstdc*.dll build/tests/ 2>/dev/null || echo "No libstdc DLLs found" + cp "$GCC_DIR"/libwinpthread*.dll build/tests/ 2>/dev/null || echo "No libwinpthread DLLs found" + + # Copy DLLs to example directories + echo "Copying DLLs to example directories" + for dir in build/examples/basic build/examples/filter build/examples/crash; do + echo "Copying to $dir" + cp "$GCC_DIR"/libgcc*.dll "$dir/" 2>/dev/null || echo "No libgcc DLLs found" + cp "$GCC_DIR"/libstdc*.dll "$dir/" 2>/dev/null || echo "No libstdc DLLs found" + cp "$GCC_DIR"/libwinpthread*.dll "$dir/" 2>/dev/null || echo "No libwinpthread DLLs found" + done + + # List files to verify + echo "Files in build/tests:" + ls -la build/tests/*.dll || echo "No DLLs in tests" + echo "Files in build/examples/basic:" + ls -la build/examples/basic/*.dll || echo "No DLLs in basic" + shell: bash + + - name: Run tests + working-directory: build + run: | + if [ "${{ matrix.compiler }}" == "gcc" ]; then + ./tests/testcoe_tests.exe + else + ./tests/Release/testcoe_tests.exe + fi + shell: bash \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 345be10..ff5a072 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,18 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(MSVC) + # Ensure debug symbols are generated and linked + add_compile_options(/Zi) + add_link_options(/DEBUG) + # Keep debug info in release builds too for stack traces + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Zi") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF") +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # Enhanced debug info for GCC/Clang + add_compile_options(-g3 -fno-omit-frame-pointer) +endif() + # googletest find_package(GTest QUIET) if(GTest_FOUND) @@ -31,6 +43,11 @@ set(STACK_WALKING_UNWIND TRUE CACHE BOOL "Use unwind for stack walking" FORCE) set(STACK_DETAILS_AUTO_DETECT FALSE CACHE BOOL "Auto detect backward details" FORCE) set(STACK_DETAILS_BACKTRACE_SYMBOL TRUE CACHE BOOL "Use backtrace symbols" FORCE) +# Windows-specific enhancements for better stack traces +if(WIN32) + set(BACKWARD_HAS_DBGHELP 1 CACHE BOOL "Enable Windows DbgHelp for better stack traces" FORCE) +endif() + find_package(Backward QUIET) if(Backward_FOUND) message(STATUS "[testcoe] Using existing Backward-cpp installation") diff --git a/README.md b/README.md index e69de29..8a2e573 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,98 @@ +# testcoe + +Visual test progress and crash handling for Google Test. + +[![Windows](https://github.com/nircoe/testcoe/actions/workflows/ci-windows.yml/badge.svg)](https://github.com/nircoe/testcoe/actions/workflows/ci-windows.yml) +[![Linux](https://github.com/nircoe/testcoe/actions/workflows/ci-linux.yml/badge.svg)](https://github.com/nircoe/testcoe/actions/workflows/ci-linux.yml) +[![macOS](https://github.com/nircoe/testcoe/actions/workflows/ci-macos.yml/badge.svg)](https://github.com/nircoe/testcoe/actions/workflows/ci-macos.yml) + +## What is testcoe? + +testcoe enhances Google Test with real-time grid visualization and crash reporting: + +``` +Running 12 tests... Completed: 8/12 (P: 6, F: 2) +P - Passed, F - Failed, R - Running, . - Not run yet + +MathTests PPF.. (3/5) +StringTests PRPP. (4/5) +VectorTests .... (0/2) +``` + +When tests crash, you get detailed stack traces instead of silent failures. + +## Quick Start + +### 1. Add to your project (CMake) + +```cmake +include(FetchContent) +FetchContent_Declare( + testcoe + GIT_REPOSITORY https://github.com/nircoe/testcoe.git + GIT_TAG v0.1.0 +) +FetchContent_MakeAvailable(testcoe) + +target_link_libraries(your_test_executable PRIVATE testcoe) +``` + +### 2. Use in your tests + +```cpp +#include + +int main(int argc, char** argv) { + testcoe::init(&argc, argv); + return testcoe::run(); +} +``` + +That's it! Run your tests and see the enhanced output. + +## Features + +- 🎯 **Grid Visualization** - See all tests progress in real-time +- πŸ›‘οΈ **Crash Handling** - Get stack traces when tests crash +- 🎨 **Color Support** - Automatic terminal detection +- πŸ” **Test Filtering** - Run specific tests or suites +- πŸ“¦ **Zero Config** - Works out of the box with Google Test + +## Examples + +See the [examples/](examples/) directory for demonstrations: +- [Basic usage](examples/basic/) - Grid visualization +- [Crash handling](examples/crash/) - Stack traces on crashes +- [Test filtering](examples/filter/) - Running specific tests + +## Requirements + +- C++17 or later +- CMake 3.14+ +- Google Test (automatically included) + +## API Reference + +```cpp +// Initialize testcoe +testcoe::init(&argc, argv); + +// Run all tests +testcoe::run(); + +// Run specific suite +testcoe::run_suite("MathTests"); + +// Run specific test +testcoe::run_test("MathTests", "Addition"); +``` + +## Documentation + +- [Architecture](docs/ARCHITECTURE.md) - How testcoe works internally +- [Contributing](docs/CONTRIBUTING.md) - Development setup and guidelines +- [Roadmap](docs/ROADMAP.md) - Version history and future plans + +## License + +MIT License - see [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..8044e81 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,102 @@ +# testcoe Architecture + +## Overview + +testcoe enhances Google Test by intercepting test events and providing visual feedback. It consists of several modular components working together. + +## Component Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Test Application β”‚ +β”‚ (Your Google Tests) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ testcoe β”‚ +β”‚ (Main Interface & API) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ GridListener β”‚ β”‚SignalHandlerβ”‚ β”‚TerminalUtilsβ”‚ +β”‚ (Visual Grid) β”‚ β”‚ (Crashes) β”‚ β”‚ (Terminal) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Core Components + +### testcoe (Main Interface) +- **File**: `src/testcoe.cpp`, `include/testcoe/testcoe.hpp` +- **Purpose**: Provides the public API for initialization and test execution +- **Key Functions**: + - `init()` - Initializes Google Test and installs custom listeners + - `run()` - Executes all tests + - `run_suite()` - Executes specific test suite + - `run_test()` - Executes specific test + +### GridListener +- **File**: `src/grid_listener.cpp`, `include/testcoe/grid_listener.hpp` +- **Purpose**: Implements Google Test event listener for visual grid display +- **Key Features**: + - Tracks test execution state + - Updates terminal display in real-time + - Collects and displays failure information + - Shows execution time statistics + +### SignalHandler +- **File**: `src/signal_handler.cpp`, `include/testcoe/signal_handler.hpp` +- **Purpose**: Catches crashes and provides detailed stack traces +- **Platform Support**: + - **Unix/Linux/macOS**: POSIX signal handlers (SIGSEGV, SIGABRT, etc.) + - **Windows**: Structured Exception Handling (SEH) +- **Uses backward-cpp for stack trace generation** + +### TerminalUtils +- **File**: `src/terminal_utils.cpp`, `include/testcoe/terminal_utils.hpp` +- **Purpose**: Cross-platform terminal operations +- **Key Functions**: + - `isAnsiEnabled()` - Detects ANSI color support + - `clear()` - Clears terminal screen + +## Data Flow + +1. **Initialization**: + - User calls `testcoe::init()` + - Google Test is initialized + - GridListener is installed + - Signal handlers are registered + +2. **Test Execution**: + - User calls `testcoe::run()` + - Google Test begins execution + - GridListener receives events: + - `OnTestProgramStart` - Initialize grid display + - `OnTestStart` - Mark test as running + - `OnTestEnd` - Mark test as passed/failed + - `OnTestProgramEnd` - Show final summary + +3. **Crash Handling**: + - If a test crashes, signal handler is triggered + - Stack trace is generated using backward-cpp + - Output streams are restored + - Detailed crash report is displayed + +## Dependencies + +### Build-time Dependencies +- **Google Test** (v1.16.0) - Test framework +- **backward-cpp** (v1.6) - Stack trace generation + +### Platform Dependencies +- **Windows**: DbgHelp.dll (for symbol resolution) +- **Unix/Linux**: Standard POSIX signal handling +- **macOS**: Standard POSIX signal handling + +## Key Design Decisions + +1. **Event Listener Pattern**: Uses Google Test's event listener interface for non-invasive integration +2. **Stream Redirection**: Temporarily redirects stdout/stderr during test execution to control output +3. **Cross-platform Abstraction**: Platform-specific code isolated in dedicated sections +4. **Header-only Public API**: Simple integration with single include \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..6c560ba --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,127 @@ +# Contributing to testcoe + +Thank you for your interest in contributing to testcoe! + +## Development Setup + +### Prerequisites +- CMake 3.14+ +- C++17 compatible compiler +- Git + +### Building from Source + +```bash +git clone https://github.com/nircoe/testcoe.git +cd testcoe +mkdir build && cd build +cmake -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON .. +cmake --build . +``` + +### Running Tests + +```bash +# Run unit tests +./tests/testcoe_tests + +# Run examples +./examples/basic/basic_example +./examples/crash/crash_example +./examples/filter/filter_example +``` + +## Project Structure + +``` +testcoe/ +β”œβ”€β”€ include/testcoe/ # Public headers +β”œβ”€β”€ src/ # Implementation files +β”œβ”€β”€ examples/ # Example programs +β”œβ”€β”€ tests/ # Integration and unit tests +└── .github/workflows/ # CI configuration +``` + +## Continuous Integration + +All pull requests are automatically tested on: + +- **Windows**: MSVC and MinGW +- **Linux**: GCC and Clang +- **macOS**: Apple Clang + +### CI Pipeline Details + +The CI runs the following checks: +1. Build the library +2. Build all examples +3. Run integration tests +4. Verify examples execute correctly + +## Making Changes + +### Code Style +- Use 4 spaces for indentation +- Follow existing naming conventions +- Keep lines under 120 characters +- Add comments for complex logic + +### Testing +- Add tests for new features +- Ensure all tests pass locally +- Test on multiple platforms if possible + +### Pull Request Process + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Run tests locally +5. Commit with clear messages +6. Push to your fork (`git push origin feature/amazing-feature`) +7. Open a Pull Request from your fork to the main repository + +### Commit Messages +- Use present tense ("Add feature" not "Added feature") +- Keep first line under 50 characters +- Reference issues if applicable + +## Platform-Specific Considerations + +### Windows +- Test with both MSVC and MinGW +- Ensure DbgHelp.dll linking works correctly +- Verify SEH handling functions properly + +### Linux/macOS +- Test signal handlers work correctly +- Verify ANSI color detection +- Check terminal clearing functions + +## Adding New Features + +When adding features: +1. Update the public API in `include/testcoe/testcoe.hpp` if needed +2. Add implementation in appropriate source file +3. Create example demonstrating the feature +4. Add tests covering the new functionality +5. Update documentation + +## Debugging Tips + +### Debugging Grid Display +- Set breakpoints in `GridListener::printGrid()` +- Check stream redirection in `OnTestStart/End` + +### Debugging Crashes +- Test signal handlers with example crash tests +- Verify backward-cpp integration +- Check platform-specific code paths + +## Questions? + +Feel free to open an issue for: +- Bug reports +- Feature requests +- Questions about the codebase +- Discussion about implementation \ No newline at end of file diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..4f15efd --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,41 @@ +# testcoe Roadmap + +## Version History + +### v0.1.0 (2024-05-31) - Initial Release +- βœ… Grid-based visualization for test execution +- βœ… Real-time test progress display +- βœ… Enhanced crash handling with stack traces +- βœ… Cross-platform support (Windows, Linux, macOS) +- βœ… Test filtering API (run_suite, run_test) +- βœ… Terminal ANSI color support detection +- βœ… Comprehensive examples and integration tests +- βœ… Support for MSVC, GCC, Clang, and MinGW compilers + +## Future Plans + +- ⏳ Customizable grid layout and colors +- ⏳ JSON/XML test report generation +- ⏳ Test execution time tracking and reporting +- ⏳ Parallel test execution visualization +- ⏳ Interactive test selection mode +- ⏳ Test history and trend analysis +- ⏳ Integration with popular IDEs +- ⏳ Support for Google Test's sharding feature +- ⏳ Custom test status indicators +- ⏳ Test dependency visualization +- ⏳ Performance profiling integration +- ⏳ Remote test execution monitoring +- ⏳ Test coverage visualization +- ⏳ Support for other test frameworks + +## Feature Requests + +Have an idea for testcoe? Please open an issue on GitHub with the "enhancement" label. + +## Versioning + +testcoe follows [Semantic Versioning](https://semver.org/): +- MAJOR version for incompatible API changes +- MINOR version for backwards-compatible functionality additions +- PATCH version for backwards-compatible bug fixes \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 0c8fa4f..98a339f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1,24 @@ +cmake_minimum_required(VERSION 3.14) +project(testcoe_examples LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(NOT TARGET testcoe) + message(STATUS "Building examples standalone - will find or fetch testcoe") + find_package(testcoe QUIET) + if(NOT testcoe_FOUND) + message(STATUS "testcoe not found, fetching from source") + include(FetchContent) + FetchContent_Declare( + testcoe + GIT_REPOSITORY https://github.com/nircoe/testcoe.git + GIT_TAG main # change to v0.1.0 later on + ) + FetchContent_MakeAvailable(testcoe) + endif() +endif() + add_subdirectory(basic) add_subdirectory(crash) add_subdirectory(filter) diff --git a/examples/README.md b/examples/README.md index fac78a1..32cfcb8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,86 +1,21 @@ # testcoe Examples -This directory contains examples demonstrating how to use testcoe in your projects. +Examples demonstrating testcoe usage: -## Directory Structure +- `basic/` - Grid visualization with multiple test files +- `crash/` - Crash handling and stack traces +- `filter/` - Running specific tests or suites -``` -examples/ -β”œβ”€β”€ basic/ # Basic usage example -β”‚ β”œβ”€β”€ CMakeLists.txt -β”‚ β”œβ”€β”€ main.cpp # Main program initializing testcoe -β”‚ β”œβ”€β”€ math_tests.cpp # Mathematical test cases -β”‚ β”œβ”€β”€ string_tests.cpp # String operation test cases -β”‚ β”œβ”€β”€ vector_tests.cpp # Vector operation test cases -β”‚ └── README.md # Example-specific instructions -β”‚ -β”œβ”€β”€ crash_handling/ # Crash handling example -β”‚ β”œβ”€β”€ CMakeLists.txt -β”‚ β”œβ”€β”€ main.cpp # Main program initializing testcoe -β”‚ β”œβ”€β”€ crash_tests.cpp # Various crash test cases -β”‚ └── README.md # Example-specific instructions -β”‚ -β”œβ”€β”€ filtering/ # Test filtering example -β”‚ β”œβ”€β”€ CMakeLists.txt -β”‚ β”œβ”€β”€ main.cpp # Main program with filtering options -β”‚ β”œβ”€β”€ test_suites.cpp # Multiple test suites -β”‚ └── README.md # Example-specific instructions -β”‚ -β”œβ”€β”€ CMakeLists.txt # Main CMake file for all examples -└── README.md # This file -``` - -## Building All Examples - -To build all examples at once: +## Quick Start ```bash -mkdir -p build -cd build -cmake .. -cmake --build . -``` - -Or build a specific example: +mkdir build && cd build +cmake .. && cmake --build . -```bash -cmake --build . --target basic_example +# Run examples +./examples/basic/basic_example +./examples/crash/crash_example +./examples/filter/filter_example --help ``` -## Running the Examples - -Each example can be run individually. See the README.md file in each example -directory for specific instructions. - -```bash -# Run the basic example -./basic/basic_example - -# Run the crash handling example -./crash_handling/crash_example - -# Run the filtering example -./filtering/filtering_example -``` - -## Key Concepts Demonstrated - -1. **Basic Example** - Shows how to use testcoe's grid visualization with multiple test files. - -2. **Crash Handling Example** - Demonstrates testcoe's ability to catch crashes and provide detailed stack traces. - -3. **Filtering Example** - Shows how to use testcoe's API to run specific test suites or individual tests. - -## Getting Started - -If you're new to testcoe, start with the Basic Example to understand the core functionality, then explore the other examples to learn about additional features. - -## Integration into Your Project - -To use testcoe in your own project, you only need to: - -1. Add testcoe to your project (via CMake's FetchContent or as a dependency) -2. Initialize it with `testcoe::init()` -3. Run tests with `testcoe::run()` (or the other run methods) - -Note that testcoe already includes and links against Google Test, so you don't need to find or fetch GTest separately. \ No newline at end of file +See each example's README for more details. \ No newline at end of file diff --git a/examples/basic/CMakeLists.txt b/examples/basic/CMakeLists.txt index 088dc9d..4dcc5e5 100644 --- a/examples/basic/CMakeLists.txt +++ b/examples/basic/CMakeLists.txt @@ -4,16 +4,18 @@ project(testcoe_basic_example LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # Find testcoe package or fetch it from GitHub -find_package(testcoe QUIET) -if(NOT testcoe_FOUND) - include(FetchContent) - FetchContent_Declare( - testcoe - GIT_REPOSITORY https://github.com/nircoe/testcoe.git - GIT_TAG v0.1.0 - ) - FetchContent_MakeAvailable(testcoe) + find_package(testcoe QUIET) + if(NOT testcoe_FOUND) + include(FetchContent) + FetchContent_Declare( + testcoe + GIT_REPOSITORY https://github.com/nircoe/testcoe.git + GIT_TAG main # use v0.1.0 later on + ) + FetchContent_MakeAvailable(testcoe) + endif() endif() # Note: You don't need to find or fetch GTest separately diff --git a/examples/crash/CMakeLists.txt b/examples/crash/CMakeLists.txt index 686ba1b..8856193 100644 --- a/examples/crash/CMakeLists.txt +++ b/examples/crash/CMakeLists.txt @@ -6,18 +6,22 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Enable debug symbols and stack trace info set(CMAKE_BUILD_TYPE Debug) -add_compile_options(-fno-omit-frame-pointer) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options(-fno-omit-frame-pointer) +endif() +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # Find testcoe package or fetch it from GitHub -find_package(testcoe QUIET) -if(NOT testcoe_FOUND) - include(FetchContent) - FetchContent_Declare( - testcoe - GIT_REPOSITORY https://github.com/nircoe/testcoe.git - GIT_TAG v0.1.0 - ) - FetchContent_MakeAvailable(testcoe) + find_package(testcoe QUIET) + if(NOT testcoe_FOUND) + include(FetchContent) + FetchContent_Declare( + testcoe + GIT_REPOSITORY https://github.com/nircoe/testcoe.git + GIT_TAG main # use v0.1.0 later on + ) + FetchContent_MakeAvailable(testcoe) + endif() endif() # Note: You don't need to find or fetch GTest separately diff --git a/examples/crash/README.md b/examples/crash/README.md index 33b30ea..8333e41 100644 --- a/examples/crash/README.md +++ b/examples/crash/README.md @@ -23,14 +23,14 @@ By default, all crash-causing tests are skipped to prevent unintentional crashes ```bash # Run the example with all tests (non-crashing) -./crash_example +./examples/crash/crash_example ``` To run a specific crash test, use the Google Test filter mechanism: ```bash # Run only the segmentation fault test -./crash_example --gtest_filter=CrashTest.SegmentationFault +./examples/crash/crash_example --gtest_filter=CrashTests.SegmentationFault ``` Or use the provided CMake targets: diff --git a/examples/crash/crash_tests.cpp b/examples/crash/crash_tests.cpp index 8ef2578..adf85cb 100644 --- a/examples/crash/crash_tests.cpp +++ b/examples/crash/crash_tests.cpp @@ -4,7 +4,9 @@ #include #include -class CrashTests : public ::testing::Test { }; +class CrashTests : public ::testing::Test +{ +}; //============================================================================== // Segmentation Fault Test @@ -16,7 +18,7 @@ TEST(CrashTests, SegmentationFault) std::cout << "This test will cause a segmentation fault by dereferencing a null pointer." << std::endl; // Skip by default - uncomment the next line and comment the GTEST_SKIP to run - GTEST_SKIP() << "Skipping intentional crash test"; + // GTEST_SKIP() << "Skipping intentional crash test"; // This will cause a segmentation fault int *nullPtr = nullptr; @@ -36,7 +38,7 @@ TEST(CrashTests, DivideByZero) std::cout << "This test will cause a floating point exception by dividing by zero." << std::endl; // Skip by default - uncomment the next line and comment the GTEST_SKIP to run - GTEST_SKIP() << "Skipping intentional crash test"; + // GTEST_SKIP() << "Skipping intentional crash test"; // This will cause a floating point exception volatile int zero = 0; @@ -56,7 +58,7 @@ TEST(CrashTests, Abort) std::cout << "This test will cause a program abort." << std::endl; // Skip by default - uncomment the next line and comment the GTEST_SKIP to run - GTEST_SKIP() << "Skipping intentional crash test"; + // GTEST_SKIP() << "Skipping intentional crash test"; // This will abort the program std::abort(); @@ -156,7 +158,9 @@ TEST(CrashTests, StreamRedirection) // These tests ensure the example runs successfully when no crash tests are enabled -class BasicTests : public ::testing::Test { }; +class BasicTests : public ::testing::Test +{ +}; TEST(BasicTests, Addition) { diff --git a/examples/crash/main.cpp b/examples/crash/main.cpp index 99520dd..773e6b6 100644 --- a/examples/crash/main.cpp +++ b/examples/crash/main.cpp @@ -22,6 +22,32 @@ int main(int argc, char **argv) testcoe::init(&argc, argv); // Run tests + if (argc > 1) + { + std::string arg = argv[1]; + + if (arg == "--run-segfault") + { + std::cout << "Running only the segmentation fault test..." << std::endl; + return testcoe::run_test("CrashTests", "SegmentationFault"); + } + else if (arg == "--run-abort") + { + std::cout << "Running only the abort test..." << std::endl; + return testcoe::run_test("CrashTests", "Abort"); + } + else if (arg == "--run-divbyzero") + { + std::cout << "Running only the divide by zero test..." << std::endl; + return testcoe::run_test("CrashTests", "DivideByZero"); + } + else if (arg == "--run-crash-suite") + { + std::cout << "Running only the crash test suite..." << std::endl; + return testcoe::run_suite("CrashTests"); + } + } + return testcoe::run(); // TODO: add specific test run, and why do they say "by default all crash tests are skipped"?? diff --git a/examples/filter/CMakeLists.txt b/examples/filter/CMakeLists.txt index 7dcc284..158e610 100644 --- a/examples/filter/CMakeLists.txt +++ b/examples/filter/CMakeLists.txt @@ -1,21 +1,22 @@ cmake_minimum_required(VERSION 3.14) -project(testcoe_filtering_example LANGUAGES CXX) +project(testcoe_filter_example LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # Find testcoe package or fetch it from GitHub -find_package(testcoe QUIET) -if(NOT testcoe_FOUND) - include(FetchContent) - FetchContent_Declare( - testcoe - GIT_REPOSITORY https://github.com/nircoe/testcoe.git - GIT_TAG v0.1.0 - ) - FetchContent_MakeAvailable(testcoe) + find_package(testcoe QUIET) + if(NOT testcoe_FOUND) + include(FetchContent) + FetchContent_Declare( + testcoe + GIT_REPOSITORY https://github.com/nircoe/testcoe.git + GIT_TAG main # use v0.1.0 later on + ) + FetchContent_MakeAvailable(testcoe) + endif() endif() - # Note: You don't need to find or fetch GTest separately # because testcoe already includes and links against it. @@ -32,7 +33,7 @@ target_link_libraries(filter_example ) # Add custom targets for running the example in different ways -add_custom_target(filter_example +add_custom_target(run_filter_example COMMAND filter_example --all DEPENDS filter_example WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} diff --git a/examples/filter/README.md b/examples/filter/README.md index 7076249..914758d 100644 --- a/examples/filter/README.md +++ b/examples/filter/README.md @@ -21,22 +21,22 @@ cmake --build . ### Run all tests: ```bash -./filtering_example --all +./examples/filter/filter_example --all ``` ### Run a specific test suite: ```bash -./filtering_example --suite=MathSuite +./examples/filter/filter_example --suite=MathSuite ``` ### Run a specific test: ```bash -./filtering_example --test=StringSuite.Length +./examples/filter/filter_example --test=StringSuite.Length ``` ### Show help message: ```bash -./filtering_example --help +./examples/filter/filter_example --help ``` ## Test Suites Available diff --git a/include/testcoe/signal_handler.hpp b/include/testcoe/signal_handler.hpp index dafabb1..5ea724b 100644 --- a/include/testcoe/signal_handler.hpp +++ b/include/testcoe/signal_handler.hpp @@ -11,4 +11,5 @@ namespace testcoe void signalHandler(int signal); void installSignalHandlers(); + void setupStackTraceEnhancements(); } // namespace testcoe \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 83b999e..96534ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,17 +19,30 @@ endif() target_link_libraries(testcoe PRIVATE - ${GTEST_TARGET} ${BACKWARD_TARGET} PUBLIC + ${GTEST_TARGET} testcoe_headers ) +if(WIN32) + target_link_libraries(testcoe PRIVATE + dbghelp + psapi + imagehlp + ) +endif() + target_include_directories(testcoe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ) +get_target_property(BACKWARD_INCLUDE_DIRS backward INTERFACE_INCLUDE_DIRECTORIES) +if(BACKWARD_INCLUDE_DIRS) + target_include_directories(testcoe SYSTEM PRIVATE ${BACKWARD_INCLUDE_DIRS}) +endif() + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(testcoe PRIVATE -Wall -Wextra -Werror) elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") diff --git a/src/grid_listener.cpp b/src/grid_listener.cpp index 13285c6..0f3e120 100644 --- a/src/grid_listener.cpp +++ b/src/grid_listener.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace testcoe { @@ -28,7 +29,7 @@ namespace testcoe << color::yellow << "R" << color::reset << " - Running, " << ". - Not run yet\n\n"; - int maxNameLength; + std::size_t maxNameLength; if(!m_suiteTestStatus.empty()) { maxNameLength = std::max_element(m_suiteTestStatus.begin(), m_suiteTestStatus.end(), @@ -66,6 +67,8 @@ namespace testcoe case TestStatus::Failed: color = color::red; break; + default: + break; } std::cout << color << static_cast(status) << color::reset; @@ -199,7 +202,7 @@ namespace testcoe void GridTestListener::OnTestIterationEnd(const testing::UnitTest &, int) { } - void GridTestListener::OnTestProgramEnd(const testing::UnitTest &unitTest) + void GridTestListener::OnTestProgramEnd(const testing::UnitTest &) { std::cout.rdbuf(m_originalCoutBuf); std::cerr.rdbuf(m_originalCerrBuf); diff --git a/src/signal_handler.cpp b/src/signal_handler.cpp index 7a5bddc..6941352 100644 --- a/src/signal_handler.cpp +++ b/src/signal_handler.cpp @@ -1,39 +1,140 @@ #include #include +#include +#include #define BACKWARD_HAS_BFD 0 #include +// Windows-specific includes for SEH and better stack traces +#ifdef _WIN32 +#include +#endif + namespace testcoe { std::streambuf *g_originalCoutBuf = nullptr; std::streambuf *g_originalCerrBuf = nullptr; +#ifdef _WIN32 + // Windows structured exception handler for better crash detection + LONG WINAPI windowsExceptionHandler(EXCEPTION_POINTERS *pExceptionPtrs) + { + // Immediate debug output to see if we even get here + OutputDebugStringA("Windows exception handler called\n"); + + // Restore streams first + if (g_originalCoutBuf) + std::cout.rdbuf(g_originalCoutBuf); + if (g_originalCerrBuf) + std::cerr.rdbuf(g_originalCerrBuf); + + // Flush any pending output first + std::cout.flush(); + std::cerr.flush(); + + std::cerr << std::endl + << std::endl + << "====== TEST TERMINATED BY EXCEPTION ======" << std::endl; + + // Validate exception pointer before using it + if (!pExceptionPtrs || !pExceptionPtrs->ExceptionRecord) + { + std::cerr << "Invalid exception pointer!" << std::endl; + std::cerr.flush(); + ExitProcess(1); + return EXCEPTION_EXECUTE_HANDLER; + } + + // Store exception code safely + DWORD exceptionCode = pExceptionPtrs->ExceptionRecord->ExceptionCode; + + std::cerr << "Windows Exception Code: 0x" << std::hex << exceptionCode << std::dec; + + // Translate common Windows exceptions to readable messages + switch (exceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: + std::cerr << " (ACCESS_VIOLATION: Segmentation fault - memory access violation)"; + break; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + std::cerr << " (ARRAY_BOUNDS_EXCEEDED: Array index out of bounds)"; + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + std::cerr << " (INT_DIVIDE_BY_ZERO: Integer division by zero)"; + break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + std::cerr << " (FLT_DIVIDE_BY_ZERO: Floating point division by zero)"; + break; + case EXCEPTION_STACK_OVERFLOW: + std::cerr << " (STACK_OVERFLOW: Stack overflow)"; + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + std::cerr << " (ILLEGAL_INSTRUCTION: Illegal instruction)"; + break; + default: + std::cerr << " (Unknown Windows exception)"; + break; + } + + std::cerr << std::endl; + std::cerr.flush(); + + // Generate enhanced stack trace using backward-cpp + backward::StackTrace stacktrace; + stacktrace.load_here(); + + backward::Printer printer; + printer.object = true; + printer.color_mode = backward::ColorMode::always; + printer.address = true; + printer.snippet = true; // Show source code snippets if available + + printer.print(stacktrace, std::cerr); + + std::cerr << std::endl + << "===== END OF CRASH REPORT =====" << std::endl + << std::endl; + + // Ensure all output is flushed before terminating + std::cerr.flush(); + std::cout.flush(); + + // Give the console time to process the output + Sleep(500); // Increased delay + + // Terminate the process + ExitProcess(1); + return EXCEPTION_EXECUTE_HANDLER; + } +#endif + void signalHandler(int signal) { - if(g_originalCoutBuf) + if (g_originalCoutBuf) std::cout.rdbuf(g_originalCoutBuf); - if(g_originalCerrBuf) + if (g_originalCerrBuf) std::cerr.rdbuf(g_originalCerrBuf); - std::cerr << std::endl << std::endl << "====== TEST TERMINATED BY SIGNAL ======" << std::endl; + std::cerr << std::endl + << std::endl + << "====== TEST TERMINATED BY SIGNAL ======" << std::endl; std::cerr << "Test crashed with signal " << signal; - if(signal == SIGSEGV) + if (signal == SIGSEGV) std::cerr << " (SIGSEGV: Segmentation fault - likely memory access violation)"; - else if(signal == SIGABRT) + else if (signal == SIGABRT) std::cerr << " (SIGABRT: Abort - likely assertion failure or std::abort call)"; - else if(signal == SIGFPE) + else if (signal == SIGFPE) std::cerr << " (SIGFPE: Floating point exception)"; - else if(signal == SIGILL) + else if (signal == SIGILL) std::cerr << " (SIGILL: Illegal instruction)"; - else if(signal == SIGTERM) + else if (signal == SIGTERM) std::cerr << " (SIGTERM: Termination request)"; - else if(signal == SIGINT) + else if (signal == SIGINT) std::cerr << " (SIGINT: Interrupt)"; std::cerr << std::endl; - std::cerr << std::endl << "Stack trace:" << std::endl; - + backward::StackTrace stacktrace; stacktrace.load_here(); @@ -51,13 +152,37 @@ namespace testcoe exit(128 + signal); } + void setupStackTraceEnhancements() + { +#ifdef _WIN32 + // Initialize Windows Debug Help Library for better symbol resolution + HANDLE process = GetCurrentProcess(); + SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES); + SymInitialize(process, NULL, TRUE); + + // Install Windows structured exception handler + SetUnhandledExceptionFilter(windowsExceptionHandler); + + std::cout << "Windows stack trace enhancements enabled." << std::endl; +#endif + } + void installSignalHandlers() { + std::cout << "Installing signal handlers..." << std::endl; + + testing::GTEST_FLAG(catch_exceptions) = false; + std::cout << "Disabled Google Test exception catching for better crash reporting." << std::endl; + signal(SIGSEGV, signalHandler); signal(SIGABRT, signalHandler); signal(SIGFPE, signalHandler); signal(SIGILL, signalHandler); signal(SIGTERM, signalHandler); signal(SIGINT, signalHandler); + + setupStackTraceEnhancements(); + + std::cout << "Signal handlers installed successfully." << std::endl; } } // namespace testcoe \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e69de29..050f8a0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.14) + +# Create test executable for example integration tests only +add_executable(testcoe_tests + examples_integration_tests.cpp +) + +# Link ONLY with Google Test - we're testing by running examples as separate processes +if(TARGET GTest::Main) + target_link_libraries(testcoe_tests PRIVATE GTest::Main GTest::gtest) +else() + target_link_libraries(testcoe_tests PRIVATE gtest_main gtest) +endif() + +# Make sure examples are built before running tests +add_dependencies(testcoe_tests basic_example crash_example filter_example) + +# Compiler flags +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(testcoe_tests PRIVATE -Wall -Wextra -Werror) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options(testcoe_tests PRIVATE /W3 /WX) +endif() + +# Target to run tests +add_custom_target(run_tests + COMMAND testcoe_tests + DEPENDS testcoe_tests + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running testcoe example integration tests" +) + +# Target to run tests including disabled crash tests (for manual testing) +add_custom_target(run_all_tests + COMMAND testcoe_tests --gtest_also_run_disabled_tests + DEPENDS testcoe_tests + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running ALL testcoe tests including crash tests" +) \ No newline at end of file diff --git a/tests/examples_integration_tests.cpp b/tests/examples_integration_tests.cpp new file mode 100644 index 0000000..6289e46 --- /dev/null +++ b/tests/examples_integration_tests.cpp @@ -0,0 +1,357 @@ +#include +#include +#include +#include +#include +#include +#include + +// Platform-specific includes +#ifdef _WIN32 +#include +#include +#include +#include +#ifndef MAX_PATH +#define MAX_PATH 260 +#endif +#else +#include +#include +#include +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif +#endif + +class ExampleTests : public ::testing::Test +{ +protected: + // Helper function to run a command and capture its output (cross-platform) + std::string runCommand(const std::string &command) + { + std::string result; + std::string fullCommand = command; + + // Redirect stderr to stdout for both platforms + fullCommand += " 2>&1"; + +#ifdef _WIN32 + FILE *pipe = _popen(fullCommand.c_str(), "r"); +#else + FILE *pipe = popen(fullCommand.c_str(), "r"); +#endif + + if (!pipe) + { + return "ERROR: Failed to run command: " + command; + } + + char buffer[512]; // Larger buffer for better performance + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) + { + result += buffer; + } + + int exitCode; +#ifdef _WIN32 + exitCode = _pclose(pipe); +#else + exitCode = pclose(pipe); + // On Unix, pclose returns the exit status in a format that needs WEXITSTATUS + if (WIFEXITED(exitCode)) + { + exitCode = WEXITSTATUS(exitCode); + } +#endif + + // Store exit code for debugging if needed + (void)exitCode; + + return result; + } + + // Helper to check if executable exists (cross-platform) + bool executableExists(const std::string &path) + { +#ifdef _WIN32 + // On Windows, use _access to check file existence and readability + return (_access(path.c_str(), 0) == 0); +#else + // On Unix-like systems, check if file exists and is executable + return (access(path.c_str(), F_OK) == 0 && access(path.c_str(), X_OK) == 0); +#endif + } + + // Helper to get the directory where the test executable is located + std::string getExecutableDirectory() + { +#ifdef _WIN32 + char path[MAX_PATH]; + DWORD length = GetModuleFileNameA(NULL, path, MAX_PATH); + if (length == 0) + { + return "."; // Fallback to current directory + } + + std::string exePath(path); + size_t lastSlash = exePath.find_last_of("\\/"); + if (lastSlash != std::string::npos) + { + return exePath.substr(0, lastSlash); + } + return "."; +#else + char path[PATH_MAX]; + ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (length == -1) + { + return "."; // Fallback to current directory + } + + path[length] = '\0'; + std::string exePath(path); + size_t lastSlash = exePath.find_last_of('/'); + if (lastSlash != std::string::npos) + { + return exePath.substr(0, lastSlash); + } + return "."; +#endif + } + + // Helper to get the correct executable path based on build system + std::string getExecutablePath(const std::string &exampleName) + { + // Get the directory where this test executable is located + std::string testDir = getExecutableDirectory(); + + // Try different relative paths from the test executable location + std::vector possiblePaths; + +#ifdef _WIN32 + // Windows paths - calculate from test executable location + possiblePaths = { + // From tests/Debug to examples (typical MSVC structure) + testDir + "/../../examples/" + exampleName + "/Debug/" + exampleName + "_example.exe", + testDir + "/../../examples/" + exampleName + "/Release/" + exampleName + "_example.exe", + testDir + "/../../examples/" + exampleName + "/" + exampleName + "_example.exe", + // From build/tests/Debug to examples + testDir + "/../examples/" + exampleName + "/Debug/" + exampleName + "_example.exe", + testDir + "/../examples/" + exampleName + "/Release/" + exampleName + "_example.exe", + testDir + "/../examples/" + exampleName + "/" + exampleName + "_example.exe", + // Same directory as test + testDir + "/" + exampleName + "_example.exe"}; +#else + // Unix-like systems + possiblePaths = { + testDir + "/../examples/" + exampleName + "/" + exampleName + "_example", + testDir + "/../../examples/" + exampleName + "/" + exampleName + "_example", + testDir + "/" + exampleName + "_example"}; +#endif + + // Return the first path that exists + for (const auto &path : possiblePaths) + { + if (executableExists(path)) + { + return path; + } + } + + // If none found, return the most likely path for error reporting +#ifdef _WIN32 + return testDir + "/../../examples/" + exampleName + "/Debug/" + exampleName + "_example.exe"; +#else + return testDir + "/../examples/" + exampleName + "/" + exampleName + "_example"; +#endif + } +}; + +// Test that basic example runs and shows expected output +TEST_F(ExampleTests, BasicExampleRuns) +{ + std::string executable = getExecutablePath("basic"); + + // Check if executable exists + ASSERT_TRUE(executableExists(executable)) << "Basic example executable not found: " << executable; + + // Run the basic example + std::string output = runCommand(executable); + + // Debug output for CI + std::cout << "Basic example output length: " << output.length() << std::endl; + if (output.find("ERROR:") != std::string::npos) + { + std::cout << "Error in output: " << output << std::endl; + } + + // Verify it contains expected elements + EXPECT_TRUE(output.find("testcoe Basic Example") != std::string::npos) + << "Missing header in basic example output"; + + EXPECT_TRUE(output.find("Running") != std::string::npos) + << "Missing 'Running' text in output"; + + EXPECT_TRUE(output.find("MathTest") != std::string::npos) + << "Missing MathTest suite in output"; + + EXPECT_TRUE(output.find("StringTest") != std::string::npos) + << "Missing StringTest suite in output"; + + EXPECT_TRUE(output.find("VectorTest") != std::string::npos) + << "Missing VectorTest suite in output"; + + // Should show grid visualization elements + EXPECT_TRUE(output.find("P - Passed") != std::string::npos || + output.find("Passed") != std::string::npos) + << "Missing grid legend in output"; + + // Should show summary + EXPECT_TRUE(output.find("Test Summary") != std::string::npos || + output.find("Summary") != std::string::npos) + << "Missing test summary in output"; +} + +// Test that filter example runs with different options +TEST_F(ExampleTests, FilterExampleRuns) +{ + std::string executable = getExecutablePath("filter"); + + ASSERT_TRUE(executableExists(executable)) << "Filter example executable not found: " << executable; + + // Test help option + std::string helpOutput = runCommand(executable + " --help"); + EXPECT_TRUE(helpOutput.find("Usage:") != std::string::npos) + << "Help option not working. Output: " << helpOutput; + + // Test running all tests + std::string allOutput = runCommand(executable + " --all"); + EXPECT_TRUE(allOutput.find("Running all tests") != std::string::npos) + << "All tests option not working"; + EXPECT_TRUE(allOutput.find("MathSuite") != std::string::npos) + << "Missing MathSuite in filter example"; + + // Test running specific suite + std::string suiteOutput = runCommand(executable + " --suite=MathSuite"); + EXPECT_TRUE(suiteOutput.find("Running suite: MathSuite") != std::string::npos) + << "Suite filtering not working"; + EXPECT_TRUE(suiteOutput.find("MathSuite") != std::string::npos) + << "MathSuite not run when filtered"; + + // Test running specific test + std::string testOutput = runCommand(executable + " --test=MathSuite.Addition"); + EXPECT_TRUE(testOutput.find("Running test: MathSuite.Addition") != std::string::npos) + << "Test filtering not working"; +} + +// Test that crash example runs (without actual crashes) +TEST_F(ExampleTests, CrashExampleRuns) +{ + std::string executable = getExecutablePath("crash"); + + ASSERT_TRUE(executableExists(executable)) << "Crash example executable not found: " << executable; + + // Run without crash flags (should run basic tests only) + std::string output = runCommand(executable); + + EXPECT_TRUE(output.find("testcoe Crash Handling Example") != std::string::npos) + << "Missing crash example header"; + + EXPECT_TRUE(output.find("BasicTests") != std::string::npos) + << "Missing BasicTests in crash example"; + + // Should mention crash test skipping (look for various skip-related words) + bool hasSkipMessage = (output.find("Skipping") != std::string::npos) || + (output.find("SKIP") != std::string::npos) || + (output.find("skipped") != std::string::npos) || + (output.find("disabled") != std::string::npos); + + EXPECT_TRUE(hasSkipMessage) + << "Crash tests should be skipped by default. Output: " << output; +} + +// Test that crash example actually handles crashes (this test is risky!) +TEST_F(ExampleTests, DISABLED_CrashExampleHandlesCrashes) +{ + // This test is disabled by default because it intentionally crashes + // Enable it by running: --gtest_also_run_disabled_tests + + std::string executable = getExecutablePath("crash"); + + ASSERT_TRUE(executableExists(executable)) << "Crash example executable not found: " << executable; + + // Test segfault handling + std::string crashOutput = runCommand(executable + " --run-segfault"); + + // Should contain crash report + EXPECT_TRUE(crashOutput.find("TEST TERMINATED BY") != std::string::npos) + << "Missing crash termination message"; + + EXPECT_TRUE(crashOutput.find("Stack trace") != std::string::npos || + crashOutput.find("CRASH REPORT") != std::string::npos) + << "Missing stack trace in crash output"; +} + +// Test that examples are built and available +TEST_F(ExampleTests, AllExamplesExist) +{ + // Check that all expected executables exist + std::vector examples = { + "basic", + "crash", + "filter"}; + + for (const auto &example : examples) + { + std::string exe = getExecutablePath(example); + EXPECT_TRUE(executableExists(exe)) << "Missing example: " << exe; + } +} + +// Test that examples produce reasonable exit codes +TEST_F(ExampleTests, ExampleExitCodes) +{ + // Basic example should not crash (exit code < 128) + std::string basicCmd = getExecutablePath("basic"); + + // Cross-platform output redirection +#ifdef _WIN32 + basicCmd += " >NUL 2>&1"; +#else + basicCmd += " >/dev/null 2>&1"; +#endif + + int basicResult = system(basicCmd.c_str()); + + // On Unix systems, system() returns exit code * 256, so we need to extract the real exit code +#ifndef _WIN32 + if (WIFEXITED(basicResult)) + { + basicResult = WEXITSTATUS(basicResult); + } +#endif + + EXPECT_LT(basicResult, 128) << "Basic example crashed with exit code: " << basicResult; + + // Filter example with help should exit cleanly + std::string filterCmd = getExecutablePath("filter"); + filterCmd += " --help"; + +#ifdef _WIN32 + filterCmd += " >NUL 2>&1"; +#else + filterCmd += " >/dev/null 2>&1"; +#endif + + int filterResult = system(filterCmd.c_str()); + +#ifndef _WIN32 + if (WIFEXITED(filterResult)) + { + filterResult = WEXITSTATUS(filterResult); + } +#endif + + EXPECT_EQ(filterResult, 0) << "Filter example help failed with exit code: " << filterResult; +} \ No newline at end of file diff --git a/tests/grid_listeners_tests.cpp b/tests/grid_listeners_tests.cpp deleted file mode 100644 index 6021a3d..0000000 --- a/tests/grid_listeners_tests.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include -#include -#include -#include - -class GridListenerTests : public ::testing::Test -{ -protected: - std::streambuf *m_originalCoutBuf; - std::streambuf *m_originalCerrBuf; - std::stringstream m_capturedOutput; - - void SetUp() override - { - m_originalCoutBuf = std::cout.rdbuf(); - m_originalCerrBuf = std::cerr.rdbuf(); - std::cout.rdbuf(m_capturedOutput.rdbuf()); - std::cerr.rdbuf(m_capturedOutput.rdbuf()); - } - - void TearDown() override - { - std::cout.rdbuf(m_originalCoutBuf); - std::cerr.rdbuf(m_originalCerrBuf); - } -}; - -TEST_F(GridListenerTests, TestStatusEnumValues) -{ - EXPECT_EQ(static_cast(testcoe::TestStatus::NotRun), '.'); - EXPECT_EQ(static_cast(testcoe::TestStatus::Running), 'R'); - EXPECT_EQ(static_cast(testcoe::TestStatus::Passed), 'P'); - EXPECT_EQ(static_cast(testcoe::TestStatus::Failed), 'F'); - EXPECT_EQ(static_cast(testcoe::TestStatus::None), '\0'); -} - -TEST_F(GridListenerTests, ColorDefinitions) -{ - bool ansiEnabled = testcoe::terminal::isAnsiEnabled(); - - bool resetEmpty = testcoe::color::reset.empty(); - EXPECT_TRUE(ansiEnabled ? !resetEmpty : resetEmpty); - bool redEmpty = testcoe::color::red.empty(); - EXPECT_TRUE(ansiEnabled ? !redEmpty : redEmpty); - bool greenEmpty = testcoe::color::green.empty(); - EXPECT_TRUE(ansiEnabled ? !greenEmpty : greenEmpty); - bool yellowEmpty = testcoe::color::yellow.empty(); - EXPECT_TRUE(ansiEnabled ? !yellowEmpty : yellowEmpty); - bool boldEmpty = testcoe::color::bold.empty(); - EXPECT_TRUE(ansiEnabled ? !boldEmpty : boldEmpty); -} - -TEST_F(GridListenerTests, ConstructorDestructor) -{ - EXPECT_NO_THROW( - { - auto emptyListener = new ::testing::EmptyTestEventListener(); - testcoe::GridTestListener gridListener(emptyListener); - }); -} - -TEST_F(GridListenerTests, ConstructorExceptionHandling) -{ - EXPECT_THROW({ testcoe::GridTestListener gridListener(nullptr); }, std::invalid_argument); -} - -TEST_F(GridListenerTests, InstallGridListener) -{ - EXPECT_NO_THROW( - { - auto &listeners = ::testing::UnitTest::GetInstance()->listeners(); - auto defaultListener = listeners.Release(listeners.default_result_printer()); - - testcoe::installGridListener(); - - while (listeners.Release(listeners.default_result_printer())) { } - listeners.Append(defaultListener); - }); -} - -TEST_F(GridListenerTests, OnTestProgramStart) -{ - auto emptyListener = new ::testing::EmptyTestEventListener(); - testcoe::GridTestListener gridListener(emptyListener); - - m_capturedOutput.str(""); - - const ::testing::UnitTest &unitTest = *::testing::UnitTest::GetInstance(); - gridListener.OnTestProgramStart(unitTest); - - std::string output = m_capturedOutput.str(); - EXPECT_FALSE(output.empty()); - EXPECT_TRUE(output.find("Running") != std::string::npos); -} - -TEST_F(GridListenerTests, TerminalUtilities) -{ - EXPECT_NO_THROW({ bool ansiEnabled = testcoe::terminal::isAnsiEnabled(); }); - - EXPECT_NO_THROW({ testcoe::terminal::clear(); }); -} \ No newline at end of file diff --git a/tests/integration_tests.cpp b/tests/integration_tests.cpp deleted file mode 100644 index 2ed1694..0000000 --- a/tests/integration_tests.cpp +++ /dev/null @@ -1,323 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Define a test process executor that runs tests in a separate process -class TestProcessExecutor -{ -public: - static std::string runTestProcess(const std::string &executable, const std::string &arguments) - { - std::string command = executable + " " + arguments; -#ifdef _WIN32 - command += " 2>&1"; // Redirect stderr to stdout on Windows - FILE *pipe = _popen(command.c_str(), "r"); -#else - command += " 2>&1"; // Redirect stderr to stdout on Unix - FILE *pipe = popen(command.c_str(), "r"); -#endif - if (!pipe) - { - return "Error: Failed to open process"; - } - - char buffer[128]; - std::string result; - - while (!feof(pipe)) - { - if (fgets(buffer, sizeof(buffer), pipe) != nullptr) - { - result += buffer; - } - } - -#ifdef _WIN32 - _pclose(pipe); -#else - pclose(pipe); -#endif - - return result; - } - - // Helper to create a temporary test executable - static std::string createTestExecutable(const std::string &testName, const std::string &testCode) - { - // Generate unique filename - std::string filename = testName + "_test"; - std::string cppFile = filename + ".cpp"; - - // Write test code to file - std::ofstream outFile(cppFile); - outFile << testCode; - outFile.close(); - - // Compile the test (platform-specific) -#ifdef _WIN32 - std::string compileCmd = "g++ -std=c++17 -o " + filename + ".exe " + cppFile + - " -I../include -L../lib -ltestcoe -lgtest -lgtest_main -pthread"; -#else - std::string compileCmd = "g++ -std=c++17 -o " + filename + " " + cppFile + - " -I../include -L../lib -ltestcoe -lgtest -lgtest_main -pthread"; -#endif - - system(compileCmd.c_str()); - -#ifdef _WIN32 - return filename + ".exe"; -#else - return "./" + filename; -#endif - } -}; - -// Sample test suite with predefined outcomes -class SampleTestSuite : public ::testing::Test -{ -}; - -TEST_F(SampleTestSuite, PassingTest) -{ - EXPECT_TRUE(true); -} - -TEST_F(SampleTestSuite, FailingTest) -{ - EXPECT_TRUE(false); -} - -// Integration tests class -class IntegrationTests : public ::testing::Test -{ -}; - -// Test init functionality -TEST_F(IntegrationTests, InitFunctionality) -{ - // Test that testcoe::init() doesn't crash - EXPECT_NO_THROW({ - testcoe::init(); - }); - - // Test init with arguments - int argc = 1; - char *argv[] = {(char *)"test_program"}; - EXPECT_NO_THROW({ - testcoe::init(&argc, argv); - }); - - // Test exceptions are thrown for invalid arguments - EXPECT_THROW({ testcoe::init(nullptr, argv); }, std::invalid_argument); -} - -// Test run functionality -TEST_F(IntegrationTests, RunFunctionality) -{ - // Test run() doesn't crash - EXPECT_NO_THROW({ - // We're not actually running the tests, just making sure the function doesn't crash - testcoe::run(); - }); - - // Test invalid suite name throws - EXPECT_THROW({ testcoe::run_suite(""); }, std::invalid_argument); - - // Test invalid test name throws - EXPECT_THROW({ testcoe::run_test("ValidSuiteName", ""); }, std::invalid_argument); - - EXPECT_THROW({ testcoe::run_test("", "ValidTestName"); }, std::invalid_argument); -} - -// Test grid visualization for mixed test results -TEST_F(IntegrationTests, GridVisualization) -{ - // This test would run better in a separate process, but we'll do a basic check - std::string testCode = R"( - #include - #include - - TEST(GridTest, PassingTest1) { - EXPECT_TRUE(true); - } - - TEST(GridTest, PassingTest2) { - EXPECT_TRUE(true); - } - - TEST(GridTest, FailingTest) { - EXPECT_TRUE(false); - } - - int main(int argc, char** argv) { - testcoe::init(&argc, argv); - return testcoe::run(); - } - )"; - - // Skip actual test execution in normal test runs as it requires compilation - // This would typically be run in a CI environment - if (std::getenv("RUN_INTEGRATION_TESTS") != nullptr) - { - std::string executable = TestProcessExecutor::createTestExecutable("grid_viz", testCode); - std::string output = TestProcessExecutor::runTestProcess(executable, ""); - - // Verify grid visualization elements are present - EXPECT_TRUE(output.find("GridTest") != std::string::npos); - EXPECT_TRUE(output.find("P") != std::string::npos); // Passed tests - EXPECT_TRUE(output.find("F") != std::string::npos); // Failed test - } - else - { - SUCCEED() << "Skipping compilation-dependent test"; - } -} - -// Test test filtering -TEST_F(IntegrationTests, TestFiltering) -{ - std::string testCode = R"( - #include - #include - - TEST(FilterTest, Test1) { - EXPECT_TRUE(true); - } - - TEST(FilterTest, Test2) { - EXPECT_TRUE(true); - } - - TEST(OtherSuite, Test1) { - EXPECT_TRUE(true); - } - - int main(int argc, char** argv) { - testcoe::init(&argc, argv); - if (std::string(argv[1]) == "suite") { - return testcoe::run_suite(argv[2]); - } else if (std::string(argv[1]) == "test") { - return testcoe::run_test(argv[2], argv[3]); - } else { - return testcoe::run(); - } - } - )"; - - // Skip actual test execution in normal test runs - if (std::getenv("RUN_INTEGRATION_TESTS") != nullptr) - { - std::string executable = TestProcessExecutor::createTestExecutable("filter", testCode); - - // Run with filter for specific test suite - std::string output1 = TestProcessExecutor::runTestProcess(executable, "suite FilterTest"); - EXPECT_TRUE(output1.find("FilterTest") != std::string::npos); - EXPECT_TRUE(output1.find("OtherSuite") == std::string::npos); - - // Run with filter for specific test - std::string output2 = TestProcessExecutor::runTestProcess(executable, "test FilterTest Test1"); - EXPECT_TRUE(output2.find("Test1") != std::string::npos); - EXPECT_TRUE(output2.find("Test2") == std::string::npos); - } - else - { - SUCCEED() << "Skipping compilation-dependent test"; - } -} - -// Test run_suite functionality -TEST_F(IntegrationTests, RunSuiteFunctionality) -{ - std::string testCode = R"( - #include - #include - #include - - TEST(Suite1, Test1) { - std::cout << "Running Suite1.Test1" << std::endl; - EXPECT_TRUE(true); - } - - TEST(Suite1, Test2) { - std::cout << "Running Suite1.Test2" << std::endl; - EXPECT_TRUE(true); - } - - TEST(Suite2, Test1) { - std::cout << "Running Suite2.Test1" << std::endl; - EXPECT_TRUE(true); - } - - int main(int argc, char** argv) { - testcoe::init(&argc, argv); - return testcoe::run_suite(argv[1]); - } - )"; - - // Skip actual test execution in normal test runs - if (std::getenv("RUN_INTEGRATION_TESTS") != nullptr) - { - std::string executable = TestProcessExecutor::createTestExecutable("run_suite", testCode); - - // Run Suite1 - std::string output1 = TestProcessExecutor::runTestProcess(executable, "Suite1"); - EXPECT_TRUE(output1.find("Running Suite1.Test1") != std::string::npos); - EXPECT_TRUE(output1.find("Running Suite1.Test2") != std::string::npos); - EXPECT_TRUE(output1.find("Running Suite2.Test1") == std::string::npos); - - // Run Suite2 - std::string output2 = TestProcessExecutor::runTestProcess(executable, "Suite2"); - EXPECT_TRUE(output2.find("Running Suite1.Test1") == std::string::npos); - EXPECT_TRUE(output2.find("Running Suite2.Test1") != std::string::npos); - } - else - { - SUCCEED() << "Skipping compilation-dependent test"; - } -} - -// Test run_test functionality -TEST_F(IntegrationTests, RunTestFunctionality) -{ - std::string testCode = R"( - #include - #include - #include - - TEST(SpecificSuite, Test1) { - std::cout << "Running SpecificSuite.Test1" << std::endl; - EXPECT_TRUE(true); - } - - TEST(SpecificSuite, Test2) { - std::cout << "Running SpecificSuite.Test2" << std::endl; - EXPECT_TRUE(true); - } - - int main(int argc, char** argv) { - testcoe::init(&argc, argv); - return testcoe::run_test(argv[1], argv[2]); - } - )"; - - // Skip actual test execution in normal test runs - if (std::getenv("RUN_INTEGRATION_TESTS") != nullptr) - { - std::string executable = TestProcessExecutor::createTestExecutable("run_test", testCode); - - // Run specific test - std::string output = TestProcessExecutor::runTestProcess(executable, "SpecificSuite Test1"); - EXPECT_TRUE(output.find("Running SpecificSuite.Test1") != std::string::npos); - EXPECT_TRUE(output.find("Running SpecificSuite.Test2") == std::string::npos); - } - else - { - SUCCEED() << "Skipping compilation-dependent test"; - } -} \ No newline at end of file diff --git a/tests/signal_handler_tests.cpp b/tests/signal_handler_tests.cpp deleted file mode 100644 index e69de29..0000000