Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ That's it! Run your tests and see the enhanced output.

## Features

- **Grid Visualization** - See all tests progress in real-time
- **Grid Visualization** - Real-time progress on interactive terminals; a single, duplication-free summary when output is piped or redirected (e.g. CI logs)
- **Crash Handling** - Get stack traces when tests crash
- **Color Support** - Automatic terminal detection
- **Test Filtering** - Run specific tests or suites
Expand Down
7 changes: 4 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ testcoe enhances Google Test by intercepting test events and providing visual fe
- **Purpose**: Implements Google Test event listener for visual grid display
- **Key Features**:
- Tracks test execution state
- Updates terminal display in real-time
- Updates terminal display in real-time on interactive terminals; when output is piped or redirected (e.g. CI logs), redraws only once, in the final summary, to avoid duplicate frames
- Collects and displays failure information
- Shows execution time statistics

Expand All @@ -59,6 +59,7 @@ testcoe enhances Google Test by intercepting test events and providing visual fe
- **Key Functions**:
- `isAnsiEnabled()` - Detects ANSI color support
- `clear()` - Clears terminal screen
- `isInteractive()` - Detects whether stdout is an interactive terminal (TTY) vs piped/redirected

## Data Flow

Expand All @@ -73,8 +74,8 @@ testcoe enhances Google Test by intercepting test events and providing visual fe
- Google Test begins execution
- GridListener receives events:
- `OnTestProgramStart` - Initialize grid display
- `OnTestStart` - Mark test as running
- `OnTestEnd` - Mark test as passed/failed
- `OnTestStart` - Mark test as running (redraws the grid only when output is interactive)
- `OnTestEnd` - Mark test as passed/failed (redraws the grid only when output is interactive)
- `OnTestProgramEnd` - Show final summary

3. **Crash Handling**:
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ cmake --build . --target run_basic_example

1. testcoe is initialized with `testcoe::init(&argc, argv)`, replacing the usual Google Test initialization.
2. Tests are run with `testcoe::run()`, replacing the usual `RUN_ALL_TESTS()`.
3. The grid visualization shows all test suites and their status in real-time.
3. The grid visualization shows all test suites and their status in real-time when run in an interactive terminal; when output is piped or redirected (e.g. `./basic_example | cat`, or a CI log), only the final grid and summary are printed once.
4. An intentional failure is included to demonstrate how failures are displayed.

## Expected Output
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ int main(int argc, char **argv)

// Run all tests
return testcoe::run();
}
}
3 changes: 1 addition & 2 deletions examples/crash/crash_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#include <vector>
#include <string>
#include <iostream>
#include <memory>
#include <cstdlib>

class CrashTests : public ::testing::Test
Expand Down Expand Up @@ -182,4 +181,4 @@ TEST(BasicTests, Addition)
TEST(BasicTests, Subtraction)
{
EXPECT_EQ(5 - 3, 2);
}
}
4 changes: 2 additions & 2 deletions examples/crash/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ int main(int argc, char **argv)
}
}

return testcoe::run();
return testcoe::run("-CrashTests.*");

// TODO: add specific test run, and why do they say "by default all crash tests are skipped"??
// you don't really do anything with the --gtest_filter flag, and we should call it "--run-test"
}
}
2 changes: 1 addition & 1 deletion examples/filter/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,4 @@ int main(int argc, char **argv)

// Default: run all tests
return testcoe::run();
}
}
1 change: 1 addition & 0 deletions include/testcoe/grid_listener.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ namespace testcoe
class GridTestListener : public testing::TestEventListener
{
testing::TestEventListener* m_originalListener;
bool m_interactive; // true only on a real TTY, gates per-event grid redraws

std::map<std::string, std::vector<TestStatus>> m_suiteTestStatus;
std::map<std::string, std::vector<std::string>> m_suiteTestNames;
Expand Down
1 change: 1 addition & 0 deletions include/testcoe/terminal_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ namespace testcoe
{
void clear();
bool isAnsiEnabled();
bool isInteractive();
} // namespace terminal
} // namespace testcoe
23 changes: 15 additions & 8 deletions src/testcoe/grid_listener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ namespace testcoe
}

GridTestListener::GridTestListener(testing::TestEventListener *listener) :
m_originalListener(listener),
m_originalListener(listener), m_interactive(terminal::isInteractive()),
m_totalTests(0), m_completedTests(0), m_passedTests(0), m_failedTests(0),
m_originalCoutBuf(std::cout.rdbuf()), m_originalCerrBuf(std::cerr.rdbuf())
{
Expand Down Expand Up @@ -122,7 +122,8 @@ namespace testcoe
std::cout << color::bold << "Running " << m_totalTests << " tests..." << std::endl
<< color::reset;

printGrid();
if (m_interactive)
printGrid();

std::cout.rdbuf(m_nullStream.rdbuf());
std::cerr.rdbuf(m_nullStream.rdbuf());
Expand Down Expand Up @@ -160,9 +161,12 @@ namespace testcoe
if(0 <= testIndex && testIndex < static_cast<int>(m_suiteTestStatus[m_currentTestSuite].size()))
m_suiteTestStatus[m_currentTestSuite][testIndex] = TestStatus::Running;

std::cout.rdbuf(m_originalCoutBuf);
printGrid();
std::cout.rdbuf(m_nullStream.rdbuf());
if (m_interactive)
{
std::cout.rdbuf(m_originalCoutBuf);
printGrid();
std::cout.rdbuf(m_nullStream.rdbuf());
}
}

void GridTestListener::OnTestPartResult(const testing::TestPartResult &testPartResult)
Expand All @@ -189,9 +193,12 @@ namespace testcoe

++m_completedTests;

std::cout.rdbuf(m_originalCoutBuf);
printGrid();
std::cout.rdbuf(m_nullStream.rdbuf());
if (m_interactive)
{
std::cout.rdbuf(m_originalCoutBuf);
printGrid();
std::cout.rdbuf(m_nullStream.rdbuf());
}
}

void GridTestListener::OnTestSuiteEnd(const testing::TestSuite &) { }
Expand Down
18 changes: 18 additions & 0 deletions src/testcoe/terminal_utils.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#include <testcoe/terminal_utils.hpp>

#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
#include <cstdio>

namespace testcoe
{
namespace terminal
Expand Down Expand Up @@ -66,5 +73,16 @@ namespace testcoe
// \033[J clears the screen from cursor to end
std::cout << "\033[H\033[J";
}

// Distinct from isAnsiEnabled(): TTY-ness, not ANSI capability. A pipe is never
// interactive regardless of color support.
bool isInteractive()
{
#ifdef _WIN32
return _isatty(_fileno(stdout)) != 0;
#else
return isatty(STDOUT_FILENO) != 0;
#endif
}
} // namespace terminal
} // namespace testcoe
37 changes: 35 additions & 2 deletions tests/examples_integration_tests.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#include <gtest/gtest.h>
#include <string>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <iostream>
#include <vector>

Expand Down Expand Up @@ -215,6 +213,41 @@ TEST_F(ExampleTests, BasicExampleRuns)
<< "Missing test summary in output";
}

// runCommand() pipes output through popen/_popen, which is never a TTY, so this covers
// the non-interactive path (see terminal::isInteractive()).
TEST_F(ExampleTests, BasicExampleNonInteractiveOutputHasNoDuplicateGridFrames)
{
std::string executable = getExecutablePath("basic");
ASSERT_TRUE(executableExists(executable)) << "Basic example executable not found: " << executable;

std::string output = runCommand(executable);

// Sits between printGrid()'s two dynamic counts, so it's never split by ANSI color codes.
const std::string gridFrameMarker = " tests... Completed: ";

std::size_t frameCount = 0;
std::size_t pos = 0;
while ((pos = output.find(gridFrameMarker, pos)) != std::string::npos)
{
++frameCount;
pos += gridFrameMarker.length();
}

EXPECT_EQ(frameCount, 1u)
<< "Non-interactive output should contain exactly one grid frame (the final summary), "
<< "not one per test start/end event. Output:\n" << output;

// Loose bound - a duplicate-frame regression would blow well past this.
std::size_t lineCount = 0;
for (char c : output)
if (c == '\n')
++lineCount;

EXPECT_LT(lineCount, 60u)
<< "Non-interactive output is unexpectedly long (" << lineCount << " lines) - possible "
<< "duplicate-frame regression. Output:\n" << output;
}

// Test that filter example runs with different options
TEST_F(ExampleTests, FilterExampleRuns)
{
Expand Down
Loading