From 8db9ae4aaebf6e6fdd7a43d570a07863d1ec6869 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Dec 2025 12:16:19 -0800 Subject: [PATCH 1/4] [fm] add a `SitrepBuilder`, to help with building sitreps This is a surprise tool that will help us later! --- Cargo.lock | 22 ++++ Cargo.toml | 1 + nexus/fm/Cargo.toml | 28 +++++ nexus/fm/src/builder.rs | 98 ++++++++++++++++ nexus/fm/src/builder/case.rs | 221 +++++++++++++++++++++++++++++++++++ nexus/fm/src/builder/rng.rs | 90 ++++++++++++++ nexus/fm/src/lib.rs | 8 ++ 7 files changed, 468 insertions(+) create mode 100644 nexus/fm/Cargo.toml create mode 100644 nexus/fm/src/builder.rs create mode 100644 nexus/fm/src/builder/case.rs create mode 100644 nexus/fm/src/builder/rng.rs create mode 100644 nexus/fm/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 711e3e1f48f..844844898c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6900,6 +6900,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "nexus-fm" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "ereport-types", + "iddqd", + "nexus-reconfigurator-planning", + "nexus-types", + "omicron-test-utils", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "serde", + "serde_json", + "slog", + "slog-error-chain", + "typed-rng", +] + [[package]] name = "nexus-internal-api" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0ef5248bac9..9f7af040725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ members = [ "nexus/db-schema", "nexus/defaults", "nexus/external-api", + "nexus/fm", "nexus/internal-api", "nexus/inventory", "nexus/lockstep-api", diff --git a/nexus/fm/Cargo.toml b/nexus/fm/Cargo.toml new file mode 100644 index 00000000000..37fd0c13684 --- /dev/null +++ b/nexus/fm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nexus-fm" +version = "0.1.0" +edition = "2021" + +[lints] +workspace = true + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +iddqd.workspace = true +nexus-types.workspace = true +omicron-uuid-kinds.workspace = true +rand.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +slog.workspace = true +slog-error-chain.workspace = true +typed-rng.workspace = true + +omicron-workspace-hack.workspace = true + +[dev-dependencies] +omicron-test-utils.workspace = true +nexus-reconfigurator-planning.workspace = true +ereport-types.workspace = true diff --git a/nexus/fm/src/builder.rs b/nexus/fm/src/builder.rs new file mode 100644 index 00000000000..24a472de3b9 --- /dev/null +++ b/nexus/fm/src/builder.rs @@ -0,0 +1,98 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Sitrep builder + +use nexus_types::fm; +use nexus_types::inventory; +use omicron_uuid_kinds::OmicronZoneUuid; +use omicron_uuid_kinds::SitrepUuid; +use slog::Logger; + +mod case; +pub use case::{AllCases, CaseBuilder}; +pub(crate) mod rng; +pub use rng::SitrepBuilderRng; + +#[derive(Debug)] +pub struct SitrepBuilder<'a> { + pub log: Logger, + pub inventory: &'a inventory::Collection, + pub parent_sitrep: Option<&'a fm::Sitrep>, + pub sitrep_id: SitrepUuid, + pub cases: case::AllCases, + comment: String, +} + +impl<'a> SitrepBuilder<'a> { + pub fn new( + log: &Logger, + inventory: &'a inventory::Collection, + parent_sitrep: Option<&'a fm::Sitrep>, + ) -> Self { + Self::new_with_rng( + log, + inventory, + parent_sitrep, + SitrepBuilderRng::from_entropy(), + ) + } + + pub fn new_with_rng( + log: &Logger, + inventory: &'a inventory::Collection, + parent_sitrep: Option<&'a fm::Sitrep>, + mut rng: SitrepBuilderRng, + ) -> Self { + // TODO(eliza): should the RNG also be seeded with the parent sitrep + // UUID and/or the Omicron zone UUID? Hmm. + let sitrep_id = rng.sitrep_id(); + let log = log.new(slog::o!( + "sitrep_id" => format!("{sitrep_id:?}"), + "parent_sitrep_id" => format!("{:?}", parent_sitrep.as_ref().map(|s| s.id())), + "inv_collection_id" => format!("{:?}", inventory.id), + )); + + let cases = + case::AllCases::new(log.clone(), sitrep_id, parent_sitrep, rng); + + slog::info!( + &log, + "preparing sitrep {sitrep_id:?}"; + "existing_open_cases" => cases.cases.len(), + ); + + SitrepBuilder { + log, + sitrep_id, + inventory, + parent_sitrep, + comment: String::new(), + cases, + } + } + + pub fn build( + self, + creator_id: OmicronZoneUuid, + time_created: chrono::DateTime, + ) -> fm::Sitrep { + fm::Sitrep { + metadata: fm::SitrepMetadata { + id: self.sitrep_id, + parent_sitrep_id: self.parent_sitrep.map(|s| s.metadata.id), + inv_collection_id: self.inventory.id, + creator_id, + comment: self.comment, + time_created, + }, + cases: self + .cases + .cases + .into_iter() + .map(|builder| fm::Case::from(builder)) + .collect(), + } + } +} diff --git a/nexus/fm/src/builder/case.rs b/nexus/fm/src/builder/case.rs new file mode 100644 index 00000000000..8385fcfa80d --- /dev/null +++ b/nexus/fm/src/builder/case.rs @@ -0,0 +1,221 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use super::rng; +use anyhow::Context; +use iddqd::id_ord_map::{self, IdOrdMap}; +use nexus_types::alert::AlertClass; +use nexus_types::fm; +use omicron_uuid_kinds::CaseUuid; +use omicron_uuid_kinds::SitrepUuid; +use std::sync::Arc; + +#[derive(Debug)] +pub struct CaseBuilder { + pub log: slog::Logger, + pub case: fm::Case, + pub sitrep_id: SitrepUuid, + rng: rng::CaseBuilderRng, +} + +#[derive(Debug)] +pub struct AllCases { + log: slog::Logger, + sitrep_id: SitrepUuid, + pub cases: IdOrdMap, + rng: rng::SitrepBuilderRng, +} + +impl AllCases { + pub(super) fn new( + log: slog::Logger, + sitrep_id: SitrepUuid, + parent_sitrep: Option<&fm::Sitrep>, + mut rng: rng::SitrepBuilderRng, + ) -> Self { + // Copy forward any open cases from the parent sitrep. + // If a case was closed in the parent sitrep, skip it. + let cases: IdOrdMap<_> = parent_sitrep + .iter() + .flat_map(|s| s.open_cases()) + .map(|case| { + let rng = rng::CaseBuilderRng::new(case.id, &mut rng); + CaseBuilder::new(&log, sitrep_id, case.clone(), rng) + }) + .collect(); + + Self { log, sitrep_id, cases, rng } + } + + pub fn open_case( + &mut self, + de: fm::DiagnosisEngineKind, + ) -> anyhow::Result> { + let (id, case_rng) = self.rng.next_case(); + let sitrep_id = self.sitrep_id; + let case = match self.cases.entry(&id) { + iddqd::id_ord_map::Entry::Occupied(_) => { + panic!("generated a colliding UUID!") + } + iddqd::id_ord_map::Entry::Vacant(entry) => { + let case = fm::Case { + id, + created_sitrep_id: self.sitrep_id, + closed_sitrep_id: None, + de, + comment: String::new(), + ereports: Default::default(), + alerts_requested: Default::default(), + }; + entry.insert(CaseBuilder::new( + &self.log, sitrep_id, case, case_rng, + )) + } + }; + + slog::info!( + self.log, + "opened case {id:?}"; + "case_id" => ?id, + "de" => %de + ); + + Ok(case) + } + + pub fn case(&self, id: &CaseUuid) -> Option<&CaseBuilder> { + self.cases.get(id) + } + + pub fn case_mut( + &mut self, + id: &CaseUuid, + ) -> Option> { + self.cases.get_mut(id) + } +} + +impl CaseBuilder { + fn new( + log: &slog::Logger, + sitrep_id: SitrepUuid, + case: fm::Case, + rng: rng::CaseBuilderRng, + ) -> Self { + let log = log.new(slog::o!( + "case_id" => case.id.to_string(), + "de" => case.de.to_string(), + "created_sitrep_id" => case.created_sitrep_id.to_string(), + )); + Self { log, case, sitrep_id, rng } + } + + pub fn request_alert( + &mut self, + class: AlertClass, + alert: &impl serde::Serialize, + ) -> anyhow::Result<()> { + let id = self.rng.next_alert(); + let req = fm::case::AlertRequest { + id, + class, + requested_sitrep_id: self.sitrep_id, + payload: serde_json::to_value(&alert).with_context(|| { + format!("failed to serialize payload for {class:?} alert") + })?, + }; + self.case.alerts_requested.insert_unique(req).map_err(|_| { + anyhow::anyhow!("an alert with ID {id:?} already exists") + })?; + + slog::info!( + &self.log, + "requested an alert"; + "alert_id" => %id, + "alert_class" => ?class, + ); + + Ok(()) + } + + pub fn close(&mut self) { + self.case.closed_sitrep_id = Some(self.sitrep_id); + + slog::info!(&self.log, "case closed"); + } + + pub fn add_ereport( + &mut self, + report: &Arc, + comment: impl ToString, + ) { + let assignment_id = self.rng.next_case_ereport(); + match self.case.ereports.insert_unique(fm::case::CaseEreport { + id: assignment_id, + ereport: report.clone(), + assigned_sitrep_id: self.sitrep_id, + comment: comment.to_string(), + }) { + Ok(_) => { + slog::info!( + self.log, + "assigned ereport {} to case", report.id(); + "ereport_id" => %report.id(), + "ereport_class" => ?report.class, + "assignment_id" => %assignment_id, + ); + } + Err(_) => { + slog::warn!( + self.log, + "ereport {} already assigned to case", report.id(); + "ereport_id" => %report.id(), + "ereport_class" => ?report.class, + ); + } + } + } + + /// Returns an iterator over all ereports that were assigned to this case in + /// the current sitrep. + pub fn new_ereports( + &self, + ) -> impl Iterator> + '_ { + self.ereports.iter().filter_map(|ereport| { + if ereport.assigned_sitrep_id == self.sitrep_id { + Some(&ereport.ereport) + } else { + None + } + }) + } +} + +impl From for fm::Case { + fn from(CaseBuilder { case, .. }: CaseBuilder) -> Self { + case + } +} + +impl core::ops::Deref for CaseBuilder { + type Target = fm::Case; + fn deref(&self) -> &Self::Target { + &self.case + } +} + +impl core::ops::DerefMut for CaseBuilder { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.case + } +} + +impl iddqd::IdOrdItem for CaseBuilder { + type Key<'a> = &'a CaseUuid; + fn key(&self) -> Self::Key<'_> { + &self.case.id + } + + iddqd::id_upcast!(); +} diff --git a/nexus/fm/src/builder/rng.rs b/nexus/fm/src/builder/rng.rs new file mode 100644 index 00000000000..ba35b70586a --- /dev/null +++ b/nexus/fm/src/builder/rng.rs @@ -0,0 +1,90 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! RNGs for sitrep generation to allow reproduceable UUID generation +//! (particularly for tests). +//! +//! This is similar to the `nexus_reconfigurator_planning::planner::rng` +//! module. + +use omicron_uuid_kinds::AlertKind; +use omicron_uuid_kinds::AlertUuid; +use omicron_uuid_kinds::CaseEreportKind; +use omicron_uuid_kinds::CaseEreportUuid; +use omicron_uuid_kinds::CaseKind; +use omicron_uuid_kinds::CaseUuid; +use omicron_uuid_kinds::SitrepUuid; +use rand::SeedableRng as _; +use rand::rngs::StdRng; +use std::hash::Hash; +use typed_rng::TypedUuidRng; + +#[derive(Clone, Debug)] +pub struct SitrepBuilderRng { + parent: StdRng, + case_rng: TypedUuidRng, +} + +impl SitrepBuilderRng { + pub fn from_entropy() -> Self { + Self::new_from_parent(StdRng::from_os_rng()) + } + + pub fn from_seed(seed: H) -> Self { + // Important to add some more bytes here, so that builders with the + // same seed but different purposes don't end up with the same UUIDs. + const SEED_EXTRA: &str = "sitrep-builder"; + Self::new_from_parent(typed_rng::from_seed(seed, SEED_EXTRA)) + } + + pub fn new_from_parent(mut parent: StdRng) -> Self { + let case_rng = TypedUuidRng::from_parent_rng(&mut parent, "case"); + + Self { parent, case_rng } + } + + pub(super) fn sitrep_id(&mut self) -> SitrepUuid { + // we only need a single sitrep UUID, so no sense storing a whole RNG + // for it in the builder RNGs... + TypedUuidRng::from_parent_rng(&mut self.parent, "sitrep").next() + } + + pub(super) fn next_case(&mut self) -> (CaseUuid, CaseBuilderRng) { + let case_id = self.case_rng.next(); + let rng = CaseBuilderRng::new(case_id, self); + (case_id, rng) + } +} + +#[derive(Clone, Debug)] +pub(super) struct CaseBuilderRng { + ereport_assignment_rng: TypedUuidRng, + alert_rng: TypedUuidRng, +} + +impl CaseBuilderRng { + pub(super) fn new( + case_id: CaseUuid, + sitrep: &mut SitrepBuilderRng, + ) -> Self { + let alert_rng = TypedUuidRng::from_parent_rng( + &mut sitrep.parent, + (case_id, "alert"), + ); + + let ereport_assignment_rng = TypedUuidRng::from_parent_rng( + &mut sitrep.parent, + (case_id, "case-ereport"), + ); + Self { alert_rng, ereport_assignment_rng } + } + + pub(super) fn next_alert(&mut self) -> AlertUuid { + self.alert_rng.next() + } + + pub(super) fn next_case_ereport(&mut self) -> CaseEreportUuid { + self.ereport_assignment_rng.next() + } +} diff --git a/nexus/fm/src/lib.rs b/nexus/fm/src/lib.rs new file mode 100644 index 00000000000..fd33f3831c7 --- /dev/null +++ b/nexus/fm/src/lib.rs @@ -0,0 +1,8 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Fault management application logic. + +pub mod builder; +pub use builder::{CaseBuilder, SitrepBuilder}; From 4ec968dda65d1a8e3fb7a16f2c9f74ed551e0c2f Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Dec 2025 12:36:20 -0800 Subject: [PATCH 2/4] [fm] sitrep test utils --- Cargo.lock | 2 +- Cargo.toml | 2 + nexus/Cargo.toml | 1 + nexus/fm/Cargo.toml | 13 ++- nexus/fm/src/lib.rs | 3 + nexus/fm/src/test_util.rs | 166 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 nexus/fm/src/test_util.rs diff --git a/Cargo.lock b/Cargo.lock index 844844898c1..335be024c89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6914,7 +6914,6 @@ dependencies = [ "omicron-uuid-kinds", "omicron-workspace-hack", "rand 0.9.2", - "schemars 0.8.22", "serde", "serde_json", "slog", @@ -8344,6 +8343,7 @@ dependencies = [ "nexus-db-schema", "nexus-defaults", "nexus-external-api", + "nexus-fm", "nexus-internal-api", "nexus-inventory", "nexus-lockstep-api", diff --git a/Cargo.toml b/Cargo.toml index 9f7af040725..d8dffae79cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -250,6 +250,7 @@ default-members = [ "nexus/db-schema", "nexus/defaults", "nexus/external-api", + "nexus/fm", "nexus/internal-api", "nexus/inventory", "nexus/lockstep-api", @@ -557,6 +558,7 @@ nexus-db-queries = { path = "nexus/db-queries" } nexus-db-schema = { path = "nexus/db-schema" } nexus-defaults = { path = "nexus/defaults" } nexus-external-api = { path = "nexus/external-api" } +nexus-fm = { path = "nexus/fm" } nexus-inventory = { path = "nexus/inventory" } nexus-internal-api = { path = "nexus/internal-api" } nexus-lockstep-api = { path = "nexus/lockstep-api" } diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 61616e6641d..dd20da01d2e 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -165,6 +165,7 @@ hubtools.workspace = true hyper-rustls.workspace = true nexus-client.workspace = true nexus-db-queries = { workspace = true, features = ["testing"] } +nexus-fm = { workspace = true, features = ["testing"] } nexus-lockstep-client.workspace = true nexus-test-utils.workspace = true nexus-test-utils-macros.workspace = true diff --git a/nexus/fm/Cargo.toml b/nexus/fm/Cargo.toml index 37fd0c13684..fd1db332803 100644 --- a/nexus/fm/Cargo.toml +++ b/nexus/fm/Cargo.toml @@ -6,6 +6,13 @@ edition = "2021" [lints] workspace = true +[features] +testing = [ + "omicron-test-utils", + "nexus-reconfigurator-planning", + "ereport-types", +] + [dependencies] anyhow.workspace = true chrono.workspace = true @@ -13,13 +20,17 @@ iddqd.workspace = true nexus-types.workspace = true omicron-uuid-kinds.workspace = true rand.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true slog.workspace = true slog-error-chain.workspace = true typed-rng.workspace = true +# deps for test utils +omicron-test-utils = { workspace = true, optional = true } +nexus-reconfigurator-planning = { workspace = true, optional = true } +ereport-types = { workspace = true, optional = true } + omicron-workspace-hack.workspace = true [dev-dependencies] diff --git a/nexus/fm/src/lib.rs b/nexus/fm/src/lib.rs index fd33f3831c7..069c2eb4dde 100644 --- a/nexus/fm/src/lib.rs +++ b/nexus/fm/src/lib.rs @@ -6,3 +6,6 @@ pub mod builder; pub use builder::{CaseBuilder, SitrepBuilder}; + +#[cfg(any(test, feature = "testing"))] +pub mod test_util; diff --git a/nexus/fm/src/test_util.rs b/nexus/fm/src/test_util.rs new file mode 100644 index 00000000000..e0bae4b29f3 --- /dev/null +++ b/nexus/fm/src/test_util.rs @@ -0,0 +1,166 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::builder::SitrepBuilderRng; +use chrono::Utc; +use nexus_reconfigurator_planning::example; +use nexus_types::fm::ereport::{ + Ena, Ereport, EreportData, EreportId, Reporter, +}; +use omicron_test_utils::dev; +use omicron_uuid_kinds::EreporterRestartKind; +use omicron_uuid_kinds::EreporterRestartUuid; +use omicron_uuid_kinds::OmicronZoneKind; +use omicron_uuid_kinds::OmicronZoneUuid; +use rand::rngs::StdRng; +use typed_rng::TypedUuidRng; + +pub struct FmTest { + pub reporters: SimReporters, + pub sitrep_rng: SitrepBuilderRng, + pub system_builder: example::ExampleSystemBuilder, +} + +impl FmTest { + pub fn new_with_logxtx(test_name: &str) -> (Self, dev::LogContext) { + let logctx = dev::test_setup_log(test_name); + (Self::new(test_name, logctx.log), logxtx) + } + + pub fn new(test_name: &str, log: &slog::Logger) -> Self { + let example_system_builder = + example::ExampleSystemBuilder::new(&log, test_name); + let reporters = SimReporters::new( + test_name, + log.new(slog::o!("component" => "sim-reporters")), + ); + Self { + reporters, + sitrep_rng: SitrepBuilderRng::from_seed(test_name), + system_builder: example_system_builder, + } + } +} + +pub struct SimReporters { + log: slog::Logger, + parent: StdRng, + collector_id_rng: TypedUuidRng, +} + +impl SimReporters { + fn new(test_name: &str, log: slog::Logger) -> Self { + let mut parent = typed_rng::from_seed(test_name, "sim-reporters"); + // TODO(eliza): would be more realistic to pick something from the + // example system's omicron zones, but these UUIDs are only used for + // debugging purposes... + let collector_id_rng = + TypedUuidRng::from_parent_rng(&mut parent, "collector-ids"); + Self { parent, collector_id_rng, log } + } + + pub fn reporter(&mut self, reporter: Reporter) -> SimReporter { + let collector_id = self.collector_id_rng.next(); + let mut restart_id_rng = TypedUuidRng::from_parent_rng( + &mut self.parent, + ("restart_id", reporter), + ); + let restart_id = restart_id_rng.next(); + SimReporter { + reporter, + restart_id, + ena: Ena(0x1), + restart_id_rng, + collector_id, + log: self.log.new(slog::o!("reporter" => reporter.to_string())), + } + } +} + +pub struct SimReporter { + reporter: Reporter, + restart_id: EreporterRestartUuid, + ena: Ena, + restart_id_rng: TypedUuidRng, + + // TODO(eliza): this is not super realistic, as it will give a new "nexus" + // to each reporter...but the DEs don't actually care who collected the + // ereport, and we just need something to put in there. + collector_id: OmicronZoneUuid, + + log: slog::Logger, +} + +impl SimReporter { + #[track_caller] + pub fn parse_ereport( + &mut self, + now: chrono::DateTime, + json: &str, + ) -> Ereport { + self.mk_ereport( + now, + json.parse().expect("must be called with valid ereport JSON"), + ) + } + + pub fn mk_ereport( + &mut self, + now: chrono::DateTime, + json: serde_json::Map, + ) -> Ereport { + self.ena.0 += 1; + mk_ereport( + &self.log, + self.reporter, + EreportId { ena: self.ena, restart_id: self.restart_id }, + self.collector_id, + now, + json, + ) + } + + pub fn restart(&mut self) { + self.ena = Ena(0x1); + self.restart_id = self.restart_id_rng.next(); + } +} + +pub fn mk_ereport( + log: &slog::Logger, + reporter: Reporter, + id: EreportId, + collector_id: OmicronZoneUuid, + time_collected: chrono::DateTime, + json: serde_json::Map, +) -> Ereport { + let data = match reporter { + Reporter::Sp { .. } => { + let raw = ereport_types::Ereport { ena: id.ena, data: json }; + EreportData::from_sp_ereport( + log, + id.restart_id, + raw, + time_collected, + collector_id, + ) + } + Reporter::HostOs { .. } => { + todo!( + "eliza: when we get around to actually ingesting host ereport \ + JSON, figure out what the field names for serial and part \ + numbers would be!", + ); + } + }; + slog::info!( + &log, + "simulating an ereport: {}", data.id; + "ereport_id" => %data.id, + "ereport_class" => ?data.class, + "serial_number" => ?data.serial_number, + "part_number" => ?data.part_number, + ); + Ereport { reporter, data } +} From 42c7091a8639cd880d0fa09109a4b26cc81828df Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Dec 2025 14:45:04 -0800 Subject: [PATCH 3/4] guhwhoops --- nexus/fm/src/test_util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/fm/src/test_util.rs b/nexus/fm/src/test_util.rs index e0bae4b29f3..e0ff5b2049b 100644 --- a/nexus/fm/src/test_util.rs +++ b/nexus/fm/src/test_util.rs @@ -25,7 +25,7 @@ pub struct FmTest { impl FmTest { pub fn new_with_logxtx(test_name: &str) -> (Self, dev::LogContext) { let logctx = dev::test_setup_log(test_name); - (Self::new(test_name, logctx.log), logxtx) + (Self::new(test_name, &logctx.log), logxtx) } pub fn new(test_name: &str, log: &slog::Logger) -> Self { From b7b2dab5592cfc5f2f734e7e48acd2cb328bf9f3 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Dec 2025 14:51:45 -0800 Subject: [PATCH 4/4] gahwhoops --- nexus/fm/src/test_util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/fm/src/test_util.rs b/nexus/fm/src/test_util.rs index e0ff5b2049b..ecbb72d0326 100644 --- a/nexus/fm/src/test_util.rs +++ b/nexus/fm/src/test_util.rs @@ -25,7 +25,7 @@ pub struct FmTest { impl FmTest { pub fn new_with_logxtx(test_name: &str) -> (Self, dev::LogContext) { let logctx = dev::test_setup_log(test_name); - (Self::new(test_name, &logctx.log), logxtx) + (Self::new(test_name, &logctx.log), logctx) } pub fn new(test_name: &str, log: &slog::Logger) -> Self {