Use the debugger to automatically debug tests

zxdb may be used in conjunction with fx test using the --break-on-failure or --breakpoint flags.

If your test uses a compatible test runner (gTest, gUnit, or Rust today), then test failures will pause test execution for that suite and show you the debugger prompt, for example:

=>  fx test --break-on-failure rust_crasher_test.cm

<...fx test startup...>

Running 1 tests

Starting: fuchsia-pkg://fuchsia.com/crasher_test#meta/rust_crasher_test.cm
Command: fx ffx test run --max-severity-logs WARN --break-on-failure fuchsia-pkg://fuchsia.com/crasher_test?hash=1cceb326c127e245f0052367142aee001f82a73c6f33091fe7999d43a94b1b34#meta/rust_crasher_test.cm

Status: [duration: 13.3s]  [tasks: 3 running, 15/18 complete]
  Running 1 tests                      [                                                                                                     ]           0.0%
⚠️  zxdb caught test failure in rust_crasher_test.cm, type `frame` to get started.
   14 LLVM_LIBC_FUNCTION(void, abort, ()) {
   15   for (;;) {
  16     CRASH_WITH_UNIQUE_BACKTRACE();
   17     _zx_process_exit(ZX_TASK_RETCODE_EXCEPTION_KILL);
   18   }
══════════════════════════
 Invalid opcode exception
══════════════════════════
 Process 1 (koid=107752) thread 1 (koid=107754)
 Faulting instruction: 0x4159210ab797

🛑 process 1 __llvm_libc::__abort_impl__()  abort.cc:16
[zxdb]

From this point, you may use zxdb as normal. Since the thread is already in a fatal exception, typical execution commands (for example step, next, and until) will not be available.

Inspection commands such as print, frame, and backtrace will be available for the duration of the debugging session.

Test failures in parallel {:#test-failures-in-parallel}

Sometimes multiple test cases may fail in parallel, depending on the options given to fx test or test runner default configurations. Supported test runners all spawn an individual process for each test case, and the default configuration may allow for multiple test processes to be running at the same time.

When running with fx test, zxdb will attach to all processes in your test's realm, and any test case failures will stop only that particular process. Parallel execution will only stop if and only if the number of test case failures is equal to the number of allowed parallel test cases. When any processes is detached from zxdb, another test case process will begin immediately.

zxdb is designed to handle multi-process debugging well. You can inspect the currently attached processes and their current execution states with the process noun, or simply with the status command. The currently “active” process will be indicated with a “▶”. Find more detailed information about the interaction model or via the help command.

Closing the debugger

When you're finished inspecting your test, you can continue by detaching from your test in any way (for example, kill, detach, and continue).

As discussed [above][#test-failures-in-parallel], multiple test cases may fail in parallel. If you do not explicitly detach from all attached processes, zxdb will remain in the foreground. You can see all attached processes using the process noun.

Certain commands will detach from everything (for example, quit, detach *, and ctrl+d) and resume execution of the test suite immediately.

Examples

Setup and background

  • {Rust}

    Rust tests are executed by the Rust test runner. Unlike gTest or gUnit runners for C++ tests, the Rust test runner defaults to running test cases in parallel. This creates a different experience while using the --break-on-failure feature. See the section for debugging [parallel processes][#test-failures-in-parallel] about expectations while debugging parallel test processes. This case is supported, and will function well with zxdb.

    Here's some sample rust test code (modified from the original), abbreviated for brevity:

    ...
    let mut log_helper2 = LogSinkHelper::new(&directory);
    log_helper2.write_log("my msg1");
    log_helper.write_log("my msg2");
    
    let mut expected = vec!["my msg1".to_owned(), "my msg3".to_owned()];
    expected.sort();
    let mut actual = vec![recv_logs.next().await.unwrap(), recv_logs.next().await.unwrap()];
    actual.sort();
    
    assert_eq!(expected, actual);
    ...
    
  • {C++}

    The gTest test runner by default executes test cases serially, so only one test failure will be debugged at a time. Executing test cases in parallel is supported, and may be done by adding the --parallel-cases flag to the fx test command line. Let's look at some sample C++ test code using gTest (abbreviated for brevity):

    // Inject 1 process.
    auto process1 = std::make_unique<MockProcess>(nullptr, kProcessKoid1, kProcessName1);
    process1->AddThread(kProcess1ThreadKoid1);
    harness.debug_agent()->InjectProcessForTest(std::move(process1));
    
    // And another, with 2 threads.
    auto process2 = std::make_unique<MockProcess>(nullptr, kProcessKoid2, kProcessName2);
    process2->AddThread(kProcess2ThreadKoid1);
    process2->AddThread(kProcess2ThreadKoid2);
    harness.debug_agent()->InjectProcessForTest(std::move(process2));
    
    reply = {};
    remote_api->OnStatus(request, &reply);
    
    ASSERT_EQ(reply.processes.size(), 3u);  // <-- This will fail, since reply.processes.size() == 2
    EXPECT_EQ(reply.processes[0].process_koid, kProcessKoid1);
    EXPECT_EQ(reply.processes[0].process_name, kProcessName1);
    ...
    

Executing your tests

We execute the tests with the fx test --break-on-failure command, for example:

  • {Rust}

    =>  fx test -o --break-on-failure archivist-unittests
    
    <...fx test startup...>
    
    Running 1 tests
    
    Starting: fuchsia-pkg://fuchsia.com/archivist-tests#meta/archivist-unittests.cm 
    Command: fx ffx test run --max-severity-logs WARN --break-on-failure fuchsia-pkg://fuchsia.com/archivist-tests?hash=9a531e48fe82d86edef22f86f7e9b819d18a7d678f0823912d9224dd91f8926f#meta/archivist-unittests.cm
    Running test 'fuchsia-pkg://fuchsia.com/archivist-tests?hash=9a531e48fe82d86edef22f86f7e9b819d18a7d678f0823912d9224dd91f8926f#meta/archivist-unittests.cm'
    
    [RUNNING]	archivist::tests::can_log_and_retrive_log
    [101430.272555][5631048][5631050][<root>][can_log_and_retrive_log] WARN: Failed to create event source for log sink requests err=Error connecting to protocol path: /events/log_sink_requested_event_stream
    
    Caused by:
        NOT_FOUND
    [101430.277339][5631048][5631050][<root>][can_log_and_retrive_log] WARN: Failed to create event source for InspectSink requests err=Error connecting to protocol path: /events/inspect_sink_requested_event_stream
    [101430.336160][5631048][5631050][<root>][can_log_and_retrive_log] INFO: archivist: Entering core loop.
    [101430.395986][5631048][5631050][<root>][can_log_and_retrive_log] ERROR: [src/lib/diagnostics/log/rust/src/lib.rs(62)] PANIC info=panicked at ../../src/diagnostics/archivist/src/archivist.rs:544:9:
    assertion `left == right` failed
      left: ["my msg1", "my msg2"]
     right: ["my msg1", "my msg3"]
    
    👋 zxdb is loading symbols to debug test failure in archivist-unittests.cm, please wait.
    ⚠️  test failure in archivist-unittests.cm, type `frame` or `help` to get started.
       11 namespace LIBC_NAMESPACE {
       12 
      13 LLVM_LIBC_FUNCTION(void, abort, ()) { CRASH_WITH_UNIQUE_BACKTRACE(); }
       14 
       15 }  // namespace LIBC_NAMESPACE
    ══════════════════════════
     Invalid opcode exception
    ══════════════════════════
     Process 10 (koid=5495424) thread 1 (koid=5495428)
     Faulting instruction: 0x41a5680114d7
    
    🛑 process 10 __llvm_libc::__abort_impl__()  abort.cc:13
    [zxdb]
    

    Notice that the output from the test is mixed up, this is because the rust test runner runs test cases in parallel by default. You can avoid this by using this --parallel-cases option to fx test, for example: fx test --parallel-cases 1 --break-on-failure archivist-unittests

  • {C++}

    =>  fx test -o --break-on-failure debug_agent_unit_tests
    
    <...fx test startup...>
    
    Starting: fuchsia-pkg://fuchsia.com/debug_agent_unit_tests#meta/debug_agent_unit_tests.cm (NOT HERMETIC)
    Command: fx ffx test run --realm /core/testing:system-tests --max-severity-logs WARN --break-on-failure fuchsia-pkg://fuchsia.com/debug_agent_unit_tests?hash=3f6d97801bb147034a344e3fe1bb69291a7b690b9d3d075246ddcba59397ac12#meta/debug_agent_unit_tests.cm
    
    Status: [duration: 30.9s]  [tasks: 3 running, 15/19 complete]
      Running 2 tests                      [                                                                                                     ]           0.0%
    ⚠️  zxdb caught test failure in debug_agent_unit_tests.cm, type `frame` to get started.
       5381      (defined(__x86_64__) || defined(__i386__)))
       5382       // with clang/gcc we can achieve the same effect on x86 by invoking int3
      5383       asm("int3");
       5384 #elif GTEST_HAS_BUILTIN(__builtin_trap)
       5385       __builtin_trap();
    🛑 thread 1 testing::UnitTest::AddTestPartResult(testing::UnitTest*, testing::TestPartResult::Type, const char*, int, std::__2::string const&, std::__2::string const&)  gtest.cc:5383
    [zxdb]
    

Examine the failure

  • {Rust}

    We caught a test failure, Rust tests issue an abort on failure, which zxdb notices and reports. We can view the code of the current frame with list, for example:

    [zxdb] list
        8 
        9 #include "src/__support/common.h"
       10 
       11 namespace LIBC_NAMESPACE {
       12 
      13 LLVM_LIBC_FUNCTION(void, abort, ()) { CRASH_WITH_UNIQUE_BACKTRACE(); }
       14 
       15 }  // namespace LIBC_NAMESPACE
    [zxdb] 
    

    But that‘s not the code we’re interested in. When we look at a stack trace, we see code from our test is in frame #17:

    [zxdb] frame
     012 «Rust library» (-r expands)
      13 std::panicking::begin_panic_handler(…)  library/std/src/panicking.rs:645
      14 core::panicking::panic_fmt(…)  library/core/src/panicking.rs:72
      15 core::panicking::assert_failed_inner(…)  library/core/src/panicking.rs:402
      16 core::panicking::assert_failed<…>(…)  /b/s/w/ir/x/w/fuchsia-third_party-rust/library/core/src/panicking.rs:357
      17 archivist_lib_lib_test::archivist::tests::can_log_and_retrive_log::test_entry_point::λ(…)  archivist.rs:544
      18 core::future::future::«impl»::poll<…>(…)  future/future.rs:123
      19 fuchsia_async::test_support::«impl»::run_singlethreaded::λ::λ(…)  test_support.rs:26
      20 fuchsia_async::test_support::«impl»::run_singlethreaded::λ::λ(…)  test_support.rs:121
      21 fuchsia_async::atomic_future::«impl»::poll<…>(…)  atomic_future.rs:78
      22 fuchsia_async::atomic_future::AtomicFuture::try_poll(…)  atomic_future.rs:223
      23 fuchsia_async::runtime::fuchsia::executor::common::Inner::try_poll(…)  executor/common.rs:588
      24 fuchsia_async::runtime::fuchsia::executor::common::Inner::poll_ready_tasks(…)  executor/common.rs:148
      25 fuchsia_async::runtime::fuchsia::executor::common::Inner::worker_lifecycle<…>(…)  executor/common.rs:448
      26 fuchsia_async::runtime::fuchsia::executor::local::LocalExecutor::run<…>(…)  executor/local.rs:100
      27 fuchsia_async::runtime::fuchsia::executor::local::LocalExecutor::run_singlethreaded<…>(…)  executor/local.rs:68
      28 fuchsia_async::test_support::«impl»::run_singlethreaded::λ()  test_support.rs:119
      29 fuchsia_async::test_support::Config::in_parallel(…)  test_support.rs:214
      30 fuchsia_async::test_support::«impl»::run_singlethreaded(…)  test_support.rs:116
      31 fuchsia_async::test_support::run_singlethreaded_test<…>(…)  test_support.rs:226
      32 fuchsia::test_singlethreaded<…>(…)  fuchsia/src/lib.rs:188
      33 archivist_lib_lib_test::archivist::tests::can_log_and_retrive_log()  archivist.rs:519
      34 archivist_lib_lib_test::archivist::tests::can_log_and_retrive_log::λ(…)  archivist.rs:520
      35 core::ops::function::FnOnce::call_once<…>(…)  /b/s/w/ir/x/w/fuchsia-third_party-rust/library/core/src/ops/function.rs:250
      36 core::ops::function::FnOnce::call_once<…>(…)  library/core/src/ops/function.rs:250 (inline)
      37 test::__rust_begin_short_backtrace<…>(…)  library/test/src/lib.rs:621
      38 test::run_test_in_spawned_subprocess(…)  library/test/src/lib.rs:749
      39 test::test_main_static_abort(…)  library/test/src/lib.rs:197
      40 archivist_lib_lib_test::main()  archivist/src/lib.rs:1
      4158 «Rust startup» (-r expands)
    [zxdb]
    

    We can view our test's source code by using the frame noun with an index as a prefix to our list command, for example:

    [zxdb] frame 17 list
       539         expected.sort();
       540 
       541         let mut actual = vec![recv_logs.next().await.unwrap(), recv_logs.next().await.unwrap()];
       542         actual.sort();
       543 
      544         assert_eq!(expected, actual);
       545 
       546         // can log after killing log sink proxy
       547         log_helper.kill_log_sink();
       548         log_helper.write_log("my msg1");
       549         log_helper.write_log("my msg2");
       550 
       551         assert_eq!(
       552             expected,
       553             vec! {recv_logs.next().await.unwrap(),recv_logs.next().await.unwrap()}
       554         );
    

    That‘s inconvenient, we have to type frame 17 before every command to interact with the part of our code we’re interested in. Notice the “▶” from the output of frame. It points to frame 0, indicating that it is the “active” frame. Let‘s select our frame as the “active” frame with just the frame noun with the frame’s index from above so we can work directly with what we want to look at:

    [zxdb] frame 17
    archivist_lib_lib_test::archivist::tests::can_log_and_retrive_log::test_entry_point::λ(…)  archivist.rs:528
    [zxdb] frame
      012 «Rust library» (-r expands)
      13 std::panicking::begin_panic_handler(…)  library/std/src/panicking.rs:645
      14 core::panicking::panic_fmt(…)  library/core/src/panicking.rs:72
      15 core::panicking::assert_failed_inner(…)  library/core/src/panicking.rs:402
      16 core::panicking::assert_failed<…>(…)  /b/s/w/ir/x/w/fuchsia-third_party-rust/library/core/src/panicking.rs:357
     17 archivist_lib_lib_test::archivist::tests::can_log_and_retrive_log::test_entry_point::λ(…)  archivist.rs:544
      18 core::future::future::«impl»::poll<…>(…)  future/future.rs:123
      19 fuchsia_async::test_support::«impl»::run_singlethreaded::λ::λ(…)  test_support.rs:26
      20 fuchsia_async::test_support::«impl»::run_singlethreaded::λ::λ(…)  test_support.rs:121
    ...
    

    Now, all commands we run will be in the context of frame #17. Let's list the source code again to be sure, with a little bit of additional context:

    [zxdb] list -c 10
       534         let mut log_helper2 = LogSinkHelper::new(&directory);
       535         log_helper2.write_log("my msg1");
       536         log_helper.write_log("my msg2");
       537 
       538         let mut expected = vec!["my msg1".to_owned(), "my msg3".to_owned()];
       539         expected.sort();
       540 
       541         let mut actual = vec![recv_logs.next().await.unwrap(), recv_logs.next().await.unwrap()];
       542         actual.sort();
       543 
      544         assert_eq!(expected, actual);
       545 
       546         // can log after killing log sink proxy
       547         log_helper.kill_log_sink();
       548         log_helper.write_log("my msg1");
       549         log_helper.write_log("my msg2");
       550 
       551         assert_eq!(
       552             expected,
       553             vec! {recv_logs.next().await.unwrap(),recv_logs.next().await.unwrap()}
       554         );
    

    Great! Now, why did the test fail? Let‘s print out some variables to see what’s going on. We have a local variable in this frame, actual, which should have some strings that we added by calling write_log on our log_helper and log_helper2 instances and by receiving them with the mpsc channel recv_logs:

    [zxdb] print expected
    vec!["my msg1", "my msg3"]
    [zxdb] print actual
    vec!["my msg1", "my msg2"]
    

    Aha, our test's expectation is slightly wrong. We expected "my msg3" to be the second string, but actually logged "my msg2". We can correct the test to expect "my msg2". We can now detach from our tests to continue and complete the test suite:

    [zxdb] quit
    
    <...fx test output continues...>
    
    Failed tests: archivist::tests::can_log_and_retrive_log
    122 out of 123 tests passed...
    
    Test fuchsia-pkg://fuchsia.com/archivist-tests?hash=8bcb30a2bfb923a4b42d1f0ea590af613ab0b1aa1ac67ada56ae4d325f3330a0#meta/archivist-unittests.cm produced unexpected high-severity logs:
    ----------------xxxxx----------------
    [105255.347070][5853309][5853311][<root>][can_log_and_retrive_log] ERROR: [src/lib/diagnostics/log/rust/src/lib.rs(62)] PANIC info=panicked at ../../src/diagnostics/archivist/src/archivist.rs:544:9:
    assertion `left == right` failed
      left: ["my msg1", "my msg2"]
     right: ["my msg1", "my msg3"]
    
    ----------------xxxxx----------------
    Failing this test. See: https://fuchsia.dev/fuchsia-src/development/diagnostics/test_and_logs#restricting_log_severity
    
    fuchsia-pkg://fuchsia.com/archivist-tests?hash=8bcb30a2bfb923a4b42d1f0ea590af613ab0b1aa1ac67ada56ae4d325f3330a0#meta/archivist-unittests.cm completed with result: FAILED
    The test was executed in the hermetic realm. If your test depends on system capabilities, pass in correct realm. See https://fuchsia.dev/go/components/non-hermetic-tests
    Tests failed.
    Deleting 1 files at /tmp/tmpgr0otc3w: ffx_logs/ffx.log
    To keep these files, set --ffx-output-directory.
    

    Now we can fix the test:

    - let mut expected = vec!["my msg1".to_owned(), "my msg3".to_owned()];
    + let mut expected = vec!["my msg1".to_owned(), "my msg2".to_owned()];
    

    and run the tests again:

    =>  fx test --break-on-failure archivist-unittests
    
    <...fx test startup...>
    
    Running 1 tests
    
    Starting: fuchsia-pkg://fuchsia.com/archivist-tests#meta/archivist-unittests.cm 
    Command: fx ffx test run --max-severity-logs WARN --break-on-failure fuchsia-pkg://fuchsia.com/archivist-tests?hash=454897cb1be6b88c2aeb4b5abf474894b629d30ca50f7dfaa23497fd3848a566#meta/archivist-unittests.cm
    Running test 'fuchsia-pkg://fuchsia.com/archivist-tests?hash=454897cb1be6b88c2aeb4b5abf474894b629d30ca50f7dfaa23497fd3848a566#meta/archivist-unittests.cm'
    [RUNNING]	accessor::tests::accessor_skips_invalid_selectors
    [RUNNING]	accessor::tests::batch_iterator_on_ready_is_called
    [RUNNING]	accessor::tests::batch_iterator_terminates_on_client_disconnect
    [RUNNING]	accessor::tests::buffered_iterator_handles_peer_closed
    [RUNNING]	accessor::tests::buffered_iterator_handles_two_consecutive_buffer_waits
    [RUNNING]	accessor::tests::logs_only_accept_basic_component_selectors
    [RUNNING]	accessor::tests::socket_writer_does_not_handle_cbor
    [RUNNING]	accessor::tests::socket_writer_handles_closed_socket
    [RUNNING]	accessor::tests::socket_writer_handles_text
    [RUNNING]	archivist::tests::can_log_and_retrive_log
    [PASSED]	accessor::tests::socket_writer_handles_text
    [RUNNING]	archivist::tests::log_from_multiple_sock
    [PASSED]	accessor::tests::buffered_iterator_handles_two_consecutive_buffer_waits
    <...lots of tests...>
    [RUNNING]	logs::tests::unfiltered_stats
    [PASSED]	logs::tests::test_debuglog_drainer
    [RUNNING]	utils::tests::drop_test
    [PASSED]	logs::tests::test_filter_by_pid
    [PASSED]	logs::tests::test_filter_by_min_severity
    [PASSED]	logs::tests::test_filter_by_tags
    [PASSED]	logs::tests::test_filter_by_tid
    [PASSED]	logs::tests::test_log_manager_dump
    [PASSED]	logs::tests::test_log_manager_simple
    [PASSED]	logs::tests::unfiltered_stats
    [PASSED]	logs::tests::test_structured_log
    [PASSED]	logs::tests::attributed_inspect_two_mixed_streams_different_identities
    [PASSED]	utils::tests::drop_test
    
    123 out of 123 tests passed...
    fuchsia-pkg://fuchsia.com/archivist-tests?hash=454897cb1be6b88c2aeb4b5abf474894b629d30ca50f7dfaa23497fd3848a566#meta/archivist-unittests.cm completed with result: PASSED
    Deleting 1 files at /tmp/tmpho9yjjz9: ffx_logs/ffx.log
    To keep these files, set --ffx-output-directory.
    
    Status: [duration: 36.4s] [tests: PASS: 1 FAIL: 0 SKIP: 0] 
      Running 1 tests                            [====================================================================================================================]            100.0% 
    
  • {C++}

    We caught a test failure, gTest has an option to insert a software breakpoint in the path of a test failure, which is inlined into our test. We can view the code of the current frame with list, for example:

    [zxdb] list
    ...
       5381      (defined(__x86_64__) || defined(__i386__)))
       5382       // with clang/gcc we can achieve the same effect on x86 by invoking int3
      5383       asm("int3");
       5384 #elif GTEST_HAS_BUILTIN(__builtin_trap)
       5385       __builtin_trap();
    ...
    

    But that‘s not the code we’re interested in. When we look at a stack trace, we see code from our test is in frame #2:

    [zxdb] frame
     0 testing::UnitTest::AddTestPartResult(…)  gtest.cc:5383
      1 testing::internal::AssertHelper::operator=(…)  gtest.cc:476
      2 debug_agent::DebugAgentTests_OnGlobalStatus_Test::TestBody(…)  debug_agent_unittest.cc:105 <-- This is the test's source code.
      3 testing::internal::HandleSehExceptionsInMethodIfSupported<…>(…) • gtest.cc:2635
      4 testing::internal::HandleExceptionsInMethodIfSupported<…>(…) • gtest.cc:2690
      5 testing::Test::Run(…) • gtest.cc:2710
      6 testing::TestInfo::Run(…) • gtest.cc:2859
      7 testing::TestSuite::Run(…) • gtest.cc:3038
      8 testing::internal::UnitTestImpl::RunAllTests(…) • gtest.cc:5942
      9 testing::internal::HandleSehExceptionsInMethodIfSupported<…>(…) • gtest.cc:2635
      10 testing::internal::HandleExceptionsInMethodIfSupported<…>(…) • gtest.cc:2690
      11 testing::UnitTest::Run(…) • gtest.cc:5506
      12 RUN_ALL_TESTS() • gtest.h:2318
      13 main(…) • run_all_unittests.cc:20
      14…17 «libc startup» (-r expands)
    [zxdb]
    

    We can view our test's source code by using the frame noun with an index as a prefix to our list command, for example:

    [zxdb] frame 2 list
       100   harness.debug_agent()->InjectProcessForTest(std::move(process2));
       101
       102   reply = {};
       103   remote_api->OnStatus(request, &reply);
       104
      105   ASSERT_EQ(reply.processes.size(), 3u); <-- This assertion failed.
       106   EXPECT_EQ(reply.processes[0].process_koid, kProcessKoid1);
       107   EXPECT_EQ(reply.processes[0].process_name, kProcessName1);
       108   ASSERT_EQ(reply.processes[0].threads.size(), 1u);
       109   EXPECT_EQ(reply.processes[0].threads[0].id.process, kProcessKoid1);
       110   EXPECT_EQ(reply.processes[0].threads[0].id.thread, kProcess1ThreadKoid1);
       111
       112   EXPECT_EQ(reply.processes[1].process_koid, kProcessKoid2);
       113   EXPECT_EQ(reply.processes[1].process_name, kProcessName2);
       114   ASSERT_EQ(reply.processes[1].threads.size(), 2u);
       115   EXPECT_EQ(reply.processes[1].threads[0].id.process, kProcessKoid2);
    

    That‘s inconvenient, we have to type frame 2 before every command to interact with the part of our code we’re interested in. Notice the “▶” from the output of frame. It points to frame 0, indicating that it is the “active” frame. Let‘s select our frame as the “active” frame with just the frame noun with the frame’s index from above so we can work directly with what we want to look at:

    [zxdb] frame 2
    debug_agent::DebugAgentTests_OnGlobalStatus_Test::TestBody(…)  debug_agent_unittest.cc:105
    [zxdb] frame
      0 testing::UnitTest::AddTestPartResult(…)  gtest.cc:5383
      1 testing::internal::AssertHelper::operator=(…)  gtest.cc:476
     2 debug_agent::DebugAgentTests_OnGlobalStatus_Test::TestBody(…)  debug_agent_unittest.cc:105
      3 testing::internal::HandleSehExceptionsInMethodIfSupported<…>(…)  gtest.cc:2635
      4 testing::internal::HandleExceptionsInMethodIfSupported<…>(…)  gtest.cc:2690
      5 testing::Test::Run(…)  gtest.cc:2710
      6 testing::TestInfo::Run(…)  gtest.cc:2859
      7 testing::TestSuite::Run(…)  gtest.cc:3038
      8 testing::internal::UnitTestImpl::RunAllTests(…)  gtest.cc:5942
      9 testing::internal::HandleSehExceptionsInMethodIfSupported<…>(…)  gtest.cc:2635
      10 testing::internal::HandleExceptionsInMethodIfSupported<…>(…)  gtest.cc:2690
      11 testing::UnitTest::Run(…)  gtest.cc:5506
      12 RUN_ALL_TESTS()  gtest.h:2318
      13 main(…)  run_all_unittests.cc:20
      1417 «libc startup» (-r expands)
    

    Now, all commands we run will be in the context of frame #2. Let's list the source code again to be sure:

    [zxdb] list
       100   harness.debug_agent()->InjectProcessForTest(std::move(process2));
       101
       102   reply = {};
       103   remote_api->OnStatus(request, &reply);
       104
      105   ASSERT_EQ(reply.processes.size(), 3u);
       106   EXPECT_EQ(reply.processes[0].process_koid, kProcessKoid1);
       107   EXPECT_EQ(reply.processes[0].process_name, kProcessName1);
       108   ASSERT_EQ(reply.processes[0].threads.size(), 1u);
       109   EXPECT_EQ(reply.processes[0].threads[0].id.process, kProcessKoid1);
       110   EXPECT_EQ(reply.processes[0].threads[0].id.thread, kProcess1ThreadKoid1);
       111
       112   EXPECT_EQ(reply.processes[1].process_koid, kProcessKoid2);
       113   EXPECT_EQ(reply.processes[1].process_name, kProcessName2);
       114   ASSERT_EQ(reply.processes[1].threads.size(), 2u);
       115   EXPECT_EQ(reply.processes[1].threads[0].id.process, kProcessKoid2);
    

    Cool! Now, why did the test fail? Let‘s print out some variables to see what’s going on. We have a local variable in this frame, reply, which should have been populated by the function call to remote_api->OnStatus:

    [zxdb] print reply
    {
      processes = {
        [0] = {
          process_koid = 4660
          process_name = "process-1"
          components = {}
          threads = {
            [0] = {
              id = {process = 4660, thread = 1}
              name = "test thread"
              state = kRunning
              blocked_reason = kNotBlocked
              stack_amount = kNone
              frames = {}
            }
          }
        }
        [1] = {
          process_koid = 22136
          process_name = "process-2"
          components = {}
          threads = {
            [0] = {
              id = {process = 22136, thread = 1}
              name = "test thread"
              state = kRunning
              blocked_reason = kNotBlocked
              stack_amount = kNone
              frames = {}
            }
            [1] = {
              id = {process = 22136, thread = 2}
              name = "test thread"
              state = kRunning
              blocked_reason = kNotBlocked
              stack_amount = kNone
              frames = {}
            }
          }
        }
      }
      limbo = {}
      breakpoints = {}
      filters = {}
    }
    

    Okay, so the reply variable has been filled in with some information, the expectation is that the size of the processes vector should be equal to 3. Let's just print that member variable of reply to get a clearer picture. We can also print the size method of that vector (general function calling support is not implemented yet):

    [zxdb] print reply.processes
    {
      [0] = {
        process_koid = 4660
        process_name = "process-1"
        components = {}
        threads = {
          [0] = {
            id = {process = 4660, thread = 1}
            name = "test thread"
            state = kRunning
            blocked_reason = kNotBlocked
            stack_amount = kNone
            frames = {}
          }
        }
      }
      [1] = {
        process_koid = 22136
        process_name = "process-2"
        components = {}
        threads = {
          [0] = {
            id = {process = 22136, thread = 1}
            name = "test thread"
            state = kRunning
            blocked_reason = kNotBlocked
            stack_amount = kNone
            frames = {}
          }
          [1] = {
            id = {process = 22136, thread = 2}
            name = "test thread"
            state = kRunning
            blocked_reason = kNotBlocked
            stack_amount = kNone
            frames = {}
          }
        }
      }
    }
    [zxdb] print reply.processes.size()
    2
    

    Aha, so the test expectation is wrong, we only injected 2 mock processes in our test, but expected there to be 3. The test simply needs to be updated to expect the size of the reply.processes vector to be 2 instead of 3. We can close the debugger now to finish up the tests and then fix our test:

    [zxdb] quit
    
    <...fx test output continues...>
    
    Failed tests: DebugAgentTests.OnGlobalStatus <-- Failed test that we debugged.
    175 out of 176 attempted tests passed, 2 tests skipped...
    fuchsia-pkg://fuchsia.com/debug_agent_unit_tests?hash=3f6d97801bb147034a344e3fe1bb69291a7b690b9d3d075246ddcba59397ac12#meta/debug_agent_unit_tests.cm completed with result: FAILED
    Tests failed.
    
    
    FAILED: fuchsia-pkg://fuchsia.com/debug_agent_unit_tests#meta/debug_agent_unit_tests.cm
    

    Now that the source of the test failure was found, we can fix the test:

    -ASSERT_EQ(reply.processes.size(), 3u)
    +ASSERT_EQ(reply.processes.size(), 2u)
    

    and run fx test again:

    =>  fx test --break-on-failure debug_agent_unit_tests
    
    You are using the new fx test, which is currently ready for general use 
    See details here: https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/scripts/fxtest/rewrite
    To go back to the old fx test, use `fx --enable=legacy_fxtest test`, and please file a bug under b/293917801.
    
    Default flags loaded from /usr/local/google/home/jruthe/.fxtestrc:
    []
    
    Logging all output to: /usr/local/google/home/jruthe/upstream/fuchsia/out/workbench_eng.x64/fxtest-2024-03-25T15:56:31.874893.log.json.gz
    Use the `--logpath` argument to specify a log location or `--no-log` to disable
    
    🛑 Debugger integration is currently experimental, follow https://fxbug.dev/319320287 for updates 🛑
    To show all output, specify the `-o/--output` flag.
    
    Found 913 total tests in //out/workbench_eng.x64/tests.json
    
    Plan to run 1 test
    
    Refreshing 1 target
    > fx build src/developer/debug/debug_agent:debug_agent_unit_tests host_x64/debug_agent_unit_tests
    Use --no-build to skip building
    
    Executing build. Status output suspended.
    ninja: Entering directory `/usr/local/google/home/jruthe/upstream/fuchsia/out/workbench_eng.x64'
    [22/22](0) STAMP obj/src/developer/debug/debug_agent/debug_agent_unit_tests.stamp
    
    Running 1 test
    
    Starting: fuchsia-pkg://fuchsia.com/debug_agent_unit_tests#meta/debug_agent_unit_tests.cm (NOT HERMETIC)
    Command: fx ffx test run --realm /core/testing:system-tests --max-severity-logs WARN --break-on-failure fuchsia-pkg://fuchsia.com/debug_agent_unit_tests?hash=399ff8d9871a6f0d53557c3d7c233cad645061016d44a7855dcea2c7b8af8101#meta/debug_agent_unit_tests.cm
    Deleting 1 files at /tmp/tmp8m56ht95: ffx_logs/ffx.log
    To keep these files, set --ffx-output-directory.
    
    PASSED: fuchsia-pkg://fuchsia.com/debug_agent_unit_tests#meta/debug_agent_unit_tests.cm
    
    Status: [duration: 16.9s] [tests: PASS: 1 FAIL: 0 SKIP: 0]
      Running 1 tests                      [=====================================================================================================]         100.0%
    =>
    

The debugger no longer appears, because we don't have any other test failures! Woohoo \o/