use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use console::Term;
use super::renderer::{Renderer, StatusFields, Table, Writer};
use super::{OutputFormat, Role, Theme, Verbosity};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PromptAnswer {
Confirm(bool),
Text(String),
Select(String),
}
pub struct DocCapture {
pub(crate) human: Arc<Mutex<String>>,
pub(crate) doc_json: Arc<Mutex<Option<serde_json::Value>>>,
}
impl DocCapture {
pub fn human(&self) -> String {
self.human.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn json(&self) -> Option<serde_json::Value> {
self.doc_json
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
}
pub struct Printer {
pub(crate) renderer: Arc<Renderer>,
pub(crate) output_format: OutputFormat,
pub(crate) sink_stderr: Arc<dyn Writer>,
pub(crate) sink_stdout: Arc<dyn Writer>,
pub(crate) multi_progress: indicatif::MultiProgress,
pub(crate) syntax_set: syntect::parsing::SyntaxSet,
pub(crate) theme_set: syntect::highlighting::ThemeSet,
pub(crate) test_doc_capture: Option<DocCapture>,
pub(crate) prompt_queue: Option<Arc<Mutex<VecDeque<PromptAnswer>>>>,
}
impl Printer {
pub fn new(verbosity: Verbosity) -> Self {
Self::with_format(verbosity, None, OutputFormat::Table)
}
pub fn with_theme_name(verbosity: Verbosity, theme_name: Option<&str>) -> Self {
Self::with_format(verbosity, theme_name, OutputFormat::Table)
}
pub fn with_format(
verbosity: Verbosity,
theme_name: Option<&str>,
output_format: OutputFormat,
) -> Self {
if std::env::var_os("NO_COLOR").is_some()
|| std::env::var_os("TERM").is_some_and(|t| t == "dumb")
|| output_format.is_structured()
{
console::set_colors_enabled(false);
console::set_colors_enabled_stderr(false);
}
let verbosity = if output_format.is_structured() {
Verbosity::Quiet
} else {
verbosity
};
let theme = theme_name.map(Theme::from_preset).unwrap_or_default();
Self {
renderer: Arc::new(Renderer::new(theme, verbosity)),
output_format,
sink_stderr: Arc::new(Term::stderr()),
sink_stdout: Arc::new(Term::stdout()),
multi_progress: indicatif::MultiProgress::new(),
syntax_set: syntect::parsing::SyntaxSet::load_defaults_newlines(),
theme_set: syntect::highlighting::ThemeSet::load_defaults(),
test_doc_capture: None,
prompt_queue: None,
}
}
pub fn verbosity(&self) -> Verbosity {
self.renderer.verbosity
}
pub fn output_format(&self) -> &OutputFormat {
&self.output_format
}
pub fn is_structured(&self) -> bool {
self.output_format.is_structured()
}
pub fn is_wide(&self) -> bool {
matches!(self.output_format, OutputFormat::Wide)
}
pub fn disable_colors() {
console::set_colors_enabled(false);
console::set_colors_enabled_stderr(false);
}
pub fn enable_colors() {
console::set_colors_enabled(true);
console::set_colors_enabled_stderr(true);
}
pub fn heading(&self, text: impl Into<String>) {
let depth = self.renderer.enforce_top_level_emit(0);
if depth == 0 {
self.renderer
.render_heading(self.sink_stderr.as_ref(), &text.into());
} else {
let text = text.into();
let styled = self.renderer.theme.header.apply_to(&text).to_string();
self.renderer
.write_line(self.sink_stderr.as_ref(), depth, &styled);
}
}
pub fn kv(&self, key: impl Into<String>, value: impl Into<String>) {
let _depth = self.renderer.enforce_top_level_emit(0);
self.renderer.render_kv(&key.into(), &value.into());
}
pub fn kv_block<I, K, V>(&self, pairs: I)
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let depth = self.renderer.enforce_top_level_emit(0);
let pairs: Vec<(String, String)> = pairs
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
self.renderer
.render_kv_block(self.sink_stderr.as_ref(), depth, &pairs);
}
pub fn hint(&self, text: impl Into<String>) {
let depth = self.renderer.enforce_top_level_emit(0);
self.renderer
.render_hint(self.sink_stderr.as_ref(), depth, &text.into());
}
pub fn note(&self, text: impl Into<String>) {
let depth = self.renderer.enforce_top_level_emit(0);
self.renderer
.render_note(self.sink_stderr.as_ref(), depth, &text.into());
}
pub fn table(&self, table: Table) {
let depth = self.renderer.enforce_top_level_emit(0);
self.renderer
.render_table(self.sink_stderr.as_ref(), depth, &table);
}
pub fn status_simple(&self, role: Role, subject: impl Into<String>) {
let depth = self.renderer.enforce_top_level_emit(0);
let subject = subject.into();
self.renderer.render_status(
self.sink_stderr.as_ref(),
depth,
&StatusFields {
role,
subject: &subject,
detail: None,
duration: None,
target: None,
},
);
}
pub fn status(
&self,
role: Role,
subject: impl Into<String>,
) -> super::status_builder::StatusBuilder<'_> {
let depth = self.renderer.enforce_top_level_emit(0);
super::status_builder::StatusBuilder::new(
self.renderer.clone(),
self.sink_stderr.clone(),
depth,
role,
subject,
)
}
#[must_use]
pub fn spinner(&self, message: impl Into<String>) -> super::spinner::Spinner<'_> {
let message = message.into();
let bar = super::spinner::make_spinner_bar(
&self.multi_progress,
&self.renderer,
self.verbosity(),
&message,
);
super::spinner::Spinner {
renderer: self.renderer.clone(),
sink: self.sink_stderr.clone(),
depth: 0,
bar,
message,
finished: false,
_phantom: std::marker::PhantomData,
}
}
#[must_use]
pub fn progress_bar(
&self,
total: u64,
message: impl Into<String>,
) -> super::spinner::ProgressBar<'_> {
let bar = super::spinner::make_progress_bar(
&self.multi_progress,
total,
self.verbosity(),
&message.into(),
);
super::spinner::ProgressBar {
bar,
_phantom: std::marker::PhantomData,
}
}
pub fn multi_progress(&self) -> &indicatif::MultiProgress {
&self.multi_progress
}
pub fn run(
&self,
cmd: &mut std::process::Command,
label: impl Into<String>,
) -> std::io::Result<super::process::CommandOutput> {
let _ = self.renderer.enforce_top_level_emit(0);
super::process::run_command(
&self.renderer,
self.sink_stderr.as_ref(),
&self.multi_progress,
0,
cmd,
&label.into(),
)
}
pub fn flush(&self) {
self.renderer.flush_kv_buffer(self.sink_stderr.as_ref());
}
pub fn render(&self, doc: super::doc::Doc) {
super::render_doc::render_doc(&self.renderer, self.sink_stderr.as_ref(), &doc);
}
pub fn emit(&self, doc: super::doc::Doc) {
if let Some(cap) = &self.test_doc_capture {
let json = doc.data_or_self_json();
*cap.doc_json.lock().unwrap_or_else(|e| e.into_inner()) = Some(json);
}
let handled = super::structured::emit_structured(
self.sink_stdout.as_ref(),
&doc,
&self.output_format,
);
if !handled {
self.render(doc);
}
}
#[must_use = "section closes when SectionGuard is dropped; bind it"]
pub fn section(&self, name: impl Into<String>) -> super::section_guard::SectionGuard<'_> {
self.renderer.render_section_open(&name.into(), true);
super::section_guard::SectionGuard {
printer: self,
renderer: self.renderer.clone(),
sink: self.sink_stderr.clone(),
depth: 1,
}
}
#[must_use = "section closes when SectionGuard is dropped; bind it"]
pub fn section_or_collapse(
&self,
name: impl Into<String>,
) -> super::section_guard::SectionGuard<'_> {
self.renderer.render_section_open(&name.into(), false);
super::section_guard::SectionGuard {
printer: self,
renderer: self.renderer.clone(),
sink: self.sink_stderr.clone(),
depth: 1,
}
}
}
impl Drop for Printer {
fn drop(&mut self) {
self.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "test-helpers")]
use crate::output::strip_ansi;
use crate::output::test_support::ColorsEnabledGuard;
use crate::test_helpers::EnvVarGuard;
use serial_test::serial;
#[test]
#[serial]
fn structured_format_auto_quiets() {
let p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Json);
assert_eq!(p.verbosity(), Verbosity::Quiet);
}
#[test]
#[serial]
fn table_format_keeps_verbosity() {
let p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Table);
assert_eq!(p.verbosity(), Verbosity::Normal);
}
#[test]
#[serial]
fn is_structured_classifies() {
let p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Json);
assert!(p.is_structured());
let p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Table);
assert!(!p.is_structured());
}
#[test]
#[serial]
fn structured_output_disables_colors() {
let _no_color = EnvVarGuard::unset("NO_COLOR");
let _term = EnvVarGuard::set("TERM", "xterm-256color");
let _guard = ColorsEnabledGuard::set(true);
for fmt in [
OutputFormat::Json,
OutputFormat::Yaml,
OutputFormat::Name,
OutputFormat::Jsonpath("{.foo}".into()),
OutputFormat::Template("{{ . }}".into()),
] {
console::set_colors_enabled(true);
console::set_colors_enabled_stderr(true);
let _p = Printer::with_format(Verbosity::Normal, None, fmt.clone());
assert!(
!console::colors_enabled(),
"stdout colors should be disabled for {fmt:?}"
);
assert!(
!console::colors_enabled_stderr(),
"stderr colors should be disabled for {fmt:?}"
);
}
}
#[test]
#[serial]
fn table_format_does_not_disable_colors_implicitly() {
let _no_color = EnvVarGuard::unset("NO_COLOR");
let _term = EnvVarGuard::set("TERM", "xterm-256color");
let _guard = ColorsEnabledGuard::set(true);
console::set_colors_enabled(true);
console::set_colors_enabled_stderr(true);
let _p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Table);
assert!(
console::colors_enabled(),
"Table format must not implicitly disable colors"
);
assert!(
console::colors_enabled_stderr(),
"Table format must not implicitly disable stderr colors"
);
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_with_bullets_renders_indented() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Files");
s.bullet("foo.txt");
s.bullet("bar.txt");
} p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Files\n"), "got: {out:?}");
assert!(out.contains("\n - foo.txt\n"), "got: {out:?}");
assert!(out.contains("\n - bar.txt\n"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_or_collapse_with_no_emits_leaves_no_trace() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let _s = p.section_or_collapse("Empty");
}
p.flush();
assert!(buf.lock().unwrap().trim().is_empty());
}
#[cfg(feature = "test-helpers")]
#[test]
fn nested_sections_indent_two_levels() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let outer = p.section("Outer");
{
let inner = outer.section("Inner");
inner.bullet("deep");
}
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Outer\n"));
assert!(out.contains("\n Inner\n"));
assert!(out.contains("\n - deep\n"));
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_kv_renders_key_value() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Details");
s.kv("Name", "cfgd");
s.kv("Version", "0.3.5");
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Details\n"), "got: {out:?}");
assert!(out.contains("Name"), "got: {out:?}");
assert!(out.contains("cfgd"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_kv_block_renders_pairs() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Config");
s.kv_block([("Profile", "default"), ("Source", "local")]);
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Config\n"), "got: {out:?}");
assert!(out.contains("Profile"), "got: {out:?}");
assert!(out.contains("default"), "got: {out:?}");
assert!(out.contains("Source"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_hint_renders() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Setup");
s.hint("Run cfgd init first");
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Setup\n"), "got: {out:?}");
assert!(out.contains("cfgd init"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_note_renders_at_verbose() {
let (p, buf) = Printer::for_test_at(Verbosity::Verbose);
{
let s = p.section("Status");
s.note("All modules up to date");
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Status\n"), "got: {out:?}");
assert!(out.contains("up to date"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_table_renders() {
use super::super::renderer::Table;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Packages");
let table = Table::new(["Name", "Version"]).row(["curl", "8.0"]);
s.table(table);
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Packages\n"), "got: {out:?}");
assert!(out.contains("curl"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_status_simple_renders() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Apply");
s.status_simple(Role::Ok, "package installed");
s.status_simple(Role::Fail, "file copy failed");
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Apply\n"), "got: {out:?}");
assert!(out.contains("package installed"), "got: {out:?}");
assert!(out.contains("file copy failed"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_status_builder_with_detail() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Apply");
s.status(Role::Ok, "brew install curl")
.detail("already installed");
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("brew install curl"), "got: {out:?}");
assert!(out.contains("already installed"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_empty_state_overrides_default() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Modules");
s.empty_state("no modules configured");
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Modules\n"), "got: {out:?}");
assert!(out.contains("no modules configured"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_or_collapse_with_child_renders() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section_or_collapse("Optional");
s.bullet("present");
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Optional\n"), "got: {out:?}");
assert!(out.contains("present"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn section_close_is_idempotent_via_explicit_close() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let s = p.section("Closing");
s.bullet("item");
s.close();
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Closing\n"), "got: {out:?}");
assert!(out.contains("item"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn nested_section_or_collapse_renders_child_content() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let outer = p.section("Outer");
{
let inner = outer.section_or_collapse("Inner");
inner.status_simple(Role::Ok, "done");
}
}
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Outer\n"), "got: {out:?}");
assert!(out.contains("Inner\n"), "got: {out:?}");
assert!(out.contains("done"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn render_doc_with_section_indents_correctly() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
let doc = Doc::new()
.heading("Status")
.kv("Profile", "dev")
.section("Files", |s| s.bullet("foo.txt").bullet("bar.txt"));
p.render(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Status\n"));
assert!(out.contains("Profile dev"));
assert!(out.contains("Files\n"));
assert!(out.contains("\n - foo.txt\n"));
}
#[cfg(feature = "test-helpers")]
#[test]
fn empty_section_or_collapse_in_doc_leaves_no_trace() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
let doc = Doc::new()
.heading("Status")
.section_or_collapse::<_>("Empty", |s| s);
p.render(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Status"));
assert!(!out.contains("Empty"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn emit_json_writes_data_payload_to_stdout() {
use super::super::doc::Doc;
#[derive(serde::Serialize)]
struct P {
foo: u32,
}
let (p, buf) = Printer::for_test_with_format(OutputFormat::Json);
let doc = Doc::new().heading("S").with_data(P { foo: 7 });
p.emit(doc);
let out = buf.lock().unwrap();
assert!(out.contains("\"foo\": 7"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn emit_table_writes_human_render() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
let doc = Doc::new().heading("Title").kv("k", "v");
p.emit(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Title"));
assert!(out.contains("k v"));
}
#[cfg(feature = "test-helpers")]
#[test]
fn emit_with_doc_capture_records_both_shapes() {
use super::super::doc::Doc;
let (p, cap) = Printer::for_test_doc();
let doc = Doc::new().heading("S").kv("k", "v");
p.emit(doc);
p.flush();
let human = cap.human();
let json = cap.json().unwrap();
assert!(human.contains("S"), "got: {human:?}");
assert!(human.contains("k"));
assert!(json["heading"].as_str() == Some("S"));
}
#[cfg(feature = "test-helpers")]
#[test]
fn render_doc_with_hint_renders_content() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
let doc = Doc::new()
.heading("Setup")
.hint("Run cfgd init to get started");
p.render(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Setup"), "got: {out:?}");
assert!(out.contains("cfgd init"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn render_doc_with_note_renders_at_verbose() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Verbose);
let doc = Doc::new().heading("Info").note("This is supplementary");
p.render(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Info"), "got: {out:?}");
assert!(out.contains("supplementary"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn render_doc_with_status_duration_and_target() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
let doc = Doc::new()
.heading("Apply")
.status_with(Role::Ok, "brew install curl", |f| {
f.detail("already installed")
.duration(std::time::Duration::from_millis(1500))
.target("/usr/local/bin/curl")
});
p.render(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("brew install curl"), "got: {out:?}");
assert!(out.contains("already installed"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn render_doc_section_with_empty_state() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
let doc = Doc::new()
.heading("Modules")
.section("Installed", |s| s.empty_state("no modules found"));
p.render(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Modules"), "got: {out:?}");
assert!(out.contains("Installed"), "got: {out:?}");
assert!(out.contains("no modules found"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn render_doc_with_kv_block() {
use super::super::doc::Doc;
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
let doc = Doc::new()
.heading("Config")
.kv_block([("Profile", "dev"), ("Source", "local")]);
p.render(doc);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Config"), "got: {out:?}");
assert!(out.contains("Profile"), "got: {out:?}");
assert!(out.contains("dev"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn status_builder_detail_opt_none() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
p.status(Role::Ok, "package check").detail_opt(None);
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("package check"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn status_builder_detail_opt_some() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
p.status(Role::Ok, "installed").detail_opt(Some("v1.2.3"));
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("installed"), "got: {out:?}");
assert!(out.contains("v1.2.3"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn status_builder_with_target_path() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
p.status(Role::Ok, "file deployed")
.target(std::path::Path::new("/home/user/.zshrc"));
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("file deployed"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
fn status_builder_with_duration() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
p.status(Role::Ok, "brew install curl")
.duration(std::time::Duration::from_secs(3));
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("brew install curl"), "got: {out:?}");
}
#[cfg(feature = "test-helpers")]
#[test]
#[cfg(debug_assertions)]
fn debug_mode_panics_on_top_level_emit_during_section() {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let (p, _buf) = Printer::for_test_at(Verbosity::Normal);
let _s = p.section("Outer");
p.heading("MidSection"); }));
assert!(result.is_err(), "expected debug_assert! panic");
}
#[cfg(feature = "test-helpers")]
#[test]
#[cfg(not(debug_assertions))]
fn release_mode_reroutes_top_level_emit_during_section() {
let (p, buf) = Printer::for_test_at(Verbosity::Normal);
{
let _s = p.section("Outer");
p.heading("MidSection"); }
p.flush();
let out = strip_ansi(&buf.lock().unwrap());
assert!(
out.contains("\n MidSection\n"),
"expected indented; got: {out:?}"
);
assert!(
!out.contains("\nMidSection\n"),
"unindented form leaked through: {out:?}"
);
}
}