From c077fd8a67f9d832fa49459886d021884656aceb Mon Sep 17 00:00:00 2001 From: Efraim Eygin Date: Mon, 12 Oct 2026 14:29:05 +0000 Subject: [PATCH 1/3] test: cover prometheus_mp's scrape output like the single-process exporter's Port the single-process Prometheus scrape, histogram and stress cases that are still worth keeping to prometheus_mp, so this coverage survives when NIX-2062 removes the single-process exporter and its tests. Add a mock telemetry plugin whose exporter throws nixlTelemetryBindFailed, so nixlTelemetry::create's run-without-a-sink path stays covered once no real exporter throws it. Signed-off-by: Efraim Eygin --- test/gtest/meson.build | 2 + test/gtest/mocks/meson.build | 8 + .../mock_bind_failed_telemetry_plugin.cpp | 44 ++++ test/gtest/telemetry_mp_scrape_test.cpp | 199 ++++++++++++++++++ test/gtest/telemetry_mp_stress_test.cpp | 147 +++++++++++++ test/gtest/telemetry_test.cpp | 13 ++ 6 files changed, 413 insertions(+) create mode 100644 test/gtest/mocks/mock_bind_failed_telemetry_plugin.cpp create mode 100644 test/gtest/telemetry_mp_scrape_test.cpp create mode 100644 test/gtest/telemetry_mp_stress_test.cpp diff --git a/test/gtest/meson.build b/test/gtest/meson.build index 3f6c669355..2f3b23361f 100644 --- a/test/gtest/meson.build +++ b/test/gtest/meson.build @@ -154,6 +154,8 @@ if is_variable('nixl_telemetry_mp_exporter_dep') gtest_sources += 'telemetry_mp_exporter_test.cpp' gtest_sources += 'telemetry_mp_e2e_test.cpp' gtest_sources += 'telemetry_mp_core_test.cpp' + gtest_sources += 'telemetry_mp_scrape_test.cpp' + gtest_sources += 'telemetry_mp_stress_test.cpp' telemetry_mp_exporter_dep = [nixl_telemetry_mp_exporter_dep] else telemetry_mp_exporter_dep = [] diff --git a/test/gtest/mocks/meson.build b/test/gtest/mocks/meson.build index a7d301c7bb..247319dbe6 100644 --- a/test/gtest/mocks/meson.build +++ b/test/gtest/mocks/meson.build @@ -46,5 +46,13 @@ mock_stale_telemetry_plugin = shared_library('libtelemetry_exporter_mock_stale', install: true, install_dir: plugin_install_dir) +mock_bind_failed_telemetry_plugin = shared_library('libtelemetry_exporter_mock_bind_failed', + ['mock_bind_failed_telemetry_plugin.cpp'], + dependencies: [nixl_infra, nixl_common_dep], + include_directories: [nixl_inc_dirs, utils_inc_dirs, gtest_inc_dirs], + name_prefix: '', + install: true, + install_dir: plugin_install_dir) + source_root = meson.project_source_root() mocks_dep = declare_dependency(variables : {'path' : meson.current_source_dir().split(source_root + '/')[1]}) diff --git a/test/gtest/mocks/mock_bind_failed_telemetry_plugin.cpp b/test/gtest/mocks/mock_bind_failed_telemetry_plugin.cpp new file mode 100644 index 0000000000..176b62cbf3 --- /dev/null +++ b/test/gtest/mocks/mock_bind_failed_telemetry_plugin.cpp @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "telemetry/telemetry_exporter.h" +#include "telemetry/telemetry_plugin.h" + +namespace { + +class mockBindFailedExporter final : public nixlTelemetryExporter { +public: + explicit mockBindFailedExporter(const nixlTelemetryExporterInitParams &init_params) + : nixlTelemetryExporter(init_params) { + throw nixlTelemetryBindFailed("mock_bind_failed telemetry endpoint could not be bound"); + } + + nixl_status_t + exportEvent(const nixlTelemetryEvent &) override { + return NIXL_SUCCESS; + } +}; + +} // namespace + +extern "C" NIXL_TELEMETRY_PLUGIN_EXPORT nixlTelemetryPlugin * +nixl_telemetry_plugin_init() { + return nixlTelemetryPluginCreator::create( + NIXL_TELEMETRY_PLUGIN_API_VERSION, "mock_bind_failed", "0.0.1"); +} + +extern "C" NIXL_TELEMETRY_PLUGIN_EXPORT void +nixl_telemetry_plugin_fini() {} diff --git a/test/gtest/telemetry_mp_scrape_test.cpp b/test/gtest/telemetry_mp_scrape_test.cpp new file mode 100644 index 0000000000..b9a58f7192 --- /dev/null +++ b/test/gtest/telemetry_mp_scrape_test.cpp @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "mp_telemetry_fixture.h" +#include "plugin_manager.h" +#include "telemetry.h" +#include "telemetry/telemetry_exporter.h" +#include "telemetry_event.h" + +#include "common.h" +#include "common/hostname.h" + +#include "histogram_buckets.h" +#include "scrape_util.h" +#include "timeseries.h" + +#include + +#include + +#include +#include +#include +#include +#include + +namespace { + +using nixl::metrics_test::labelSet; +using nixl::metrics_test::scrapeMetrics; +using nixl::metrics_test::seriesId; + +constexpr auto TX_BYTES = nixl_telemetry_event_type_t::AGENT_TX_BYTES; +constexpr auto RX_BYTES = nixl_telemetry_event_type_t::AGENT_RX_BYTES; +constexpr auto XFER_TIME = nixl_telemetry_event_type_t::AGENT_XFER_TIME; + +constexpr char PLUGIN_PROBE_WARNING[] = "Plugin file does not exist"; + +[[nodiscard]] std::unique_ptr +createExporter(const std::string &agent_name) { + const gtest::LogIgnoreGuard lig(PLUGIN_PROBE_WARNING); + const auto handle = nixlPluginManager::getInstance().loadTelemetryPlugin("prometheus_mp"); + return handle == nullptr ? nullptr : handle->createExporter(initParams(agent_name)); +} + +TEST_F(MpExporterTest, ScrapeEmitsExactlyTheDescriptorSeries) { + const auto exporter = createExporter("mp_parity_agent"); + ASSERT_NE(exporter, nullptr); + + std::set expected{"agent_errors_total"}; + for (const auto type : telemetry_metric_event_types) { + const auto descriptor = nixlEnumStrings::telemetryMetricDescriptor(type); + for (const char *name : {descriptor.counterName, descriptor.gaugeName}) { + if (name != nullptr) { + expected.insert(name); + } + } + if (descriptor.histogramName != nullptr) { + for (const char *suffix : {"_bucket", "_sum", "_count"}) { + expected.insert(std::string(descriptor.histogramName) + suffix); + } + } + } + + const auto metrics = scrapeMetrics(port_); + std::set actual; + for (const auto &id : std::views::keys(metrics.series())) { + if (id.labels.contains("agent_name")) { + actual.insert(id.name); + } + } + EXPECT_EQ(actual, expected); +} + +TEST_F(MpExporterTest, AgentMetricsAppearInScrape) { + env_.unsetVar("LOCAL_RANK"); + const std::string agent_name = "mp_scrape_agent"; + const auto exporter = createExporter(agent_name); + ASSERT_NE(exporter, nullptr); + + const std::string host = nixl::getHostname().value_or("unknown"); + const auto tx_series = [&](const std::string &agent) { + return seriesId{"agent_tx_bytes_total", {{"agent_name", agent}, {"hostname", host}}}; + }; + EXPECT_TRUE(scrapeMetrics(port_).series().contains(tx_series(agent_name))) + << "a lone agent must carry only agent_name and hostname, no pid or agent_instance"; + + const std::string peer_name = "mp_scrape_agent_peer"; + { + const auto peer = createExporter(peer_name); + ASSERT_NE(peer, nullptr); + const auto both = scrapeMetrics(port_); + EXPECT_TRUE(both.series().contains(tx_series(agent_name))); + EXPECT_TRUE(both.series().contains(tx_series(peer_name))); + } + EXPECT_TRUE(scrapeMetrics(port_).series().contains(tx_series(agent_name))) + << "the first agent's series must survive the peer's teardown"; +} + +TEST_F(MpExporterTest, ByteCounterSumsWhileLastGaugeTracksFinalOp) { + const std::string agent_name = "mp_last_gauge_agent"; + const auto exporter = createExporter(agent_name); + ASSERT_NE(exporter, nullptr); + for (const uint64_t bytes : {1000, 2000, 3500}) { + exporter->exportEvent({TX_BYTES, bytes}); + } + for (const uint64_t bytes : {500, 1500}) { + exporter->exportEvent({RX_BYTES, bytes}); + } + + const auto metrics = scrapeMetrics(port_); + const labelSet agent{{"agent_name", agent_name}}; + EXPECT_EQ(metrics.latestValue("agent_tx_bytes_total", agent), 6500.0); + EXPECT_EQ(metrics.latestValue("agent_tx_last_bytes", agent), 3500.0); + EXPECT_EQ(metrics.latestValue("agent_rx_bytes_total", agent), 2000.0); + EXPECT_EQ(metrics.latestValue("agent_rx_last_bytes", agent), 1500.0); +} + +TEST_F(MpExporterTest, ErrorCountersUseBoundedStatusLabel) { + const std::string agent_name = "mp_error_counter_agent"; + const auto exporter = createExporter(agent_name); + ASSERT_NE(exporter, nullptr); + exporter->exportEvent({nixl_telemetry_event_type_t::AGENT_ERR_INVALID_PARAM, 1}); + exporter->exportEvent({nixl_telemetry_event_type_t::AGENT_ERR_INVALID_PARAM, 1}); + exporter->exportEvent({nixl_telemetry_event_type_t::AGENT_ERR_BACKEND, 1}); + + const auto metrics = scrapeMetrics(port_); + EXPECT_EQ(metrics.latestValue("agent_errors_total", + {{"agent_name", agent_name}, {"status", "invalid_param"}}), + 2.0); + EXPECT_EQ(metrics.latestValue("agent_errors_total", + {{"agent_name", agent_name}, {"status", "backend"}}), + 1.0); +} + +TEST_F(MpExporterTest, XferTimeHistogramRecordsObservations) { + const std::string agent_name = "mp_histogram_agent"; + const auto exporter = createExporter(agent_name); + ASSERT_NE(exporter, nullptr); + for (const uint64_t us : {5, 30, 200, 700}) { + exporter->exportEvent({XFER_TIME, us}); + } + + const auto metrics = scrapeMetrics(port_); + const auto bucket = [&](const std::string &le) { + return metrics.latestValue("agent_xfer_time_us_bucket", + {{"agent_name", agent_name}, {"le", le}}); + }; + EXPECT_EQ(metrics.latestValue("agent_xfer_time_us_count", {{"agent_name", agent_name}}), 4.0); + EXPECT_EQ(metrics.latestValue("agent_xfer_time_us_sum", {{"agent_name", agent_name}}), 935.0); + EXPECT_EQ(bucket("10"), 1.0); + EXPECT_EQ(bucket("100"), 2.0); + EXPECT_EQ(bucket("250"), 3.0); + EXPECT_EQ(bucket("1000"), 4.0); +} + +TEST_F(MpExporterTest, HistogramBucketsEnvOverride) { + env_.addVar(nixl::telemetry::histogramBucketsUsVar, "100,200,300"); + const auto exporter = createExporter("mp_histogram_env_agent"); + ASSERT_NE(exporter, nullptr); + + const auto metrics = scrapeMetrics(port_); + std::set bounds; + for (const auto &id : std::views::keys(metrics.series())) { + if (id.name == "agent_xfer_time_us_bucket") { + bounds.insert(id.labels.at("le")); + } + } + EXPECT_EQ(bounds, (std::set{"100", "200", "300", "+Inf"})); +} + +TEST_F(MpExporterTest, BindCollisionCreateIsNonFatalWarn) { + prometheus::Exposer blocker("127.0.0.1:" + std::to_string(port_)); + env_.addVar(telemetryExporterVar, "prometheus_mp"); + + const gtest::LogIgnoreGuard probe(PLUGIN_PROBE_WARNING); + const gtest::LogIgnoreGuard lig("is held by a process outside this run"); + std::unique_ptr telemetry; + EXPECT_NO_THROW(telemetry = nixlTelemetry::create("mp_bind_collision_create_agent")); + EXPECT_NE(telemetry, nullptr); + EXPECT_EQ(lig.getIgnoredCount(), 1u); + EXPECT_FALSE(singleStoreFile().empty()); +} + +} // namespace diff --git a/test/gtest/telemetry_mp_stress_test.cpp b/test/gtest/telemetry_mp_stress_test.cpp new file mode 100644 index 0000000000..97e7a4b3a1 --- /dev/null +++ b/test/gtest/telemetry_mp_stress_test.cpp @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "mp_telemetry_fixture.h" +#include "telemetry_core_scrape.h" + +#include "common.h" +#include "telemetry.h" +#include "telemetry_event.h" + +#include "scrape_util.h" +#include "timeseries.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using nixl::metrics_test::labelSet; +using nixl::metrics_test::timeSeries; +using testing::AnyOf; +using testing::Optional; + +constexpr char PLUGIN_PROBE_WARNING[] = "Plugin file does not exist"; + +void +runConcurrently(uint64_t iterations, const std::vector> &ops) { + std::latch start_gate(1); + std::vector threads; + for (const auto &op : ops) { + threads.emplace_back([&start_gate, &op, iterations] { + start_gate.wait(); + for (uint64_t i = 0; i < iterations; ++i) { + op(); + } + }); + } + start_gate.count_down(); + for (auto &thread : threads) { + thread.join(); + } +} + +TEST_F(MpExporterTest, ConcurrentAddXferStatsOverflowConservation) { + constexpr uint64_t kThreads = 4; + constexpr uint64_t kCallsPerThread = 5000; + constexpr uint64_t kEventsPerCall = 4; + + const gtest::LogIgnoreGuard lig(PLUGIN_PROBE_WARNING); + const auto scrape = gtest::scrapeCoreOverflow( + {.port = port_, + .exporter = "prometheus_mp", + .agent = "mp_concurrent_xfer_overflow_agent", + .accepted_metric = "agent_tx_requests_num_total", + .accepted_event_weight = kEventsPerCall, + .expected_total_events = kThreads * kCallsPerThread * kEventsPerCall, + .flush_interval = std::chrono::milliseconds(50), + .settle_timeout = std::chrono::seconds(20)}, + [](nixlTelemetry &telemetry) { + runConcurrently( + kCallsPerThread, std::vector>(kThreads, [&telemetry] { + telemetry.addXferStats( + std::chrono::microseconds(10), true, 2000, std::chrono::microseconds(1)); + })); + }); + + ASSERT_TRUE(scrape.ok) << "accepted*4 + dropped must equal the produced events"; + EXPECT_GT(scrape.dropped, 0.0); + EXPECT_EQ(std::fmod(scrape.dropped, static_cast(kEventsPerCall)), 0.0); +} + +TEST_F(MpExporterTest, ConcurrentMixedWorkloadAggregation) { + constexpr uint64_t kIters = 500; + constexpr uint64_t kTxA = 1000; + constexpr uint64_t kTxB = 3000; + constexpr uint64_t kRxA = 500; + constexpr uint64_t kRxB = 1500; + constexpr uint64_t kMem = 4096; + const std::string agent_name = "mp_concurrent_mixed_agent"; + + env_.addVar(TELEMETRY_BUFFER_SIZE_VAR, "65536"); + env_.addVar(TELEMETRY_RUN_INTERVAL_VAR, "5"); + const gtest::LogIgnoreGuard lig(PLUGIN_PROBE_WARNING); + nixlTelemetry telemetry(agent_name, "prometheus_mp"); + runConcurrently(kIters, + {[&] { telemetry.updateTxBytes(kTxA); }, + [&] { telemetry.updateTxBytes(kTxB); }, + [&] { telemetry.updateRxBytes(kRxA); }, + [&] { telemetry.updateRxBytes(kRxB); }, + [&] { telemetry.updateTxRequestsNum(1); }, + [&] { telemetry.updateMemoryRegistered(kMem); }, + [&] { telemetry.updateErrorCount(NIXL_ERR_BACKEND); }, + [&] { telemetry.updateErrorCount(NIXL_ERR_INVALID_PARAM); }}); + + const labelSet agent{{"agent_name", agent_name}}; + const std::vector> totals{ + {"agent_tx_bytes_total", agent, kIters * (kTxA + kTxB)}, + {"agent_rx_bytes_total", agent, kIters * (kRxA + kRxB)}, + {"agent_tx_requests_num_total", agent, kIters}, + {"agent_memory_registered_total", agent, kIters * kMem}, + {"agent_errors_total", {{"agent_name", agent_name}, {"status", "backend"}}, kIters}, + {"agent_errors_total", {{"agent_name", agent_name}, {"status", "invalid_param"}}, kIters}, + {"agent_telemetry_events_dropped_total", agent, 0}}; + const auto metrics = nixl::metrics_test::scrapeUntil( + port_, std::chrono::seconds(30), [&](const timeSeries &scrape) { + return std::ranges::all_of(totals, [&](const auto &total) { + return scrape.latestValue(std::get<0>(total), std::get<1>(total)) == + std::get<2>(total); + }); + }); + for (const auto &[name, labels, value] : totals) { + EXPECT_EQ(metrics.latestValue(name, labels), value) << name; + } + + EXPECT_THAT(metrics.latestValue("agent_tx_last_bytes", agent), + AnyOf(Optional(kTxA), Optional(kTxB))); + EXPECT_THAT(metrics.latestValue("agent_rx_last_bytes", agent), + AnyOf(Optional(kRxA), Optional(kRxB))); + EXPECT_THAT(metrics.latestValue("agent_memory_registered_last_bytes", agent), Optional(kMem)); +} + +} // namespace diff --git a/test/gtest/telemetry_test.cpp b/test/gtest/telemetry_test.cpp index 7b8380587e..fb31ddb04c 100644 --- a/test/gtest/telemetry_test.cpp +++ b/test/gtest/telemetry_test.cpp @@ -144,6 +144,19 @@ TEST_F(telemetryTest, NonexistentExporterThrows) { { nixlTelemetry telemetry(testFile_, "nixl_nonexistent_exporter"); }, std::runtime_error); } +TEST_F(telemetryTest, ExporterBindFailureRunsWithoutSink) { + envHelper_.addVar(TELEMETRY_EXPORTER_VAR, "mock_bind_failed"); + const gtest::LogIgnoreGuard ignore_missing_plugin("Plugin file does not exist"); + const gtest::LogIgnoreGuard ignore_sinkless("will run without a telemetry sink"); + + std::unique_ptr telemetry; + EXPECT_NO_THROW(telemetry = nixlTelemetry::create(testFile_)); + EXPECT_EQ(telemetry, nullptr); + EXPECT_EQ(ignore_sinkless.getIgnoredCount(), 1u); + + envHelper_.popVar(); +} + TEST_F(telemetryTest, CreateFallsBackToNopWithoutSink) { envHelper_.popVar(); // drop NIXL_TELEMETRY_DIR set by SetUp -> no sink // Neutralize any inherited NIXL_TELEMETRY_EXPORTER (empty is ignored) so the From d0f58763b45bf0d765482e342c4f993d32e02a57 Mon Sep 17 00:00:00 2001 From: Efraim Eygin Date: Mon, 12 Oct 2026 15:36:40 +0000 Subject: [PATCH 2/3] test: tighten prometheus_mp's lone-agent label check Clear NIXL_TELEMETRY_RANK_ENV as well as LOCAL_RANK, since either can add a local_rank label, and reject pid and agent_instance on every series of the lone agent's scrape rather than only on the series looked up. Signed-off-by: Efraim Eygin --- test/gtest/telemetry_mp_scrape_test.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/gtest/telemetry_mp_scrape_test.cpp b/test/gtest/telemetry_mp_scrape_test.cpp index b9a58f7192..7425ca2192 100644 --- a/test/gtest/telemetry_mp_scrape_test.cpp +++ b/test/gtest/telemetry_mp_scrape_test.cpp @@ -87,6 +87,7 @@ TEST_F(MpExporterTest, ScrapeEmitsExactlyTheDescriptorSeries) { } TEST_F(MpExporterTest, AgentMetricsAppearInScrape) { + env_.unsetVar("NIXL_TELEMETRY_RANK_ENV"); env_.unsetVar("LOCAL_RANK"); const std::string agent_name = "mp_scrape_agent"; const auto exporter = createExporter(agent_name); @@ -96,8 +97,12 @@ TEST_F(MpExporterTest, AgentMetricsAppearInScrape) { const auto tx_series = [&](const std::string &agent) { return seriesId{"agent_tx_bytes_total", {{"agent_name", agent}, {"hostname", host}}}; }; - EXPECT_TRUE(scrapeMetrics(port_).series().contains(tx_series(agent_name))) - << "a lone agent must carry only agent_name and hostname, no pid or agent_instance"; + const auto lone = scrapeMetrics(port_); + EXPECT_TRUE(lone.series().contains(tx_series(agent_name))) + << "a lone agent must carry only agent_name and hostname"; + for (const auto &id : std::views::keys(lone.series())) { + EXPECT_FALSE(id.labels.contains("pid") || id.labels.contains("agent_instance")) << id.name; + } const std::string peer_name = "mp_scrape_agent_peer"; { From 5f8a2c72d20174452428438ac5e27b4d9ef62d24 Mon Sep 17 00:00:00 2001 From: Efraim Eygin Date: Mon, 12 Oct 2026 16:10:58 +0000 Subject: [PATCH 3/3] test: update the MpExporterTest fixture comments Four test files now name MpExporterTest, and the scrape and stress tests load the plugin by name as well as LoadsThroughPluginManager does. Signed-off-by: Efraim Eygin --- test/gtest/mp_telemetry_fixture.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/gtest/mp_telemetry_fixture.h b/test/gtest/mp_telemetry_fixture.h index a731b86bb4..01e8d5cee3 100644 --- a/test/gtest/mp_telemetry_fixture.h +++ b/test/gtest/mp_telemetry_fixture.h @@ -132,13 +132,13 @@ class mpTempDirTest : public ::testing::Test { }; // The multi-process exporter pointed at that directory and a port of its own. -// Driven both directly (telemetry_mp_exporter_test.cpp) and through the core -// (telemetry_mp_core_test.cpp): both files name this single fixture class, so -// gtest groups them into one suite and runs SetUpTestSuite once per iteration. +// Driven both directly and through the core, from several test files that all +// name this single fixture class, so gtest groups them into one suite and runs +// SetUpTestSuite once per iteration. class MpExporterTest : public mpTempDirTest { protected: - // A build tree has no /plugins, so LoadsThroughPluginManager - // finds the plugin only if the build path is registered. Registered once per + // A build tree has no /plugins, so a test that loads the plugin + // by name finds it only if the build path is registered. Registered once per // process (--gtest_repeat re-enters this hook), and only when it exists: // re-registering, or registering a missing directory, logs a warning/error // that the gtest main counts as a failure. When it is absent (a binary run