Skip to content

gza/gtest-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gtest-example — C++ Legacy Toolchain Demo

A minimal C++ project (a multiplication quiz) whose real purpose is to demonstrate a comfortable VS Code developer setup pinned to a legacy toolchain:

Tool Version
OS Ubuntu 20.04 (Focal)
Compiler GCC 9
Build system CMake 3.22
Package manager Conan 1.66
Test framework GoogleTest 1.8.1
Debugger GDB (bundled)

The application source is intentionally small. The real value is showing the setup: a devcontainer that pins every tool version, one-key build, F5 step-debug for both the app and the tests, and GoogleTest crash dumps on test failure.


Table of contents

  1. Quickstart (devcontainer)
  2. Project layout
  3. Build & Run
  4. Run tests
  5. Trigger a failure dump
  6. Inspect a core dump with GDB
  7. VS Code workflow
  8. Architecture diagram
  9. Troubleshooting

1. Quickstart (devcontainer)

Prerequisites

No local installation of GCC, CMake, Conan, or GDB is needed on your host machine. Everything runs inside the container.

Steps

  1. Clone this repository:

    git clone <repo-url> gtest-example
    cd gtest-example
  2. Open the folder in VS Code:

    code .
  3. VS Code detects .devcontainer/devcontainer.json and shows a notification:

    "Folder contains a Dev Container configuration file. Reopen folder to develop in a container."

    Click Reopen in Container.

    Alternatively open the Command Palette (Ctrl+Shift+P) and run:

    Dev Containers: Reopen in Container

  4. Docker builds the image (Ubuntu 20.04 + GCC 9 + CMake 3.22 + Conan 1.66). First build takes a few minutes while packages are downloaded and installed. Subsequent reopens are nearly instant (the image is cached).

  5. Once the container is ready, open VS Code's integrated terminal (Ctrl+`` ``) and verify the toolchain:

    # Compiler — should print "gcc (Ubuntu 9.x.x …) 9.x.x"
    gcc --version
    
    # CMake — should print "cmake version 3.22.6"
    cmake --version
    
    # Conan — should print "Conan version 1.66.0"
    conan --version
    
    # Debugger — should print "GNU gdb … x.x"
    gdb --version

    You are now inside a fully configured legacy C++ development environment.


2. Project layout

gtest-example/
├── .devcontainer/
│   ├── Dockerfile          # Ubuntu 20.04 image: GCC-9, CMake 3.22, Conan 1.66, GDB
│   └── devcontainer.json   # VS Code container config, extensions, postCreateCommand
│
├── .vscode/                # (Phase 4) VS Code build & debug config
│   ├── extensions.json     #   Recommended extensions
│   ├── tasks.json          #   Build tasks (Ctrl+Shift+B)
│   ├── launch.json         #   Debug configurations (F5)
│   ├── c_cpp_properties.json # IntelliSense compiler/include paths
│   └── settings.json       #   Workspace settings
│
├── src/
│   ├── quiz_lib/           # (Phase 2) Core library: question generation, scoring
│   │   ├── quiz.hpp
│   │   ├── quiz.cpp
│   │   └── CMakeLists.txt
│   └── quiz_app/           # (Phase 2) Interactive CLI: asks questions, reports score
│       ├── main.cpp
│       └── CMakeLists.txt
│
├── tests/                  # (Phase 3) GoogleTest suite
│   ├── quiz_lib_test.cpp         # Passing unit tests for quiz_lib
│   ├── quiz_failure_demo_test.cpp # Deliberately failing test (gated by env var)
│   ├── dump_on_failure_listener.hpp # Custom GTest listener: aborts on failure
│   ├── dump_on_failure_listener.cpp # … so the OS writes a core dump
│   ├── main.cpp                  # Test entry-point: registers the listener
│   └── CMakeLists.txt
│
├── conanfile.txt           # (Phase 2) Conan dependencies: gtest/1.8.1
├── CMakeLists.txt          # (Phase 2) Top-level build definition
├── .gitignore              # Excludes build/, core dumps, Conan cache
├── specs/                  # Project specification and planning documents
└── README.md               # This file

3. Build & Run

All commands are run from inside the devcontainer in the VS Code integrated terminal.

One-time setup (already done by postCreateCommand)

conan profile new default --detect and the libstdc++11 ABI fix are run automatically when the container starts. You only need to redo this if you wipe the Conan cache (~/.conan/).

Build steps

# 1. Create and enter the build directory.
mkdir -p build && cd build

# 2. Fetch GoogleTest 1.8.1 and generate conanbuildinfo.cmake.
#    --build=missing  — build any package that has no pre-built binary for
#                       this profile (expected the first time).
conan install .. --build=missing

# 3. Configure the CMake project.
#    -DCMAKE_BUILD_TYPE=Debug — keeps debug symbols for step-debugging in F5.
cmake .. -DCMAKE_BUILD_TYPE=Debug

# 4. Compile everything (quiz_lib + quiz_app).
cmake --build .

After a successful build you will see:

build/
├── bin/
│   └── quiz_app          ← the executable
├── conanbuildinfo.cmake  ← generated by conan install
└── compile_commands.json ← used by IntelliSense

Run the app

# From the repo root, or from inside build/:
./build/bin/quiz_app

Expected output (answers are yours to provide):

=== Multiplication Quiz ===
Answer 5 multiplication questions.

Q1: What is 1 × 1? 1
  Correct!
Q2: What is 2 × 1? 3
  Wrong. The answer was 2.
...
Result: 4 / 5 correct (80%)

The process exits with code 0 on a perfect score and 1 otherwise, making it scriptable.

Rebuild after source changes

CMake tracks dependencies automatically. After editing any .cpp or .hpp file, just re-run step 4:

cd build && cmake --build .

4. Run tests

Build the test binary first (if not already built — the cmake --build step from §3 builds everything including quiz_tests).

Run all tests via ctest

cd build
ctest --output-on-failure

Expected output:

Test project /workspaces/gtest-example/build
    Start 1: quiz_tests
1/1 Test #1: quiz_tests .......................   Passed   0.01 sec

100% tests passed, 0 tests failed out of 1

An XML report is written to build/test-reports/quiz_tests.xml automatically (wired into the add_test() command in tests/CMakeLists.txt).

Run the test binary directly

# From the repo root:
./build/bin/quiz_tests

# Or with an explicit XML output path and verbose output:
./build/bin/quiz_tests \
    --gtest_output=xml:build/test-reports/quiz_tests.xml \
    --gtest_color=yes

Run a single test by name

# --gtest_filter accepts wildcards; * matches any substring.
./build/bin/quiz_tests --gtest_filter=GenerateQuestion.CorrectAnswerEqualsProduct

5. Trigger a failure dump

Two environment variables control the dump pipeline:

Variable Effect
QUIZ_DEMO_FAILURE=1 Activates the intentionally-failing test in quiz_failure_demo_test.cpp. Without it the test silently passes (GTest 1.8.1 has no GTEST_SKIP()).
GTEST_DUMP_ON_FAILURE=1 Activates DumpOnFailureListener — when any test fails, std::abort() is called, raising SIGABRT.

There are two ways to capture the failure state, depending on whether the container has --privileged access.

Path A — Live inspection under GDB (works in any container)

Run the test binary directly under gdb. When std::abort() fires, gdb catches SIGABRT and you land in an interactive session with the full call stack visible:

cd /workspaces/gtest-example

QUIZ_DEMO_FAILURE=1 GTEST_DUMP_ON_FAILURE=1 \
gdb -q \
  -ex 'run --gtest_filter=DemoFailure.IntentionallyFailsWhenEnabled \
       --gtest_output=xml:build/test-reports/quiz_tests_fail.xml' \
  -ex 'bt' \
  --args build/bin/quiz_tests

GDB catches the abort and shows a backtrace like:

#2  DumpOnFailureListener::OnTestPartResult (…)  ← our listener called abort()
#6  DemoFailure_IntentionallyFailsWhenEnabled_Test::TestBody (…)
      quiz_failure_demo_test.cpp:48              ← EXPECT_EQ(2+2, 5) is here

From there you can use all the usual gdb commands (up, print, list, frame N, …) to inspect the failure live. See §6 for the command reference.

Path B — Core dump to disk (requires container rebuild)

This path requires the container to be started with --privileged so that core_pattern can be set to write dumps as plain files instead of piping them to systemd-coredump (which is not running inside Docker).

The devcontainer has already been updated (--privileged is enabled in .devcontainer/devcontainer.json). If you are in an older container:

  1. Open the Command Palette (Ctrl+Shift+P) and run:

    Dev Containers: Rebuild Container

  2. The postCreateCommand automatically sets:

    /proc/sys/kernel/core_pattern = core.%e.%p
    

    This writes core files as core.<binary>.<pid> in the process cwd.

  3. Trigger the dump:

    ulimit -c        # verify: prints "unlimited"
    
    # Run from build/dumps/ so the core lands there
    cd /workspaces/gtest-example/build/dumps
    
    QUIZ_DEMO_FAILURE=1 GTEST_DUMP_ON_FAILURE=1 \
        ../bin/quiz_tests \
        --gtest_output=xml:../test-reports/quiz_tests_fail.xml

    Expected output:

    [  FAILED  ] DemoFailure.IntentionallyFailsWhenEnabled
    [DumpOnFailureListener] GTEST_DUMP_ON_FAILURE=1 detected.
    [DumpOnFailureListener] Calling std::abort() to trigger core dump.
    Aborted (core dumped)
    
  4. Verify the core file:

    ls -lh /workspaces/gtest-example/build/dumps/
    # should show: core.quiz_tests.<pid>

Timeout-state capture (stuck test diagnosis)

The environment variable QUIZ_DEMO_HANG=1 activates a test that blocks indefinitely inside simulate_blocking_work() (which calls pause()). The VS Code task run-tests-with-timeout-dump attaches gdb from outside after a 5-second watchdog expires, captures the full thread stack, writes a core file for post-mortem use, and then terminates the hung process.

This is different from the GTEST_DUMP_ON_FAILURE path above: rather than triggering an abort inside the process on assertion failure, the watchdog observes the live stuck process from outside, which preserves the exact blocked state without perturbing it.

Run the demo (VS Code):

Open the Command Palette (Ctrl+Shift+P) → Tasks: Run Taskrun-tests-with-timeout-dump.

Run the demo (terminal):

cd /workspaces/gtest-example/build

QUIZ_DEMO_HANG=1 ./bin/quiz_tests \
    --gtest_filter=DemoHang.HangsWhenEnabled &
TEST_PID=$!

sleep 5   # watchdog timeout

sudo gdb -batch -q -p "$TEST_PID" \
    -ex "set pagination off" \
    -ex "thread apply all bt full" \
    -ex "generate-core-file dumps/core.quiz_tests.timeout" \
    -ex quit 2>&1 | tee dumps/timeout_backtrace.txt

kill "$TEST_PID" 2>/dev/null

Expected output (excerpt from build/dumps/timeout_backtrace.txt):

Thread 1 (process <PID>):
#0  __GI___libc_pause ()
#1  pause ()
#2  simulate_blocking_work ()      ← quiz_failure_demo_test.cpp
#3  DemoHang_HangsWhenEnabled_Test::TestBody ()
#4  testing::Test::Run ()
...

Artifacts written:

File Contents
build/dumps/timeout_backtrace.txt Full thread apply all bt full output
build/dumps/core.quiz_tests.timeout Core file — inspect with gdb build/bin/quiz_tests build/dumps/core.quiz_tests.timeout (see §6)

6. Inspect a core dump with GDB

Option A — Live session (no core file needed)

If you used Path A from §5 (running under gdb directly), you are already in an interactive gdb session when the abort fires. Skip to the useful commands below.

Option B — Post-mortem from a core file

If you used Path B (core dump to disk), open the dump:

cd /workspaces/gtest-example
gdb build/bin/quiz_tests build/dumps/core.quiz_tests.*

Useful GDB commands

# Full call stack at the point of abort:
(gdb) bt

# Jump to the frame with your test code (number from bt output,
# usually the TestBody frame):
(gdb) frame 6

# List source code around the current line:
(gdb) list

# Print a local variable:
(gdb) print result

# Inspect a struct:
(gdb) print q

# Quit:
(gdb) quit

Expected backtrace structure

#0  __GI_raise (sig=6)       ← C stdlib raise()
#1  __GI_abort ()            ← C stdlib abort()
#2  DumpOnFailureListener::OnTestPartResult (…)  ← our listener (dump_on_failure_listener.cpp:47)
#3  testing::internal::TestEventRepeater::OnTestPartResult (…)
#4  testing::UnitTest::AddTestPartResult (…)     ← GTest records the EXPECT_EQ failure
#5  testing::internal::AssertHelper::operator= (…)
#6  DemoFailure_IntentionallyFailsWhenEnabled_Test::TestBody (…)
       quiz_failure_demo_test.cpp:48             ← EXPECT_EQ(2+2, 5) lives here
#7  testing::Test::Run ()
...  (GTest internals)
#14 main ()  main.cpp:32

Type frame 6 to jump to the failing test body, then list and print to inspect local state.

Inspecting the XML report

cat build/test-reports/quiz_tests_fail.xml

The XML report is written before std::abort() fires (the GTest result is recorded as soon as the assertion fails; the abort happens in the listener, which runs after recording).


7. VS Code workflow

All of the following assumes you are working inside the devcontainer (VS Code shows Dev Container: C++ Legacy Toolchain in the bottom-left corner).

First time: build the project

Press Ctrl+Shift+B (or run Terminal → Run Build Task…).

This triggers the cmake-build default task, which chains:

conan-install  →  cmake-configure  →  cmake-build

All three steps run in the integrated terminal. A successful build ends with:

[100%] Built target quiz_tests

Outputs: build/bin/quiz_app and build/bin/quiz_tests.

Subsequent builds after a source-only change: press Ctrl+Shift+B again. Conan and CMake are idempotent — only changed files are recompiled.

Debug quiz_app (F5)

  1. Open src/quiz_app/main.cpp.
  2. Click in the gutter next to the line:
    quiz::Question q = quiz::generate_question(seed);
    A red dot appears — this is your breakpoint.
  3. Open the Run & Debug panel (Ctrl+Shift+D).
  4. Confirm the drop-down shows Debug quiz_app, then press F5.
  5. Execution pauses at main() (stopAtEntry: true). Press F5 again to continue to your breakpoint.
  6. Type an answer in the cppdbg: quiz_app terminal at the bottom of VS Code.
  7. Step controls:
    • F10 — step over (stay in the current function)
    • F11 — step into (enter generate_question / check_answer / Score)
    • F5 — continue to the next breakpoint
    • Shift+F5 — stop debugging

Debug quiz_tests (F5)

  1. Open tests/quiz_failure_demo_test.cpp.
  2. Set a breakpoint on the EXPECT_EQ(2 + 2, 5) line.
  3. In the Run & Debug drop-down, select Debug quiz_tests, then press F5.
  4. Execution stops at your breakpoint inside the failing test body.
    • The environment has QUIZ_DEMO_FAILURE=1 (activates the test) and GTEST_DUMP_ON_FAILURE=0 (keeps the process alive for step-debugging).
    • To observe the std::abort() call instead, change GTEST_DUMP_ON_FAILURE to "1" in .vscode/launch.json — GDB will catch SIGABRT and you can inspect the stack at the point of abort.
  5. To debug a passing test, remove the --gtest_filter arg in launch.json and set a breakpoint anywhere in tests/quiz_lib_test.cpp.

Run tests from the Command Palette

Open Ctrl+Shift+PTasks: Run Task → choose:

Task What it does
run-tests Runs all tests; writes XML report to build/test-reports/quiz_tests.xml
run-tests-with-failure-dump Activates the failing test, runs under gdb, prints backtrace
run-tests-with-timeout-dump Activates the hanging test, attaches gdb after 5 s, saves backtrace + core to build/dumps/

IntelliSense

IntelliSense is driven by build/compile_commands.json (set in .vscode/c_cpp_properties.json). If you see red squiggles on #include <gtest/gtest.h>:

  1. Make sure you have run the build at least once (Ctrl+Shift+B).
  2. Open the Command Palette → C/C++: Reset IntelliSense Database.
  3. The squiggles disappear once IntelliSense re-indexes.

8. Architecture diagram

flowchart TD
    Dev[Developer in VS Code] -->|Reopen in Container| DC[Devcontainer\nUbuntu 20.04 · GCC-9 · CMake 3.22 · Conan 1.66]
    DC -->|task: conan-install| Conan[Conan 1.66\nfetch GTest 1.8.1]
    Conan --> Build[CMake configure + build\nquiz_lib · quiz_app · quiz_tests]
    Build -->|F5: Debug quiz_app| AppDbg[GDB step-debug\nquiz_app]
    Build -->|F5: Debug quiz_tests| TestDbg[GDB step-debug\nquiz_tests]
    Build -->|task: run-tests| TestRun[quiz_tests\n--gtest_output=xml]
    TestRun -->|on failure + GTEST_DUMP_ON_FAILURE=1| Abort[Custom listener\nstd::abort]
    Abort --> Core[Core dump\nbuild/dumps/]
    TestRun --> XML[XML report\nbuild/test-reports/]
    Core -->|gdb post-mortem| Inspect[Inspect dump\nin VS Code / terminal]
Loading

9. Troubleshooting

Conan: linker errors mentioning __cxx11 or std::string ABI mismatch

Symptom: Build fails with errors like:

undefined reference to `testing::internal::…[abi:cxx11]…`

Cause: Conan 1.x defaults to the old GCC ABI (libstdc++) but Ubuntu 20.04 system headers use the new ABI (libstdc++11). The mismatch is caught at link time.

Fix: Run the two profile commands (already in postCreateCommand, but sometimes needed manually after a cache wipe):

conan profile new default --detect
conan profile update settings.compiler.libcxx=libstdc++11 default

Then re-run the full build chain (Ctrl+Shift+B or conan install → cmake → make).


Conan: ERROR: Profile not found: default

Symptom: conan install fails immediately with "Profile not found".

Cause: The Conan default profile has not been created yet (e.g. after a Conan-cache wipe or a fresh container that skipped postCreateCommand).

Fix:

conan profile new default --detect
conan profile update settings.compiler.libcxx=libstdc++11 default

Core dump not written to disk (§5 Path B)

Symptom: Running quiz_tests with GTEST_DUMP_ON_FAILURE=1 prints Aborted but no core file appears in build/dumps/.

Cause A — ulimit is 0: The per-process core size limit is 0. Check with:

ulimit -c      # should print "unlimited"

If it prints 0, open a new terminal tab in the container so that /etc/bash.bashrc (which runs ulimit -c unlimited) is sourced fresh.

Cause B — core_pattern pipes to apport / systemd-coredump: This is the default on many Ubuntu hosts. Cores are swallowed by apport or systemd-coredump instead of being written as plain files. Fix (run on the host, not inside the container):

sudo sysctl -w kernel.core_pattern=core.%e.%p

To make it permanent:

echo 'kernel.core_pattern=core.%e.%p' | \
    sudo tee /etc/sysctl.d/60-core-pattern.conf

Cause C — container not --privileged: Without --privileged the container cannot override core_pattern even from postCreateCommand. Verify: cat /proc/sys/kernel/core_pattern inside the container should show core.%e.%p. If it shows a pipe (|/usr/share/apport/…), rebuild the container after confirming "--privileged" is present in .devcontainer/devcontainer.jsonrunArgs.


GDB: No source file named quiz_failure_demo_test.cpp

Symptom: After attaching GDB, list or frame N says the source file cannot be found, or breakpoints bind but show no source.

Cause A — binary built without debug symbols: The binary was compiled in Release mode or without -g.

Fix: Rebuild with Debug type (already the default in cmake-configure):

cd build && cmake .. -DCMAKE_BUILD_TYPE=Debug && cmake --build .

Cause B — GDB source path mismatch: Add the workspace root to GDB's directory list at the (gdb) prompt:

(gdb) directory /workspaces/gtest-example

IntelliSense: red squiggles on #include <gtest/gtest.h>

Symptom: Every #include <gtest/gtest.h> is underlined red; hover shows "cannot open source file".

Cause: build/compile_commands.json does not exist yet (project not built) or IntelliSense has not re-indexed since the last build.

Fix:

  1. Build the project at least once (Ctrl+Shift+B).
  2. Open the Command Palette (Ctrl+Shift+P) → C/C++: Reset IntelliSense Database.
  3. The squiggles disappear once IntelliSense re-indexes (typically a few seconds).

Testing UI (beaker icon): no tests discovered

Symptom: The VS Code Testing panel shows no tests or spins indefinitely.

Cause A — binary not built yet: The TestMate extension scans for the executable at build/bin/quiz_tests. If it does not exist, discovery is silently skipped.

Fix: Build once (Ctrl+Shift+B).

Cause B — extension not activated: matepek.vscode-catch2-test-adapter may not have initialised yet.

Fix: Command Palette → Developer: Reload Window.


Container changes not taking effect after re-open

Symptom: A change to .devcontainer/devcontainer.json or Dockerfile (e.g. adding --privileged, updating a build arg) has no visible effect after reopening the container.

Cause: Reopen in Container reuses the cached Docker image. It does not rebuild the image.

Fix: Force a full image rebuild:

Command Palette (Ctrl+Shift+P) → Dev Containers: Rebuild Container

This re-executes all RUN layers in the Dockerfile and re-runs postCreateCommand.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors