diff --git a/src/install/PackageManager/PackageManagerDirectories.rs b/src/install/PackageManager/PackageManagerDirectories.rs index e9c0a7b328ac..e85b9814571c 100644 --- a/src/install/PackageManager/PackageManagerDirectories.rs +++ b/src/install/PackageManager/PackageManagerDirectories.rs @@ -1179,7 +1179,6 @@ pub fn save_lockfile( if this .lockfile .has_meta_hash_changed(false, packages_len_before_install) - .unwrap_or(false) { Output::panic(format_args!( "Lockfile metahash non-deterministic after saving" diff --git a/src/install/PackageManager/PopulateManifestCache.rs b/src/install/PackageManager/PopulateManifestCache.rs index 8716968ad97f..cf1ec6942259 100644 --- a/src/install/PackageManager/PopulateManifestCache.rs +++ b/src/install/PackageManager/PopulateManifestCache.rs @@ -366,13 +366,13 @@ pub fn populate_manifest_cache( } /// Prints the fetch failures a [`Packages::Ids`] pass logged; true when one of them is a required dependency's. -pub fn print_fetch_failures(manager: &PackageManager) -> crate::Result { +pub fn print_fetch_failures(manager: &PackageManager) -> bool { let log = manager.log_mut(); let failed_required = log.has_errors(); if !log.msgs.is_empty() { Output::flush(); - log.print(core::ptr::from_mut(Output::error_writer()))?; + let _ = log.print(core::ptr::from_mut(Output::error_writer())); log.reset(); } - Ok(failed_required) + failed_required } diff --git a/src/install/PackageManager/install_with_manager.rs b/src/install/PackageManager/install_with_manager.rs index 718d9c436bcf..5993bda15690 100644 --- a/src/install/PackageManager/install_with_manager.rs +++ b/src/install/PackageManager/install_with_manager.rs @@ -138,7 +138,7 @@ pub fn install_with_manager( manager.progress = Default::default(); match &load_result { - lockfile::LoadResult::Err(cause) => report_lockfile_load_error(manager, cause, log_level)?, + lockfile::LoadResult::Err(cause) => report_lockfile_load_error(manager, cause, log_level), lockfile::LoadResult::Ok(ok) => { if manager.subcommand == Subcommand::Update { record_updating_package_versions(manager); @@ -163,7 +163,7 @@ pub fn install_with_manager( let mut lockfile = Lockfile::default(); let mut maybe_root = lockfile::Package::default(); - let source_copy = root_package_json_source(manager, root_package_json_path)?; + let source_copy = root_package_json_source(manager, root_package_json_path); let mut resolver: () = (); // `parse` needs `manager`, `manager.log` and a fresh @@ -659,9 +659,9 @@ pub fn install_with_manager( print_kept_patched(manager); let had_errors_before_cleaning_lockfile = manager.log_mut().has_errors(); - manager + let _ = manager .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; + .print(std::ptr::from_mut(Output::error_writer())); manager.log_mut().reset(); super::add_catalog::refuse_declared_positionals(manager); @@ -801,15 +801,11 @@ pub fn install_with_manager( )) { break 'frozen_lockfile; } - } else if !(manager - .lockfile - .has_meta_hash_changed( - PackageManager::verbose_install() - || manager.options.do_.print_meta_hash_string(), - packages_len_before_install, - ) - .unwrap_or(false)) - { + } else if !manager.lockfile.has_meta_hash_changed( + PackageManager::verbose_install() + || manager.options.do_.print_meta_hash_string(), + packages_len_before_install, + ) { break 'frozen_lockfile; } } @@ -918,9 +914,9 @@ pub fn install_with_manager( }; if log_level != Options::LogLevel::Silent { - manager + let _ = manager .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; + .print(std::ptr::from_mut(Output::error_writer())); } if had_errors_before_cleaning_lockfile || manager.log_mut().has_errors() { Global::crash(); @@ -938,7 +934,7 @@ pub fn install_with_manager( manager.lockfile.has_meta_hash_changed( PackageManager::verbose_install() || manager.options.do_.print_meta_hash_string(), packages_len_before_install.min(manager.lockfile.packages.len()), - )? + ) }; // It's unnecessary work to re-save the lockfile if there are no changes. @@ -992,7 +988,7 @@ pub fn install_with_manager( did_meta_hash_change, requests_removed_from_lockfile, log_level, - )?; + ); } if install_summary.fail > 0 { @@ -1141,7 +1137,7 @@ fn print_install_summary( did_meta_hash_change: bool, requests_removed_from_lockfile: u32, log_level: Options::LogLevel, -) -> crate::Result<()> { +) { let _flush_guard = Output::flush_guard(); let mut printed_timestamp = false; @@ -1151,12 +1147,12 @@ fn print_install_summary( if install_summary.fail > 0 { print_summary_failed(install_summary); } - return Ok(()); + return; } if this.subcommand == Subcommand::Update && this.options.dry_run { - return Ok(()); + return; } - print_summary_tree(this, install_summary, log_level)?; + print_summary_tree(this, install_summary, log_level); let print_removed = this.subcommand == Subcommand::Remove && this.summary.remove > 0 @@ -1226,17 +1222,17 @@ fn print_install_summary( if this.options.do_.summary() && !printed_timestamp { print_summary_timing_fallback(ctx.start_time); } - - Ok(()) } +/// Best-effort: node_modules and the lockfile are already on disk, so a closed +/// stdout must not turn the install into an error. #[cold] #[inline(never)] fn print_summary_tree( this: &mut PackageManager, install_summary: &PackageInstallSummary, log_level: Options::LogLevel, -) -> crate::Result<()> { +) { // `Tree::print` reads `manager.{subcommand, workspace_name_hash, update_target_workspaces, updating_packages}` and writes `manager.{track_installed_bin, kept_patched_text, manifests}`, none overlapping the `Printer`'s `lockfile` / `options` / `update_requests` borrows. let mgr: *mut PackageManager = this; // `mgr` is the sole provenance root from here through the `Tree::print` @@ -1262,7 +1258,7 @@ fn print_summary_tree( Output::enable_buffering(); let writer = Output::writer_buffered(); // Runtime bool → const-generic dispatch. - if Output::enable_ansi_colors_stdout() { + let printed = if Output::enable_ansi_colors_stdout() { LockfilePrinter::Tree::print::<_, true>( &printer, // SAFETY: `mgr` is the sole provenance root; `Tree::print` writes only fields @@ -1270,7 +1266,7 @@ fn print_summary_tree( unsafe { &mut *mgr }, writer, log_level, - )?; + ) } else { LockfilePrinter::Tree::print::<_, false>( &printer, @@ -1279,9 +1275,11 @@ fn print_summary_tree( unsafe { &mut *mgr }, writer, log_level, - )?; + ) + }; + if let Err(crate::Error::Alloc(bun_alloc::AllocError)) = printed { + bun_core::out_of_memory(); } - Ok(()) } #[cold] @@ -1515,7 +1513,7 @@ fn report_lockfile_load_error( manager: &mut PackageManager, cause: &lockfile::LoadResultErr, log_level: Options::LogLevel, -) -> crate::Result<()> { +) { if log_level != Options::LogLevel::Silent && !crate::migration::reported_unsupported_lockfile_version(cause) { @@ -1531,9 +1529,9 @@ fn report_lockfile_load_error( } if manager.log_mut().errors > 0 { - manager + let _ = manager .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; + .print(std::ptr::from_mut(Output::error_writer())); manager.log_mut().reset(); } Output::flush(); @@ -1542,7 +1540,6 @@ fn report_lockfile_load_error( if manager.options.enable.fail_early() { Global::crash(); } - Ok(()) } /// Returns the rows the plan re-resolved so the overrides/catalogs invalidation loops that follow leave them pinned; only tracked when those loops will run. @@ -1867,23 +1864,20 @@ fn record_updating_package_versions(manager: &mut PackageManager) { } } -fn root_package_json_source( - manager: &mut PackageManager, - root_package_json_path: &ZStr, -) -> crate::Result { +fn root_package_json_source(manager: &mut PackageManager, root_package_json_path: &ZStr) -> Source { let (verb, err) = match manager.workspace_package_json_cache.get_with_path( manager.log_mut(), root_package_json_path.as_bytes(), Default::default(), ) { - WorkspacePackageJsonCacheResult::Entry(entry) => return Ok(entry.source.clone()), + WorkspacePackageJsonCacheResult::Entry(entry) => return entry.source.clone(), WorkspacePackageJsonCacheResult::ReadErr(err) => ("read", err), WorkspacePackageJsonCacheResult::ParseErr(err) => ("parse", err), }; if manager.log_mut().errors > 0 { - manager + let _ = manager .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; + .print(std::ptr::from_mut(Output::error_writer())); } Output::err( err, @@ -1930,7 +1924,7 @@ fn create_new_lockfile_and_enqueue( Global::crash(); } - let source_copy = root_package_json_source(manager, root_package_json_path)?; + let source_copy = root_package_json_source(manager, root_package_json_path); let mut resolver: () = (); { @@ -2202,7 +2196,7 @@ fn save_lockfile_only( manager.lockfile.meta_hash = manager.lockfile.generate_meta_hash( PackageManager::verbose_install() || manager.options.do_.print_meta_hash_string(), packages_len_before_install, - )?; + ); let saved = save_lockfile( manager, diff --git a/src/install/PackageManager/runTasks.rs b/src/install/PackageManager/runTasks.rs index 21b6e8a3f180..8495000e06f2 100644 --- a/src/install/PackageManager/runTasks.rs +++ b/src/install/PackageManager/runTasks.rs @@ -1039,8 +1039,7 @@ fn run_tasks_erased( if !task.log.msgs.is_empty() { // `IntoLogWrite` is implemented for `*mut bun_core::io::Writer`, // not `&mut Writer` (the underlying `Writer` is the FFI shape). - // Propagate the write error (WriteFailed) out of `run_tasks`. - task.log.print(std::ptr::from_mut(Output::error_writer()))?; + let _ = task.log.print(std::ptr::from_mut(Output::error_writer())); if task.log.errors > 0 { manager.any_failed_to_install = true; } diff --git a/src/install/error.rs b/src/install/error.rs index 1b2e2e464587..903c2f415bc8 100644 --- a/src/install/error.rs +++ b/src/install/error.rs @@ -212,7 +212,7 @@ pub enum Error { #[error(transparent)] Alloc(#[from] bun_alloc::AllocError), #[error(transparent)] - Core(#[from] bun_core::Error), + Core(bun_core::Error), #[error(transparent)] Resolver(#[from] bun_resolver::Error), #[error(transparent)] @@ -444,6 +444,17 @@ impl From for Error { } } +impl From for Error { + fn from(e: bun_core::Error) -> Self { + match e { + bun_core::Error::Alloc(a) => Self::Alloc(a), + bun_core::Error::WriteFailed => Self::WriteFailed, + bun_core::Error::InvalidCharacter => Self::InvalidCharacter, + other => Self::Core(other), + } + } +} + impl From for bun_core::Error { fn from(e: Error) -> Self { match e { diff --git a/src/install/lockfile.rs b/src/install/lockfile.rs index 4c721ac55ae6..00b47cbca499 100644 --- a/src/install/lockfile.rs +++ b/src/install/lockfile.rs @@ -1685,7 +1685,7 @@ impl<'a> Printer<'a> { // `IntoLogWrite` is implemented for `*mut bun_core::io::Writer`, // not `&mut &mut Writer` — pass the raw vtable pointer. let ew: *mut bun_core::io::Writer = Output::error_writer(); - log.print(ew)?; + let _ = log.print(ew); } Global::crash(); } @@ -2897,26 +2897,45 @@ impl Lockfile { &mut self, print_name_version_string: bool, packages_len: usize, - ) -> Result { + ) -> bool { let previous_meta_hash = self.meta_hash; - self.meta_hash = self.generate_meta_hash(print_name_version_string, packages_len)?; - Ok(!strings::eql_long( - &previous_meta_hash, - &self.meta_hash, - false, - )) + self.meta_hash = self.generate_meta_hash(print_name_version_string, packages_len); + !strings::eql_long(&previous_meta_hash, &self.meta_hash, false) } pub(crate) fn generate_meta_hash( &self, print_name_version_string: bool, packages_len: usize, - ) -> Result { + ) -> MetaHash { if packages_len <= 1 { - return Ok(ZERO_HASH); + return ZERO_HASH; + } + + let input = self.meta_hash_input(packages_len); + let input = input.written_slice(); + if print_name_version_string { + // The `--verbose` dump is best-effort; `bun pm hash-string` prints + // `meta_hash_input()` itself so it can report a failed write. + Output::flush(); + let _buffering = Output::disable_buffering_scope(); + let _ = Output::writer().write_all(input); } + let mut digest = ZERO_HASH; + // SAFETY: engine is null (default). + unsafe { Crypto::SHA512_256::hash(input, &mut digest, core::ptr::null_mut()) }; + digest + } + + /// The text `generate_meta_hash` digests; empty when the lockfile holds at + /// most the root package. + pub fn meta_hash_input(&self, packages_len: usize) -> bun_core::StringBuilder { let mut string_builder = bun_core::StringBuilder::default(); + if packages_len <= 1 { + return string_builder; + } + let names: &[SemverString] = &self.packages.items_name()[..packages_len]; let resolutions: &[Resolution] = &self.packages.items_resolution()[..packages_len]; let bytes = self.buffers.string_bytes.as_slice(); @@ -3046,29 +3065,7 @@ impl Lockfile { } let _ = string_builder.append(HASH_SUFFIX); - - let len = string_builder.len; - let alphabetized_name_version_string = &string_builder.allocated_slice()[..len]; - if print_name_version_string { - Output::flush(); - Output::disable_buffering(); - Output::writer() - .write_all(alphabetized_name_version_string) - .expect("unreachable"); - Output::enable_buffering(); - } - - let mut digest = ZERO_HASH; - // SAFETY: engine is null (default). - unsafe { - Crypto::SHA512_256::hash( - alphabetized_name_version_string, - &mut digest, - core::ptr::null_mut(), - ) - }; - - Ok(digest) + string_builder } pub(crate) fn resolve_package_from_name_and_version( diff --git a/src/install/migration.rs b/src/install/migration.rs index dd5270fe8424..4f9acb389857 100644 --- a/src/install/migration.rs +++ b/src/install/migration.rs @@ -409,7 +409,7 @@ fn migrate_npm_lockfile<'a>( this.verify_data()?; } - this.meta_hash = this.generate_meta_hash(false, this.packages.len())?; + this.meta_hash = this.generate_meta_hash(false, this.packages.len()); Ok(LoadResult::Ok(LoadResultOk { lockfile: this, diff --git a/src/install/update_transitive.rs b/src/install/update_transitive.rs index eaac11d84443..54981a2b0865 100644 --- a/src/install/update_transitive.rs +++ b/src/install/update_transitive.rs @@ -450,7 +450,7 @@ pub(crate) fn enqueue_peer_rows( index_sort::sort_indices_unstable(&mut targets, &mut |a, b| a.cmp(&b)); targets.dedup(); populate_manifest_cache::populate_manifest_cache(manager, Packages::Exact(&targets))?; - print_log(manager)?; + print_log(manager); for &row in rows { moved.push((row, manager.lockfile.buffers.resolutions[row as usize])); reresolve(manager, row)?; @@ -463,13 +463,12 @@ pub(crate) fn enqueue_peer_rows( } /// Pending log lines go to stderr; `--silent` drops everything but errors. -fn print_log(manager: &PackageManager) -> crate::Result<()> { +fn print_log(manager: &PackageManager) { let log = manager.log_mut(); if manager.options.log_level != LogLevel::Silent || log.has_errors() { - log.print(core::ptr::from_mut(Output::error_writer()))?; + let _ = log.print(core::ptr::from_mut(Output::error_writer())); } log.reset(); - Ok(()) } /// `moved` pairs an invalidated edge with the package it used to resolve to; every other edge still on that package follows it to the edge's new npm resolution when its range allows. @@ -1216,7 +1215,7 @@ fn plan_edges( } index_sort::sort_vec_unstable_by(&mut unchecked, |a, b| a.cmp(b)); warn_unchecked(manager, msgs_before, &unchecked); - print_log(manager)?; + print_log(manager); for (range, hash, pre) in pre_strings { let pre = manager diff --git a/src/install/yarn.rs b/src/install/yarn.rs index 542067033283..8a38a4f1b6ce 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -1913,7 +1913,7 @@ pub(crate) fn migrate_yarn_lockfile<'a>( this.verify_data()?; } - this.meta_hash = this.generate_meta_hash(false, this.packages.len())?; + this.meta_hash = this.generate_meta_hash(false, this.packages.len()); let result = LoadResult::Ok(lockfile::LoadResultOk { lockfile: this, diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 2dbefc463781..cd5a01edd012 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -1342,7 +1342,8 @@ pub fn format2( fmt.can_throw_stack_overflow = true; fmt.error_display_level = options.error_display_level; let tag = formatter::Tag::get(vals[0], global)?; - if fmt.write_indent(writer).is_err() { + fmt.write_indent(writer); + if fmt.failed { return Ok(()); } @@ -1406,7 +1407,8 @@ pub fn format2( fmt.error_display_level = options.error_display_level; let mut tag: formatter::TagResult; - if fmt.write_indent(writer).is_err() { + fmt.write_indent(writer); + if fmt.failed { return Ok(()); } @@ -2648,18 +2650,8 @@ pub mod formatter { /// Mirror of `Formatter::write_indent` routed through the wrapped /// `ctx` writer. Takes the current `Formatter::indent` by value. pub(crate) fn write_indent(&mut self, indent: u32) { - let mut total_remain: u32 = indent; - while total_remain > 0 { - let written: u8 = total_remain.min(32) as u8; - if self - .ctx - .write_all(&INDENTATION_BUF[0..(written as usize) * 2]) - .is_err() - { - self.failed = true; - return; - } - total_remain = total_remain.saturating_sub(u32::from(written)); + if write_indent_n(indent, self.ctx).is_err() { + self.failed = true; } } @@ -2717,11 +2709,6 @@ pub mod formatter { const INDENTATION_BUF: [u8; 64] = [b' '; 64]; - /// Free-function indent writer for callsites where a `WrappedWriter` - /// already holds `&mut self.estimated_line_length`, which would otherwise - /// conflict with the `&self` borrow `Formatter::write_indent` takes. - /// `self.indent` is a disjoint field read, so passing it by value here - /// keeps the borrow checker happy. fn write_indent_n(indent: u32, writer: &mut dyn bun_io::Write) -> bun_io::Result<()> { let mut total_remain: u32 = indent; while total_remain > 0 { @@ -2732,18 +2719,27 @@ pub mod formatter { Ok(()) } + /// Like `WrappedWriter`, these record a failed write in `self.failed` + /// rather than returning it; callers check `failed` to stop early. impl Formatter<'_> { - pub(crate) fn write_indent(&self, writer: &mut dyn bun_io::Write) -> bun_io::Result<()> { - write_indent_n(self.indent, writer) + pub(crate) fn write_all(&mut self, writer: &mut dyn bun_io::Write, bytes: &[u8]) { + if writer.write_all(bytes).is_err() { + self.failed = true; + } + } + + pub(crate) fn write_indent(&mut self, writer: &mut dyn bun_io::Write) { + if write_indent_n(self.indent, writer).is_err() { + self.failed = true; + } } pub(crate) fn print_comma( &mut self, writer: &mut dyn bun_io::Write, - ) -> bun_io::Result<()> { - writer.write_all(pfmt!(",", ENABLE_ANSI_COLORS).as_bytes())?; + ) { + self.write_all(writer, pfmt!(",", ENABLE_ANSI_COLORS).as_bytes()); self.estimated_line_length += 1; - Ok(()) } } @@ -2781,10 +2777,8 @@ pub mod formatter { return; } if SINGLE_LINE && this.count > 0 { - this.formatter - .print_comma::(this.writer) - .expect("unreachable"); - this.writer.write_all(b" ").expect("unreachable"); + this.formatter.print_comma::(this.writer); + this.formatter.write_all(this.writer, b" "); } if !IS_ITERATOR { let Ok(key) = next_value.get_index(global_object, 0) else { @@ -2795,9 +2789,10 @@ pub mod formatter { }; if !SINGLE_LINE { - this.formatter - .write_indent(this.writer) - .expect("unreachable"); + this.formatter.write_indent(this.writer); + } + if this.formatter.failed { + return; } let mut opts = TagOptions::HIDE_GLOBAL; if this.formatter.disable_inspect_custom { @@ -2813,7 +2808,10 @@ pub mod formatter { key, this.formatter.global_this, ); - this.writer.write_all(b": ").expect("unreachable"); + this.formatter.write_all(this.writer, b": "); + if this.formatter.failed { + return; + } let Ok(value_tag) = Tag::get_advanced(value, global_object, opts) else { return; }; @@ -2825,10 +2823,11 @@ pub mod formatter { ); } else { if !SINGLE_LINE { - this.writer.write_all(b"\n").expect("unreachable"); - this.formatter - .write_indent(this.writer) - .expect("unreachable"); + this.formatter.write_all(this.writer, b"\n"); + this.formatter.write_indent(this.writer); + } + if this.formatter.failed { + return; } let mut opts = TagOptions::HIDE_GLOBAL; if this.formatter.disable_inspect_custom { @@ -2846,11 +2845,9 @@ pub mod formatter { } this.count += 1; if !SINGLE_LINE { - this.formatter - .print_comma::(this.writer) - .expect("unreachable"); + this.formatter.print_comma::(this.writer); if !IS_ITERATOR { - this.writer.write_all(b"\n").expect("unreachable"); + this.formatter.write_all(this.writer, b"\n"); } } } @@ -2878,14 +2875,15 @@ pub mod formatter { } if SINGLE_LINE { if !this.is_first { - this.formatter - .print_comma::(this.writer) - .expect("unreachable"); - this.writer.write_all(b" ").expect("unreachable"); + this.formatter.print_comma::(this.writer); + this.formatter.write_all(this.writer, b" "); } this.is_first = false; } else { - let _ = this.formatter.write_indent(this.writer); + this.formatter.write_indent(this.writer); + } + if this.formatter.failed { + return; } let mut opts = TagOptions::HIDE_GLOBAL; if this.formatter.disable_inspect_custom { @@ -2902,10 +2900,8 @@ pub mod formatter { ); if !SINGLE_LINE { - this.formatter - .print_comma::(this.writer) - .expect("unreachable"); - this.writer.write_all(b"\n").expect("unreachable"); + this.formatter.print_comma::(this.writer); + this.formatter.write_all(this.writer, b"\n"); } } } @@ -2944,10 +2940,10 @@ pub mod formatter { self.formatter.indent += 1; self.formatter.depth += 1; if self.single_line { - let _ = self.writer.write_all(b"{ "); + self.formatter.write_all(self.writer, b"{ "); } else { - let _ = self.writer.write_all(b"{\n"); - let _ = self.formatter.write_indent(self.writer); + self.formatter.write_all(self.writer, b"{\n"); + self.formatter.write_indent(self.writer); } Ok(()) } @@ -3551,8 +3547,15 @@ pub mod formatter { return Ok(()); } - JSPrinter::write_json_string(str.latin1(), writer.ctx, JSPrinter::Encoding::Latin1) - .expect("unreachable"); + if JSPrinter::write_json_string( + str.latin1(), + writer.ctx, + JSPrinter::Encoding::Latin1, + ) + .is_err() + { + writer.failed = true; + } if C { writer.write_all(pfmt!("", true).as_bytes()); @@ -3579,13 +3582,14 @@ pub mod formatter { failed: false, estimated_line_length: &mut self.estimated_line_length, }; - } else { - JSPrinter::write_json_string( - str.latin1(), - writer.ctx, - JSPrinter::Encoding::Latin1, - ) - .expect("unreachable"); + } else if JSPrinter::write_json_string( + str.latin1(), + writer.ctx, + JSPrinter::Encoding::Latin1, + ) + .is_err() + { + writer.failed = true; } writer.print(format_args!("]")); @@ -4630,7 +4634,7 @@ pub mod formatter { } } if !self.single_line { - let _ = self.write_indent(writer_); + self.write_indent(writer_); } let _ = writer_.write_all(b"}"); Ok(()) @@ -4694,7 +4698,7 @@ pub mod formatter { } } if !self.single_line { - let _ = self.write_indent(writer_); + self.write_indent(writer_); } let _ = writer_.write_all(b"}"); Ok(()) @@ -4772,7 +4776,7 @@ pub mod formatter { } } if !self.single_line { - let _ = self.write_indent(writer_); + self.write_indent(writer_); } let _ = writer_.write_all(b"}"); Ok(()) @@ -4841,7 +4845,7 @@ pub mod formatter { let old_quote_strings = self.quote_strings; self.quote_strings = true; let _qs = defer_restore!(self.quote_strings, old_quote_strings); - self.write_indent(writer_).expect("unreachable"); + self.write_indent(writer_); if self.single_line { let _ = write!( @@ -4872,7 +4876,7 @@ pub mod formatter { { if message_value.is_string() { if !self.single_line { - self.write_indent(writer_).expect("unreachable"); + self.write_indent(writer_); } let _ = write!( writer_, @@ -4887,7 +4891,7 @@ pub mod formatter { if self.failed { return Ok(()); } - self.print_comma::(writer_).expect("unreachable"); + self.print_comma::(writer_); if !self.single_line { let _ = writer_.write_all(b"\n"); } @@ -4897,7 +4901,7 @@ pub mod formatter { match event_type { EventType::MessageEvent => { if !self.single_line { - self.write_indent(writer_).expect("unreachable"); + self.write_indent(writer_); } let _ = write!( writer_, @@ -4914,7 +4918,7 @@ pub mod formatter { if self.failed { return Ok(()); } - self.print_comma::(writer_).expect("unreachable"); + self.print_comma::(writer_); if !self.single_line { let _ = writer_.write_all(b"\n"); } @@ -4924,7 +4928,7 @@ pub mod formatter { value.fast_get(self.global_this, jsc::BuiltinName::Error)? { if !self.single_line { - self.write_indent(writer_).expect("unreachable"); + self.write_indent(writer_); } let _ = write!( writer_, @@ -4939,7 +4943,7 @@ pub mod formatter { if self.failed { return Ok(()); } - self.print_comma::(writer_).expect("unreachable"); + self.print_comma::(writer_); if !self.single_line { let _ = writer_.write_all(b"\n"); } @@ -4950,7 +4954,7 @@ pub mod formatter { } if !self.single_line { - self.write_indent(writer_).expect("unreachable"); + self.write_indent(writer_); } let _ = writer_.write_all(b"}"); Ok(()) @@ -5144,7 +5148,7 @@ pub mod formatter { ) { writer.write_all(b"\n"); - write_indent_n(self.indent, writer.ctx).expect("unreachable"); + writer.write_indent(self.indent); } else if props_i + 1 < count_without_children { writer.space(); } @@ -5175,21 +5179,18 @@ pub mod formatter { } else { self.indent += 1; writer.write_all(b"\n"); - write_indent_n(self.indent, writer.ctx) - .expect("unreachable"); + writer.write_indent(self.indent); self.indent = self.indent.saturating_sub(1); writer.write_string(&children_string); writer.write_all(b"\n"); - write_indent_n(self.indent, writer.ctx) - .expect("unreachable"); + writer.write_indent(self.indent); } } Tag::JSX => { writer.write_all(b">\n"); { self.indent += 1; - write_indent_n(self.indent, writer.ctx) - .expect("unreachable"); + writer.write_indent(self.indent); let _ind = defer_decrement!(self.indent); if writer.failed { self.failed = true; @@ -5208,8 +5209,7 @@ pub mod formatter { }; } writer.write_all(b"\n"); - write_indent_n(self.indent, writer.ctx) - .expect("unreachable"); + writer.write_indent(self.indent); } Tag::Array => { let length = children.get_length(self.global_this)?; @@ -5219,8 +5219,7 @@ pub mod formatter { writer.write_all(b">\n"); { self.indent += 1; - write_indent_n(self.indent, writer.ctx) - .expect("unreachable"); + writer.write_indent(self.indent); let _prev_quote_strings = self.quote_strings; self.quote_strings = false; let _qs2 = defer_restore!( @@ -5256,15 +5255,13 @@ pub mod formatter { }; if (j as u64) + 1 < length { writer.write_all(b"\n"); - write_indent_n(self.indent, writer.ctx) - .expect("unreachable"); + writer.write_indent(self.indent); } j += 1; } } writer.write_all(b"\n"); - write_indent_n(self.indent, writer.ctx) - .expect("unreachable"); + writer.write_indent(self.indent); } _ => unreachable!(), } @@ -5394,7 +5391,7 @@ pub mod formatter { let _ = writer_.write_all(b" "); } else if self.always_newline_scope || self.good_time_for_a_new_line() { let _ = writer_.write_all(b"\n"); - let _ = self.write_indent(writer_); + self.write_indent(writer_); self.reset_line(); } @@ -5437,9 +5434,9 @@ pub mod formatter { if iter_always_newline { self.indent = self.indent.saturating_sub(1); - self.print_comma::(writer_).expect("unreachable"); + self.print_comma::(writer_); let _ = writer_.write_all(b"\n"); - let _ = self.write_indent(writer_); + self.write_indent(writer_); let _ = writer_.write_all(b"}"); self.estimated_line_length += 1; } else { diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 05b0b17c80e2..3a1d110c7252 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -236,7 +236,7 @@ pub trait ConsoleFormatter { IndentScope::new(self) } /// `Formatter.writeIndent(Writer, writer)` — emit `2 * indent` spaces. - fn write_indent(&self, writer: &mut W) -> core::fmt::Result; + fn write_indent(&mut self, writer: &mut W) -> core::fmt::Result; /// `Formatter.resetLine()` — reset `estimated_line_length` to current /// indent so wrap heuristics start fresh on the next line. fn reset_line(&mut self); @@ -306,19 +306,28 @@ impl<'a> ConsoleFormatter for self::console_object::Formatter<'a> { fn reset_line(&mut self) { self::console_object::Formatter::reset_line(self) } - fn write_indent(&self, writer: &mut W) -> core::fmt::Result { + fn write_indent(&mut self, writer: &mut W) -> core::fmt::Result { // Inherent `Formatter::write_indent` takes `&mut dyn bun_io::Write`; // bridge the `core::fmt::Write` sink the same way `print_as` does. let mut sink = bun_io::FmtAdapter::new(writer); - self::console_object::Formatter::write_indent(self, &mut sink).map_err(|_| core::fmt::Error) + self::console_object::Formatter::write_indent(self, &mut sink); + if self.failed { + Err(core::fmt::Error) + } else { + Ok(()) + } } fn print_comma( &mut self, writer: &mut W, ) -> core::fmt::Result { let mut sink = bun_io::FmtAdapter::new(writer); - self::console_object::Formatter::print_comma::(self, &mut sink) - .map_err(|_| core::fmt::Error) + self::console_object::Formatter::print_comma::(self, &mut sink); + if self.failed { + Err(core::fmt::Error) + } else { + Ok(()) + } } fn print_as( &mut self, diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index 94066a86d194..f5f2f5e1003c 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -2067,10 +2067,7 @@ impl BuildArtifact { )?; { - formatter.indent_inc(); - // NOTE: reshaped for borrowck — scopeguard cannot reborrow - // `formatter` while it is also borrowed for the body; decrement - // after the block instead. + let mut formatter = bun_jsc::IndentScope::new(&mut *formatter); formatter.write_indent(writer)?; bun_core::write_pretty!( @@ -2079,9 +2076,7 @@ impl BuildArtifact { "path: \"{s}\"", bstr::BStr::new(&self.path), )?; - formatter - .print_comma::(writer) - .expect("unreachable"); + formatter.print_comma::(writer)?; writer.write_str("\n")?; formatter.write_indent(writer)?; @@ -2092,9 +2087,7 @@ impl BuildArtifact { <&'static str>::from(self.loader), )?; - formatter - .print_comma::(writer) - .expect("unreachable"); + formatter.print_comma::(writer)?; writer.write_str("\n")?; formatter.write_indent(writer)?; @@ -2107,9 +2100,7 @@ impl BuildArtifact { )?; if self.hash != 0 { - formatter - .print_comma::(writer) - .expect("unreachable"); + formatter.print_comma::(writer)?; writer.write_str("\n")?; formatter.write_indent(writer)?; @@ -2121,20 +2112,16 @@ impl BuildArtifact { )?; } - formatter - .print_comma::(writer) - .expect("unreachable"); + formatter.print_comma::(writer)?; writer.write_str("\n")?; formatter.write_indent(writer)?; formatter.reset_line(); self.blob - .write_format::(formatter, writer)?; + .write_format::(&mut formatter, writer)?; if self.output_kind != OutputKind::Sourcemap { - formatter - .print_comma::(writer) - .expect("unreachable"); + formatter.print_comma::(writer)?; writer.write_str("\n")?; formatter.write_indent(writer)?; write!( @@ -2151,8 +2138,11 @@ impl BuildArtifact { // SAFETY: `as_` returned a non-null wrapper-owned pointer; // `write_format` is `&self` so a shared borrow of `sm_ptr` // is sound even if it aliases `self`. - unsafe { &*sm_ptr } - .write_format::(sm_value, formatter, writer)?; + unsafe { &*sm_ptr }.write_format::( + sm_value, + &mut formatter, + writer, + )?; } else { write!( writer, @@ -2161,8 +2151,6 @@ impl BuildArtifact { )?; } } - - formatter.indent_dec(); } writer.write_str("\n")?; formatter.write_indent(writer)?; diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 655cdd33cf6c..abb56c86e444 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -601,9 +601,9 @@ impl BuildCommand { let result = this_transpiler.transform(ctx.log, ctx.args.clone())?; if log_ref.has_errors() { - log_ref.print(std::ptr::from_mut::( + let _ = log_ref.print(std::ptr::from_mut::( Output::error_writer(), - ))?; + )); if !result.errors.is_empty() || result.output_files.is_empty() { Output::flush(); @@ -651,11 +651,11 @@ impl BuildCommand { Ok(r) => r, Err(err) => { if !log_ref.msgs.is_empty() { - log_ref.print(std::ptr::from_mut::( + let _ = log_ref.print(std::ptr::from_mut::( Output::error_writer(), - ))?; + )); } else { - write!(Output::error_writer(), "error: {}", err.name())?; + let _ = write!(Output::error_writer(), "error: {}", err.name()); } Output::flush(); @@ -799,9 +799,10 @@ impl BuildCommand { { // if --no-bundle is passed, it won't have an output dir if let options::OutputFileValue::Buffer { bytes } = &output_files[0].value { - writer.write_all(bytes)?; + // The bundle is the result: write it unbuffered so a failed write is reported. + Output::flush(); + Output::writer().write_all(bytes)?; } - Output::flush(); break 'dump; } } @@ -1081,88 +1082,22 @@ impl BuildCommand { debug_assert!(!bun_paths::is_absolute(&f.dest_path)); - let rel_path = strings::trim_prefix(&f.dest_path, b"./"); - - // Print summary - let padding_count = 2usize.max(rel_path.len().max(max_path_len) - rel_path.len()); - splat_byte_all(writer, b' ', 2)?; - - if Output::enable_ansi_colors_stdout() { - writer.write_all(&Output::pretty_fmt::(match f.output_kind { - options::OutputKind::EntryPoint => "", - options::OutputKind::Chunk => "", - options::OutputKind::Asset => "", - options::OutputKind::Sourcemap => "", - options::OutputKind::Bytecode => "", - options::OutputKind::ModuleInfo - | options::OutputKind::BuiltinBytecode - | options::OutputKind::BytecodeStringTable - | options::OutputKind::ModuleInfoStringTable => "", - options::OutputKind::MetafileJson - | options::OutputKind::MetafileMarkdown => "", - }))?; - } - - writer.write_all(rel_path)?; - if Output::enable_ansi_colors_stdout() { - // highlight big files - let warn_threshold: usize = match f.output_kind { - options::OutputKind::EntryPoint | options::OutputKind::Chunk => 128 * 1024, - options::OutputKind::Asset => 16 * 1024 * 1024, - _ => usize::MAX, - }; - if f.size > warn_threshold { - writer.write_all(&Output::pretty_fmt::(""))?; - } else { - writer.write_all(b"\x1b[0m")?; - } - } - - splat_byte_all(writer, b' ', padding_count)?; - write!(writer, "{} ", bun_fmt::size(f.size, Default::default()))?; - splat_byte_all( - writer, - b' ', - size_padding - - bun_fmt::count(format_args!( - "{}", - bun_fmt::size(f.size, Default::default()) - )), - )?; - - if Output::enable_ansi_colors_stdout() { - writer.write_all(b"\x1b[2m")?; - } - write!( - writer, - "({})", - match f.output_kind { - options::OutputKind::EntryPoint => "entry point", - options::OutputKind::Chunk => "chunk", - options::OutputKind::Asset => "asset", - options::OutputKind::Sourcemap => "source map", - options::OutputKind::Bytecode => "bytecode", - options::OutputKind::ModuleInfo => "module info", - options::OutputKind::BuiltinBytecode => "builtin bytecode", - options::OutputKind::BytecodeStringTable => "bytecode strings", - options::OutputKind::ModuleInfoStringTable => "module info strings", - options::OutputKind::MetafileJson => "metafile json", - options::OutputKind::MetafileMarkdown => "metafile markdown", - } - )?; - if Output::enable_ansi_colors_stdout() { - writer.write_all(b"\x1b[0m")?; - } - writer.write_all(b"\n")?; + // The summary line is best-effort; every remaining file is still written. + let _ = print_output_file_line(writer, f, max_path_len, size_padding); } bun_core::prettyln!("\n"); Output::flush(); } - log_ref.print(std::ptr::from_mut::( + let _ = log_ref.print(std::ptr::from_mut::( Output::error_writer(), - ))?; + )); + // `exit_or_watch` never returns and runs no destructors, so free the + // option snapshots here rather than leave them to LeakSanitizer's + // stack scan. + drop(opt_output_dir); + drop(opt_public_path); exit_or_watch( if had_err { 1 } else { 0 }, ctx.debug.hot_reload == HotReload::Watch, @@ -1170,6 +1105,85 @@ impl BuildCommand { } } +fn print_output_file_line( + writer: &mut bun_core::io::Writer, + f: &options::OutputFile, + max_path_len: usize, + size_padding: usize, +) -> Result<(), crate::Error> { + let rel_path = strings::trim_prefix(&f.dest_path, b"./"); + let padding_count = 2usize.max(rel_path.len().max(max_path_len) - rel_path.len()); + splat_byte_all(writer, b' ', 2)?; + + if Output::enable_ansi_colors_stdout() { + writer.write_all(&Output::pretty_fmt::(match f.output_kind { + options::OutputKind::EntryPoint => "", + options::OutputKind::Chunk => "", + options::OutputKind::Asset => "", + options::OutputKind::Sourcemap => "", + options::OutputKind::Bytecode => "", + options::OutputKind::ModuleInfo + | options::OutputKind::BuiltinBytecode + | options::OutputKind::BytecodeStringTable + | options::OutputKind::ModuleInfoStringTable => "", + options::OutputKind::MetafileJson | options::OutputKind::MetafileMarkdown => "", + }))?; + } + + writer.write_all(rel_path)?; + if Output::enable_ansi_colors_stdout() { + // highlight big files + let warn_threshold: usize = match f.output_kind { + options::OutputKind::EntryPoint | options::OutputKind::Chunk => 128 * 1024, + options::OutputKind::Asset => 16 * 1024 * 1024, + _ => usize::MAX, + }; + if f.size > warn_threshold { + writer.write_all(&Output::pretty_fmt::(""))?; + } else { + writer.write_all(b"\x1b[0m")?; + } + } + + splat_byte_all(writer, b' ', padding_count)?; + write!(writer, "{} ", bun_fmt::size(f.size, Default::default()))?; + splat_byte_all( + writer, + b' ', + size_padding + - bun_fmt::count(format_args!( + "{}", + bun_fmt::size(f.size, Default::default()) + )), + )?; + + if Output::enable_ansi_colors_stdout() { + writer.write_all(b"\x1b[2m")?; + } + write!( + writer, + "({})", + match f.output_kind { + options::OutputKind::EntryPoint => "entry point", + options::OutputKind::Chunk => "chunk", + options::OutputKind::Asset => "asset", + options::OutputKind::Sourcemap => "source map", + options::OutputKind::Bytecode => "bytecode", + options::OutputKind::ModuleInfo => "module info", + options::OutputKind::BuiltinBytecode => "builtin bytecode", + options::OutputKind::BytecodeStringTable => "bytecode strings", + options::OutputKind::ModuleInfoStringTable => "module info strings", + options::OutputKind::MetafileJson => "metafile json", + options::OutputKind::MetafileMarkdown => "metafile markdown", + } + )?; + if Output::enable_ansi_colors_stdout() { + writer.write_all(b"\x1b[0m")?; + } + writer.write_all(b"\n")?; + Ok(()) +} + fn exit_or_watch(code: u8, watch: bool) -> ! { if watch { // the watcher thread will exit the process. `std::thread::sleep` diff --git a/src/runtime/cli/multi_run.rs b/src/runtime/cli/multi_run.rs index 5794e20d308f..72d12af729db 100644 --- a/src/runtime/cli/multi_run.rs +++ b/src/runtime/cli/multi_run.rs @@ -84,20 +84,20 @@ bun_io::impl_buffered_reader_parent! { has_on_read_chunk = true; on_read_chunk = |this, chunk, _has_more| { let state = &mut *((*(*this).handle).state as *mut State); - let _ = state.read_chunk(&mut *this, &chunk); + state.read_chunk(&mut *this, &chunk); true }; on_reader_done = |this| { (*this).ended = true; let handle = (*this).handle; let state = &mut *(*handle).state.cast_mut(); - let _ = state.maybe_finish(&mut *handle); + state.maybe_finish(&mut *handle); }; on_reader_error = |this, _err| { (*this).ended = true; let handle = (*this).handle; let state = &mut *(*handle).state.cast_mut(); - let _ = state.maybe_finish(&mut *handle); + state.maybe_finish(&mut *handle); }; loop_ = |this| bun_io::uws_to_native((*(*this).event_loop_ptr()).loop_); event_loop = |this| (*(*(*this).handle).state).event_loop_handle.as_event_loop_ctx(); @@ -329,7 +329,7 @@ bun_spawn::link_impl_ProcessExit! { ProcessHandle::drain_and_close_pipes(this); } let state = &mut *(*this).state.cast_mut(); - let _ = state.maybe_finish(&mut *this); + state.maybe_finish(&mut *this); }, } } @@ -366,7 +366,7 @@ impl<'a> State<'a> { self.remaining_scripts == 0 } - fn read_chunk(&mut self, pipe: &mut PipeReader<'a>, chunk: &[u8]) -> Result<(), Error> { + fn read_chunk(&mut self, pipe: &mut PipeReader<'a>, chunk: &[u8]) { pipe.line_buffer.extend_from_slice(chunk); // Route to correct parent stream: child stdout -> parent stdout, child stderr -> parent stderr @@ -381,11 +381,9 @@ impl<'a> State<'a> { let line = &pipe.line_buffer[0..newline_pos + 1]; // SAFETY: pipe.handle backref set in ProcessHandle::start() let handle = unsafe { &*pipe.handle }; - self.write_line_with_prefix(handle, line, writer)?; - // Remove processed line from buffer + let _ = self.write_line_with_prefix(handle, line, writer); pipe.line_buffer.drain_front(newline_pos + 1); } - Ok(()) } fn write_line_with_prefix( @@ -418,11 +416,7 @@ impl<'a> State<'a> { Ok(()) } - fn flush_pipe_buffer( - &self, - handle: &ProcessHandle<'a>, - pipe: &mut PipeReader<'a>, - ) -> Result<(), Error> { + fn flush_pipe_buffer(&self, handle: &ProcessHandle<'a>, pipe: &mut PipeReader<'a>) { if !pipe.line_buffer.is_empty() { let line = &pipe.line_buffer[..]; let needs_newline = !line.is_empty() && line[line.len() - 1] != b'\n'; @@ -431,42 +425,15 @@ impl<'a> State<'a> { } else { Output::writer() }; - self.write_line_with_prefix(handle, line, writer)?; + let _ = self.write_line_with_prefix(handle, line, writer); if needs_newline { let _ = writer.write_all(b"\n"); } pipe.line_buffer.clear(); } - Ok(()) } - /// A script is finished once its process has exited *and* both pipes have - /// ended; finishing on exit alone can drop output the exit notification - /// beat. The exit path force-ends pipes a leftover child holds open - /// (`drain_and_close_pipes`); on abort, exit alone suffices. - fn maybe_finish(&mut self, handle: &mut ProcessHandle<'a>) -> Result<(), Error> { - let exited = matches!(&handle.process, Some(p) if !matches!(p.status, Status::Running)); - let pipes_open = !handle.stdout_reader.ended || !handle.stderr_reader.ended; - if handle.finished || !exited || (pipes_open && !self.aborted) { - return Ok(()); - } - handle.finished = true; - self.remaining_scripts -= 1; - - // Flush remaining buffers (stdout first, then stderr) - // Reshaped for borrowck — `flush_pipe_buffer` would need both - // `&ProcessHandle` and `&mut handle.stdout_reader` which overlap. Route - // through a raw ptr (the State/handle backref pattern is already - // raw-ptr-based throughout this file). - let handle_ptr = std::ptr::from_mut::(handle); - // SAFETY: handle_ptr is live for this call; flush_pipe_buffer reads only - // `config`/`color_idx` from `handle` and writes only `pipe.line_buffer`. - unsafe { - self.flush_pipe_buffer(&*handle_ptr, &mut (*handle_ptr).stdout_reader)?; - self.flush_pipe_buffer(&*handle_ptr, &mut (*handle_ptr).stderr_reader)?; - } - - // Print exit status to stderr (status messages always go to stderr) + fn print_exit_status(&self, handle: &ProcessHandle<'a>) -> Result<(), Error> { let writer = Output::error_writer(); self.write_prefix(handle, writer)?; @@ -497,9 +464,40 @@ impl<'a> State<'a> { writer.write_all(b"Error\n")?; } } + Ok(()) + } + + /// A script is finished once its process has exited *and* both pipes have + /// ended; finishing on exit alone can drop output the exit notification + /// beat. The exit path force-ends pipes a leftover child holds open + /// (`drain_and_close_pipes`); on abort, exit alone suffices. + fn maybe_finish(&mut self, handle: &mut ProcessHandle<'a>) { + let exited = matches!(&handle.process, Some(p) if !matches!(p.status, Status::Running)); + let pipes_open = !handle.stdout_reader.ended || !handle.stderr_reader.ended; + if handle.finished || !exited || (pipes_open && !self.aborted) { + return; + } + handle.finished = true; + self.remaining_scripts -= 1; + + // Flush remaining buffers (stdout first, then stderr) + // Reshaped for borrowck — `flush_pipe_buffer` would need both + // `&ProcessHandle` and `&mut handle.stdout_reader` which overlap. Route + // through a raw ptr (the State/handle backref pattern is already + // raw-ptr-based throughout this file). + let handle_ptr = std::ptr::from_mut::(handle); + // SAFETY: handle_ptr is live for this call; flush_pipe_buffer reads only + // `config`/`color_idx` from `handle` and writes only `pipe.line_buffer`. + unsafe { + self.flush_pipe_buffer(&*handle_ptr, &mut (*handle_ptr).stdout_reader); + self.flush_pipe_buffer(&*handle_ptr, &mut (*handle_ptr).stderr_reader); + } + + // Best-effort: a closed stderr must not stop the abort or the dependents below. + let _ = self.print_exit_status(handle); // Check if we should abort on error - let failed = match &slot.status { + let failed = match &handle.process.as_ref().unwrap().status { Status::Exited(exited) => exited.code != 0, Status::Signaled(_) => true, _ => true, @@ -507,7 +505,7 @@ impl<'a> State<'a> { if failed && !self.no_exit_on_error { self.abort(); - return Ok(()); + return; } if failed { @@ -521,7 +519,7 @@ impl<'a> State<'a> { if !self.aborted { Self::start_dependents(&next); } - return Ok(()); + return; } // Success: cascade to all dependents @@ -531,9 +529,7 @@ impl<'a> State<'a> { Self::start_dependents(&group); Self::start_dependents(&next); } - Ok(()) } - fn start_dependents(dependents: &[*mut ProcessHandle]) { for &dependent in dependents { // SAFETY: dependent points into State.handles which outlives this call @@ -590,7 +586,7 @@ impl<'a> State<'a> { // handles finish when their exit arrives. // SAFETY: same `self.handles` element as above; the exclusive // reborrow is confined to this call. - let _ = self.maybe_finish(unsafe { &mut *handle }); + self.maybe_finish(unsafe { &mut *handle }); } } diff --git a/src/runtime/cli/outdated_command.rs b/src/runtime/cli/outdated_command.rs index c0218f66dc04..bbe34f923343 100644 --- a/src/runtime/cli/outdated_command.rs +++ b/src/runtime/cli/outdated_command.rs @@ -191,7 +191,7 @@ impl OutdatedCommand { &workspace_pkg_ids, was_filtered, )?; - if populate_manifest_cache::print_fetch_failures(manager)? { + if populate_manifest_cache::print_fetch_failures(manager) { Global::crash(); } Ok(()) diff --git a/src/runtime/cli/package_manager_command.rs b/src/runtime/cli/package_manager_command.rs index 7c66aafb4657..767d1c3e2d0d 100644 --- a/src/runtime/cli/package_manager_command.rs +++ b/src/runtime/cli/package_manager_command.rs @@ -134,10 +134,11 @@ impl PackageManagerCommand { Self::handle_load_lockfile_errors_for(&load_lockfile, log_level, "hash"); - Output::flush(); - Output::disable_buffering(); - Output::writer().print(format_args!("{}", pm.lockfile.fmt_meta_hash()))?; - Output::enable_buffering(); + { + Output::flush(); + let _buffering = Output::disable_buffering_scope(); + Output::writer().print(format_args!("{}", pm.lockfile.fmt_meta_hash()))?; + } Global::exit(0); } @@ -403,22 +404,24 @@ Learn more about these at https://bun.com/docs/cli/pm.\n"; let pm = unsafe { &mut *pm_ptr }; let _ = pm .lockfile - .has_meta_hash_changed(false, pm.lockfile.packages.len())?; + .has_meta_hash_changed(false, pm.lockfile.packages.len()); - Output::flush(); - Output::disable_buffering(); - Output::writer().print(format_args!("{}", pm.lockfile.fmt_meta_hash()))?; - Output::enable_buffering(); + { + Output::flush(); + let _buffering = Output::disable_buffering_scope(); + Output::writer().print(format_args!("{}", pm.lockfile.fmt_meta_hash()))?; + } Global::exit(0); } else if strings::eql_comptime(subcommand, b"hash-print") { let log_level = pm.options.log_level; let load_lockfile = pm.load_lockfile_from_cwd::(); Self::handle_load_lockfile_errors_for(&load_lockfile, log_level, "hash"); - Output::flush(); - Output::disable_buffering(); - Output::writer().print(format_args!("{}", pm.lockfile.fmt_meta_hash()))?; - Output::enable_buffering(); + { + Output::flush(); + let _buffering = Output::disable_buffering_scope(); + Output::writer().print(format_args!("{}", pm.lockfile.fmt_meta_hash()))?; + } Global::exit(0); } else if strings::eql_comptime(subcommand, b"hash-string") { let log_level = pm.options.log_level; @@ -427,9 +430,12 @@ Learn more about these at https://bun.com/docs/cli/pm.\n"; // SAFETY: pm_ptr is the unique owner; lockfile borrow released above. let pm = unsafe { &mut *pm_ptr }; - let _ = pm - .lockfile - .has_meta_hash_changed(true, pm.lockfile.packages.len())?; + { + let input = pm.lockfile.meta_hash_input(pm.lockfile.packages.len()); + Output::flush(); + let _buffering = Output::disable_buffering_scope(); + Output::writer().write_all(input.written_slice())?; + } Global::exit(0); } else if strings::eql_comptime(subcommand, b"cache") { if pm.options.positionals.len() > 1 diff --git a/src/runtime/cli/pm_view_command.rs b/src/runtime/cli/pm_view_command.rs index 1151dd51b750..02ad8e6d37fd 100644 --- a/src/runtime/cli/pm_view_command.rs +++ b/src/runtime/cli/pm_view_command.rs @@ -156,7 +156,7 @@ pub(crate) fn view( } }; if log.errors > 0 { - log.print(std::ptr::from_mut(Output::error_writer()))?; + let _ = log.print(std::ptr::from_mut(Output::error_writer())); Global::crash(); } diff --git a/src/runtime/cli/test_command.rs b/src/runtime/cli/test_command.rs index 82f818bb028b..d5c0812ac951 100644 --- a/src/runtime/cli/test_command.rs +++ b/src/runtime/cli/test_command.rs @@ -1740,54 +1740,19 @@ impl CommandLineReporter { let base_fraction = opts.fractions; let mut failing = false; + // Printing the table is best-effort; the lcov file and `opts.fractions.failing` are not. if reporters_text { - if console.write_all(pretty_lit!("")).is_err() { - return Ok(()); - } - if console - .splat_byte_all(b'-', max_filepath_length + 2) - .is_err() - { - return Ok(()); - } - if console - .write_all(pretty_lit!("|---------|---------|-------------------\n")) - .is_err() - { - return Ok(()); - } - if console.write_all(b"File").is_err() { - return Ok(()); - } - if console - .splat_byte_all(b' ', max_filepath_length - b"File".len() + 1) - .is_err() - { - return Ok(()); - } - if console - .write_all(pretty_lit!( - " | % Funcs | % Lines | Uncovered Line #s\n" - )) - .is_err() - { - return Ok(()); - } - if console.write_all(pretty_lit!("")).is_err() { - return Ok(()); - } - if console - .splat_byte_all(b'-', max_filepath_length + 2) - .is_err() - { - return Ok(()); - } - if console - .write_all(pretty_lit!("|---------|---------|-------------------\n")) - .is_err() - { - return Ok(()); - } + let _ = console.write_all(pretty_lit!("")); + let _ = console.splat_byte_all(b'-', max_filepath_length + 2); + let _ = console.write_all(pretty_lit!("|---------|---------|-------------------\n")); + let _ = console.write_all(b"File"); + let _ = console.splat_byte_all(b' ', max_filepath_length - b"File".len() + 1); + let _ = console.write_all(pretty_lit!( + " | % Funcs | % Lines | Uncovered Line #s\n" + )); + let _ = console.write_all(pretty_lit!("")); + let _ = console.splat_byte_all(b'-', max_filepath_length + 2); + let _ = console.write_all(pretty_lit!("|---------|---------|-------------------\n")); } let mut console_buffer: Vec = Vec::new(); @@ -1965,7 +1930,7 @@ impl CommandLineReporter { } }; - coverage::Text::write_format_with_values( + let _ = coverage::Text::write_format_with_values( b"All files", max_filepath_length, avg, @@ -1974,29 +1939,15 @@ impl CommandLineReporter { &mut console, false, enable_ansi_colors, - )?; + ); - console.write_all(pretty_lit!(" |\n"))?; + let _ = console.write_all(pretty_lit!(" |\n")); } - console.write_all(&console_buffer)?; - console.write_all(pretty_lit!(""))?; - // Disarm the lcov cleanup guard before the early `Ok(())`; the - // temp file is left for the OS. - if console - .splat_byte_all(b'-', max_filepath_length + 2) - .is_err() - { - let _ = scopeguard::ScopeGuard::into_inner(lcov_guard); - return Ok(()); - } - if console - .write_all(pretty_lit!("|---------|---------|-------------------\n")) - .is_err() - { - let _ = scopeguard::ScopeGuard::into_inner(lcov_guard); - return Ok(()); - } + let _ = console.write_all(&console_buffer); + let _ = console.write_all(pretty_lit!("")); + let _ = console.splat_byte_all(b'-', max_filepath_length + 2); + let _ = console.write_all(pretty_lit!("|---------|---------|-------------------\n")); opts.fractions.failing = failing; Output::flush(); diff --git a/src/runtime/cli/update_interactive_command.rs b/src/runtime/cli/update_interactive_command.rs index c9c3bbb1f9aa..fa71ebb1cf6d 100644 --- a/src/runtime/cli/update_interactive_command.rs +++ b/src/runtime/cli/update_interactive_command.rs @@ -549,9 +549,9 @@ impl UpdateInteractiveCommand { // for every subcommand and is non-null for the command's // lifetime. if unsafe { (*ctx_log_ptr).has_errors() } { - manager + let _ = manager .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; + .print(std::ptr::from_mut(Output::error_writer())); } } Global::crash(); @@ -583,7 +583,7 @@ impl UpdateInteractiveCommand { manager, populate_manifest_cache::Packages::Ids(&workspace_pkg_ids), )?; - if populate_manifest_cache::print_fetch_failures(manager)? { + if populate_manifest_cache::print_fetch_failures(manager) { Global::crash(); } diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index 5eacda6287ba..35e3f273828c 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -2586,7 +2586,7 @@ impl bun_jsc::ConsoleFormatter for Formatter<'_> { fn indent_dec(&mut self) { self.indent = self.indent.saturating_sub(1); } #[inline] fn reset_line(&mut self) { Formatter::reset_line(self) } - fn write_indent(&self, writer: &mut W) -> core::fmt::Result { + fn write_indent(&mut self, writer: &mut W) -> core::fmt::Result { let mut sink = bun_io::FmtAdapter::new(writer); Formatter::write_indent(self, &mut sink).map_err(|_| core::fmt::Error) } @@ -2888,10 +2888,13 @@ impl JestPrettyFormat { )?; *this.amf_quote_strings() = original_quote_strings; } else if let Some(instance) = value.as_class_ref::() { - let printed = expect::ExpectCustomAsymmetricMatcher::custom_print( + // With `dont_throw` the only error left is the writer's. + let Ok(printed) = expect::ExpectCustomAsymmetricMatcher::custom_print( instance, value, this.amf_global_this(), &mut *writer.ctx, true, - ) - .expect("unreachable"); + ) else { + writer.failed = true; + return Ok(true); + }; if !printed { // default print (non-overridden by user) let flags = instance.flags; diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index 1f84288e6305..7a02513d043d 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -1106,7 +1106,7 @@ impl BlobExt for Blob { { writer.write_str(" {\n")?; { - formatter.indent_inc(); + let mut formatter = bun_jsc::IndentScope::new(&mut *formatter); if show_name { formatter.write_indent(writer)?; @@ -1162,8 +1162,6 @@ impl BlobExt for Blob { self.last_modified.get(), )?; } - - formatter.indent_dec(); } formatter.write_indent(writer)?; writer.write_str("}")?; diff --git a/src/runtime/webcore/Request.rs b/src/runtime/webcore/Request.rs index 8ceaadd119dd..5a3f9882adc7 100644 --- a/src/runtime/webcore/Request.rs +++ b/src/runtime/webcore/Request.rs @@ -559,9 +559,7 @@ impl Request { // Wire-form token (e.g. "M-SEARCH"), not the Rust Debug variant identifier. writer.write_str(self.method.as_str())?; writer.write_str("\"")?; - formatter - .print_comma::<_, ENABLE_ANSI_COLORS>(writer) - .expect("unreachable"); + formatter.print_comma::<_, ENABLE_ANSI_COLORS>(writer)?; writer.write_str("\n")?; formatter.write_indent(writer)?; @@ -578,9 +576,7 @@ impl Request { Output::pretty_fmt::("\""), ) )?; - formatter - .print_comma::<_, ENABLE_ANSI_COLORS>(writer) - .expect("unreachable"); + formatter.print_comma::<_, ENABLE_ANSI_COLORS>(writer)?; writer.write_str("\n")?; if params_object.is_cell() { @@ -596,9 +592,7 @@ impl Request { bun_jsc::JSType::Object, ) .map_err(js_err)?; - formatter - .print_comma::<_, ENABLE_ANSI_COLORS>(writer) - .expect("unreachable"); + formatter.print_comma::<_, ENABLE_ANSI_COLORS>(writer)?; writer.write_str("\n")?; } diff --git a/src/sys/lib.rs b/src/sys/lib.rs index 59f319ce8884..1938644d5e58 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -9249,17 +9249,17 @@ fn qw_set_fd(qw: &mut bun_core::output::QuietWriter, fd: Fd) { } } -/// Best-effort write-all loop. Returns `false` on I/O error / zero-write so -/// `ScopedLogger::log` can disable the scope; "quiet" callers discard the bool. -fn fd_write_all_quiet(fd: Fd, mut bytes: &[u8]) -> bool { +/// Write-all behind `Output::writer()`/`error_writer()` and the scoped-debug +/// `QuietWriter`. Nothing is logged on failure; the error is returned so the +/// adapter can surface it and `ScopedLogger::log` can disable the scope. +fn fd_write_all_quiet(fd: Fd, mut bytes: &[u8]) -> Maybe<()> { while !bytes.is_empty() { - match write(fd, bytes) { - Ok(0) => return false, // short write → give up - Ok(n) => bytes = &bytes[n..], - Err(_) => return false, + match write(fd, bytes)? { + 0 => return Err(Error::from_code(E::EIO, Tag::write)), + n => bytes = &bytes[n..], } } - true + Ok(()) } /// Concrete repr behind the opaque `bun_core::output::QuietWriterAdapter` @@ -9305,19 +9305,18 @@ unsafe fn adapter_write_all( // SAFETY: `w` points at the first field of a SysQuietWriterAdapter (repr(C)). let this = unsafe { &mut *w.cast::() }; if this.cap == 0 { - let _ = fd_write_all_quiet(this.fd, bytes); - return Ok(()); + return fd_write_all_quiet(this.fd, bytes).map_err(|_| bun_core::Error::WriteFailed); } if this.pos + bytes.len() > this.cap { // Drain buffered bytes first. if this.pos > 0 { - let _ = fd_write_all_quiet(this.fd, this.buffered()); + let drained = fd_write_all_quiet(this.fd, this.buffered()); this.pos = 0; + drained.map_err(|_| bun_core::Error::WriteFailed)?; } // Large writes bypass the buffer so the next small write still coalesces. if bytes.len() >= this.cap { - let _ = fd_write_all_quiet(this.fd, bytes); - return Ok(()); + return fd_write_all_quiet(this.fd, bytes).map_err(|_| bun_core::Error::WriteFailed); } } // SAFETY: `this.buf` has capacity `this.cap`; the branch above ensures @@ -9333,8 +9332,9 @@ unsafe fn adapter_flush(w: *mut bun_core::io::Writer) -> core::result::Result<() // SAFETY: `w` points at the first field of a SysQuietWriterAdapter (repr(C)). let this = unsafe { &mut *w.cast::() }; if this.pos > 0 { - let _ = fd_write_all_quiet(this.fd, this.buffered()); + let drained = fd_write_all_quiet(this.fd, this.buffered()); this.pos = 0; + return drained.map_err(|_| bun_core::Error::WriteFailed); } Ok(()) } @@ -9411,7 +9411,7 @@ bun_core::link_impl_OutputSink! { }, // QuietWriter itself is unbuffered (buffering lives in the Adapter). quiet_writer_flush(_qw) => (), - quiet_writer_write_all(qw, bytes) => fd_write_all_quiet(qw_fd(qw), bytes), + quiet_writer_write_all(qw, bytes) => fd_write_all_quiet(qw_fd(qw), bytes).is_ok(), quiet_writer_fd(qw) => qw_fd(qw), tty_winsize(fd) => sink_tty_winsize(fd), is_terminal(fd) => isatty(fd), diff --git a/test/bundler/cli.test.ts b/test/bundler/cli.test.ts index 9142e6350096..9ecfdddcc151 100644 --- a/test/bundler/cli.test.ts +++ b/test/bundler/cli.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, isWindows, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness"; -import fs, { mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import fs, { closeSync, mkdirSync, openSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import path, { join } from "node:path"; describe.concurrent( @@ -912,3 +912,59 @@ describe.concurrent("modules that fail to print", () => { expect(exitCode).toBe(1); }); }); + +test.skipIf(isWindows)("bun build still writes every output file when stdout cannot be written", async () => { + using dir = tempDir("build-ebadf", { + "a.js": `import "./shared.js"; console.log("a");`, + "b.js": `import "./shared.js"; console.log("b");`, + "shared.js": `console.log("shared");`, + }); + // A read-only descriptor as stdout makes the summary's write(2) fail with EBADF. + const stdoutFd = openSync("/dev/null", "r"); + try { + await using proc = Bun.spawn({ + cmd: [bunExe(), "build", "./a.js", "./b.js", "--outdir=out", "--splitting"], + cwd: String(dir), + env: bunEnv, + stdin: "ignore", + stdout: stdoutFd, + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + // Two entry points and the shared chunk are all on disk. + const files = fs.readdirSync(join(String(dir), "out")).sort(); + expect(files).toHaveLength(3); + expect(files).toContain("a.js"); + expect(files).toContain("b.js"); + expect(exitCode).toBe(0); + } finally { + closeSync(stdoutFd); + } +}); + +test.skipIf(isWindows)( + "bun build to stdout reports a failed write, also for a bundle smaller than the output buffer", + async () => { + using dir = tempDir("build-stdout-ebadf", { + "a.js": `console.log("a");`, + }); + // A read-only descriptor as stdout makes the bundle's write(2) fail with EBADF. + const stdoutFd = openSync("/dev/null", "r"); + try { + await using proc = Bun.spawn({ + cmd: [bunExe(), "build", "./a.js"], + cwd: String(dir), + env: bunEnv, + stdin: "ignore", + stdout: stdoutFd, + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(stderr).toContain("WriteFailed"); + expect(exitCode).toBe(1); + } finally { + closeSync(stdoutFd); + } + }, +); diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index 8ab93b18ba94..9f27fdd6aacc 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -1,8 +1,17 @@ import { spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, test } from "bun:test"; import { exists, mkdir, writeFile } from "fs/promises"; -import { bunEnv, bunExe, bunEnv as env, normalizeBunSnapshot, readdirSorted, tempDir, tmpdirSync } from "harness"; -import { cpSync } from "node:fs"; +import { + bunEnv, + bunExe, + bunEnv as env, + isWindows, + normalizeBunSnapshot, + readdirSorted, + tempDir, + tmpdirSync, +} from "harness"; +import { closeSync, cpSync, openSync } from "node:fs"; import { join } from "path"; import { dummyAfterAll, @@ -796,6 +805,39 @@ it("bun pm migrate", async () => { expect(hash).toMatchSnapshot(); }); +// `hash-string` prints nothing for a lockfile with a single package, so the +// fixture carries one dependency. +test.skipIf(isWindows).each(["hash", "hash-string"])("bun pm %s reports a failed write to stdout", async subcommand => { + const dep = "github:example/repo#abc1234"; + using dir = tempDir("pm-hash-write-failed", { + "package.json": JSON.stringify({ name: "pm-hash-write-failed", version: "1.0.0", dependencies: { dep } }), + "bun.lock": JSON.stringify({ + lockfileVersion: 1, + configVersion: 1, + workspaces: { "": { name: "pm-hash-write-failed", dependencies: { dep } } }, + packages: { dep: [`dep@${dep}`, {}, "example-repo-abc1234"] }, + }), + }); + + // A read-only descriptor as stdout makes the child's write(2) fail with EBADF. + const stdoutFd = openSync("/dev/null", "r"); + try { + await using proc = Bun.spawn({ + cmd: [bunExe(), "pm", subcommand], + cwd: String(dir), + stdin: "ignore", + stdout: stdoutFd, + stderr: "pipe", + env: bunEnv, + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(stderr).toContain("WriteFailed"); + expect(exitCode).toBe(1); + } finally { + closeSync(stdoutFd); + } +}); + test("bun whoami executes pm whoami", async () => { // Test that "bun whoami" doesn't show reservation message and instead executes pm whoami // First create a simple package.json diff --git a/test/cli/install/bun-update-transitive.test.ts b/test/cli/install/bun-update-transitive.test.ts index 43d7ae599a80..7d4693baaf85 100644 --- a/test/cli/install/bun-update-transitive.test.ts +++ b/test/cli/install/bun-update-transitive.test.ts @@ -1,7 +1,8 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { exists } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, tempDir } from "harness"; +import { VerdaccioRegistry, bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { closeSync, openSync } from "node:fs"; import { join } from "path"; // Registry: no-deps 1.0.0/1.0.1/1.1.0/2.0.0, a-dep 1.0.1..1.0.10, @types/no-deps 1.0.0/2.0.0, one-range-dep@1.0.0 -> no-deps ^1.0.0, one-fixed-dep@1.0.0 -> no-deps 1.0.0, dep-with-tags latest=3.0.0, pre-2=2.0.1, 3.0.1 published above latest. @@ -1839,6 +1840,33 @@ test.concurrent( }, ); +test.concurrent.skipIf(isWindows)( + "`bun outdated` still exits 0 when the manifest warning cannot be written to stderr", + async () => { + const knobs: RegistryKnobs = { status: {} }; + using server = await serveRegistry(TAGGED_FREE, {}, knobs); + const dir = await staleDirectLeaf(server, { leaf: "optionalDependencies" }); + knobs.status!.leaf = 502; + // A read-only descriptor as stderr makes the warning's write(2) fail with EBADF. + const stderrFd = openSync("/dev/null", "r"); + try { + await using proc = Bun.spawn({ + cmd: [bunExe(), "outdated"], + cwd: dir, + env: { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(dir, ".bun-cache") }, + stdout: "pipe", + stderr: stderrFd, + stdin: "ignore", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expectHeaderOnly(stdout, "outdated"); + expect(exitCode).toBe(0); + } finally { + closeSync(stderrFd); + } + }, +); + test.concurrent( "a 502 for a direct dependency's manifest fails `bun update -i` instead of offering nothing", async () => { diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index a22306c4108d..82c5806a73b4 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -1,7 +1,7 @@ import { spawnSync } from "bun"; import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, isWindows, tempDir, tempDirWithFiles } from "harness"; -import { existsSync, symlinkSync } from "node:fs"; +import { closeSync, existsSync, openSync, symlinkSync } from "node:fs"; import { setTimeout as sleep } from "node:timers/promises"; import { join } from "path"; @@ -1210,3 +1210,32 @@ describe("output timing", () => { expect(Date.now() - start).toBeLessThan(15000); }); }); + +test.skipIf(isWindows)("--parallel keeps running the scripts when stdout cannot be written", async () => { + using dir = tempDir("parallel-ebadf", { + "package.json": JSON.stringify({ + name: "root", + scripts: { a: "echo a-out && echo a-err 1>&2", b: "echo b-out && echo b-err 1>&2" }, + }), + }); + // A read-only descriptor as stdout makes the parent's write(2) fail with EBADF. + const stdoutFd = openSync("/dev/null", "r"); + try { + await using proc = Bun.spawn({ + cmd: [bunExe(), "run", "--parallel", "a", "b"], + cwd: String(dir), + env: bunEnv, + stdin: "ignore", + stdout: stdoutFd, + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + // Both scripts ran to completion and reported their status on stderr. + expect(stderr).toContain("a-err"); + expect(stderr).toContain("b-err"); + expect(stderr.match(/Done in/g)?.length).toBe(2); + expect(exitCode).toBe(0); + } finally { + closeSync(stdoutFd); + } +}); diff --git a/test/cli/test/coverage.test.ts b/test/cli/test/coverage.test.ts index 49abf8315fd4..49c7ff8bfb3a 100644 --- a/test/cli/test/coverage.test.ts +++ b/test/cli/test/coverage.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness"; -import { readFileSync } from "node:fs"; +import { bunEnv, bunExe, isWindows, normalizeBunSnapshot, tempDir } from "harness"; +import { closeSync, openSync, readFileSync } from "node:fs"; import path from "path"; test("coverage crash", () => { @@ -641,3 +641,32 @@ test("only imports the function", () => { expect(stderr).toMatch(/ subject\.ts +\| +100\.00 +\| +100\.00 +\| +\n/); expect(exitCode).toBe(0); }); + +test.skipIf(isWindows)("coverage still writes lcov and fails the threshold when stderr cannot be written", async () => { + using dir = tempDir("cov-ebadf", { + "demo.ts": `export function covered() { return 1; }\nexport function uncovered() { return 2; }\ncovered();\n`, + "demo.test.ts": `import { test, expect } from "bun:test"; import { covered } from "./demo"; test("x", () => { expect(covered()).toBe(1); });`, + "bunfig.toml": `[test]\ncoverageThreshold = 0.99\n`, + }); + // A read-only descriptor as stderr makes the coverage table's write(2) fail with EBADF. + const stderrFd = openSync("/dev/null", "r"); + try { + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "--coverage", "--coverage-reporter=text", "--coverage-reporter=lcov"], + cwd: String(dir), + env: bunEnv, + stdin: "ignore", + stdout: "pipe", + stderr: stderrFd, + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + const lcov = readFileSync(path.join(String(dir), "coverage", "lcov.info"), "utf-8"); + expect(lcov).toContain("SF:demo.ts"); + // Only the version banner goes to stdout; the table went to the dead stderr. + expect(stdout.trim()).toMatch(/^bun test v\S+ \([0-9a-f]+\)$/); + // The threshold miss is still reported through the exit code. + expect(exitCode).toBe(1); + } finally { + closeSync(stderrFd); + } +}); diff --git a/test/js/web/console/console-log.test.ts b/test/js/web/console/console-log.test.ts index 4356ae993262..b266bc16ed38 100644 --- a/test/js/web/console/console-log.test.ts +++ b/test/js/web/console/console-log.test.ts @@ -1,6 +1,7 @@ import { file, spawn } from "bun"; import { expect, it } from "bun:test"; -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, isWindows } from "harness"; +import { closeSync, openSync } from "node:fs"; import { join } from "node:path"; it("should log to console correctly", async () => { @@ -143,6 +144,38 @@ NamedError: console.error a named error `); }); +it.skipIf(isWindows)("console.log gives up quietly when stdout cannot be written", async () => { + // Each value formats to more than the 4 KB console buffer, so the write + // fails part-way through printing it. + const code = /* js */ ` + const s = Buffer.alloc(64, "x").toString(); + const entries = Array.from({ length: 200 }, (_, i) => ["k" + i, s]); + console.log(new Map(entries)); + console.log(new Set(entries.map(e => e.join("")))); + console.log(entries.map(e => e[1])); + console.log(Object.fromEntries(entries)); + console.log(new MessageEvent("message", { data: Buffer.alloc(8192, "d").toString() })); + console.log(new Request("http://example.com/" + Buffer.alloc(8192, "p").toString())); + console.error("done"); + `; + // A read-only descriptor as stdout makes the child's write(2) fail with EBADF. + const stdoutFd = openSync("/dev/null", "r"); + try { + await using proc = spawn({ + cmd: [bunExe(), "-e", code], + env: bunEnv, + stdin: "ignore", + stdout: stdoutFd, + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(stderr).toBe("done\n"); + expect(exitCode).toBe(0); + } finally { + closeSync(stdoutFd); + } +}); + it("console.log with SharedArrayBuffer", () => { // console.log(x) === Bun.inspect(x) + "\n" written to stdout. expect(Bun.inspect(new ArrayBuffer(0))).toBe("ArrayBuffer(0) []");