Use nightly rust for formatting to group import statements

This commit is contained in:
Nick Groenen 2024-08-03 13:38:23 +02:00
parent bd1220028b
commit da9238a78c
No known key found for this signature in database
GPG Key ID: 4F0AD019928AE098
13 changed files with 103 additions and 83 deletions

View File

@ -19,7 +19,7 @@ jobs:
strategy: strategy:
matrix: matrix:
job: job:
- cargo fmt --all -- --check - rustup toolchain install nightly --profile minimal --component rustfmt && cargo +nightly fmt --all -- --check
- cargo test --all-targets --all-features - cargo test --all-targets --all-features
- cargo clippy --all-targets --all-features -- -D warning - cargo clippy --all-targets --all-features -- -D warning
fail-fast: false fail-fast: false
@ -54,7 +54,7 @@ jobs:
needs: populate-rust-cache needs: populate-rust-cache
env: env:
# These hooks are expensive and already run as dedicated jobs above # These hooks are expensive and already run as dedicated jobs above
SKIP: "tests,clippy" SKIP: "rustfmt,tests,clippy"
steps: steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4

View File

@ -15,7 +15,7 @@ repos:
hooks: hooks:
- id: rustfmt - id: rustfmt
name: Check formatting name: Check formatting
entry: cargo fmt -- entry: cargo +nightly fmt --
language: system language: system
files: \.rs$ files: \.rs$
- id: tests - id: tests

View File

@ -35,9 +35,11 @@ You can see some examples of this in:
## Conventions ## Conventions
Code is formatted with [rustfmt](https://github.com/rust-lang/rustfmt) using the default options. Code is formatted with [rustfmt](https://github.com/rust-lang/rustfmt).
In addition, all default [clippy](https://github.com/rust-lang/rust-clippy) checks on the latest stable Rust compiler must also pass. The nightly toolchain is used for this as things like sorting of imports is not yet available on stable yet.
Both of these are enforced through CI using GitHub actions. If you don't have the nightly toolchain installed, run: `rustup toolchain install nightly --component rustfmt`
In addition, [clippy](https://github.com/rust-lang/rust-clippy) is configured to be quite pedantic and all of its checks must also pass for CI builds to succeed.
> **💡 Tip: install pre-commit hooks** > **💡 Tip: install pre-commit hooks**
> >

7
rustfmt.toml Normal file
View File

@ -0,0 +1,7 @@
comment_width = 120
format_code_in_doc_comments = true
group_imports = "StdExternalCrate"
imports_granularity = "Module"
imports_layout = "HorizontalVertical"
use_field_init_shorthand = true
wrap_comments = true

View File

@ -1,6 +1,7 @@
use crate::Frontmatter;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::Frontmatter;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
/// Context holds metadata about a note which is being parsed. /// Context holds metadata about a note which is being parsed.
/// ///
@ -36,10 +37,9 @@ pub struct Context {
/// # let mut context = Context::new(PathBuf::from("source"), PathBuf::from("destination")); /// # let mut context = Context::new(PathBuf::from("source"), PathBuf::from("destination"));
/// let key = Value::String("foo".to_string()); /// let key = Value::String("foo".to_string());
/// ///
/// context.frontmatter.insert( /// context
/// key.clone(), /// .frontmatter
/// Value::String("bar".to_string()), /// .insert(key.clone(), Value::String("bar".to_string()));
/// );
/// ``` /// ```
pub frontmatter: Frontmatter, pub frontmatter: Frontmatter,
} }

View File

@ -2,8 +2,8 @@ use serde_yaml::Result;
/// YAML front matter from an Obsidian note. /// YAML front matter from an Obsidian note.
/// ///
/// This is essentially an alias of [`serde_yaml::Mapping`] so all the methods available on that type /// This is essentially an alias of [`serde_yaml::Mapping`] so all the methods available on that
/// are available with `Frontmatter` as well. /// type are available with `Frontmatter` as well.
/// ///
/// # Examples /// # Examples
/// ///
@ -14,10 +14,7 @@ use serde_yaml::Result;
/// let mut frontmatter = Frontmatter::new(); /// let mut frontmatter = Frontmatter::new();
/// let key = Value::String("foo".to_string()); /// let key = Value::String("foo".to_string());
/// ///
/// frontmatter.insert( /// frontmatter.insert(key.clone(), Value::String("bar".to_string()));
/// key.clone(),
/// Value::String("bar".to_string()),
/// );
/// ///
/// assert_eq!( /// assert_eq!(
/// frontmatter.get(&key), /// frontmatter.get(&key),
@ -67,10 +64,11 @@ pub enum FrontmatterStrategy {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_yaml::Value; use serde_yaml::Value;
use super::*;
#[test] #[test]
fn empty_string_should_yield_empty_frontmatter() { fn empty_string_should_yield_empty_frontmatter() {
assert_eq!(frontmatter_from_str("").unwrap(), Frontmatter::new()); assert_eq!(frontmatter_from_str("").unwrap(), Frontmatter::new());

View File

@ -1,5 +1,4 @@
pub use pulldown_cmark; pub use {pulldown_cmark, serde_yaml};
pub use serde_yaml;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
@ -10,11 +9,16 @@ pub mod postprocessors;
mod references; mod references;
mod walker; mod walker;
pub use context::Context; use std::ffi::OsString;
pub use frontmatter::{Frontmatter, FrontmatterStrategy}; use std::fs::{self, File};
pub use walker::{vault_contents, WalkOptions}; use std::io::prelude::*;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::{fmt, str};
pub use context::Context;
use frontmatter::{frontmatter_from_str, frontmatter_to_str}; use frontmatter::{frontmatter_from_str, frontmatter_to_str};
pub use frontmatter::{Frontmatter, FrontmatterStrategy};
use pathdiff::diff_paths; use pathdiff::diff_paths;
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag}; use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag};
@ -23,14 +27,8 @@ use rayon::prelude::*;
use references::{ObsidianNoteReference, RefParser, RefParserState, RefType}; use references::{ObsidianNoteReference, RefParser, RefParserState, RefType};
use slug::slugify; use slug::slugify;
use snafu::{ResultExt, Snafu}; use snafu::{ResultExt, Snafu};
use std::ffi::OsString;
use std::fmt;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::str;
use unicode_normalization::UnicodeNormalization; use unicode_normalization::UnicodeNormalization;
pub use walker::{vault_contents, WalkOptions};
/// A series of markdown [Event]s that are generated while traversing an Obsidian markdown note. /// A series of markdown [Event]s that are generated while traversing an Obsidian markdown note.
pub type MarkdownEvents<'a> = Vec<Event<'a>>; pub type MarkdownEvents<'a> = Vec<Event<'a>>;
@ -38,11 +36,12 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// A post-processing function that is to be called after an Obsidian note has been fully parsed and /// A post-processing function that is to be called after an Obsidian note has been fully parsed and
/// converted to regular markdown syntax. /// converted to regular markdown syntax.
/// ///
/// Postprocessors are called in the order they've been added through [`Exporter::add_postprocessor`] /// Postprocessors are called in the order they've been added through
/// just before notes are written out to their final destination. /// [`Exporter::add_postprocessor`] just before notes are written out to their final destination.
/// They may be used to achieve the following: /// They may be used to achieve the following:
/// ///
/// 1. Modify a note's [Context], for example to change the destination filename or update its [Frontmatter] (see [`Context::frontmatter`]). /// 1. Modify a note's [Context], for example to change the destination filename or update its
/// [Frontmatter] (see [`Context::frontmatter`]).
/// 2. Change a note's contents by altering [`MarkdownEvents`]. /// 2. Change a note's contents by altering [`MarkdownEvents`].
/// 3. Prevent later postprocessors from running ([`PostprocessorResult::StopHere`]) or cause a note /// 3. Prevent later postprocessors from running ([`PostprocessorResult::StopHere`]) or cause a note
/// to be skipped entirely ([`PostprocessorResult::StopAndSkipNote`]). /// to be skipped entirely ([`PostprocessorResult::StopAndSkipNote`]).
@ -64,8 +63,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// replaced with a blank document) but doesn't affect the root note. /// replaced with a blank document) but doesn't affect the root note.
/// ///
/// It's possible to pass the same functions to [`Exporter::add_postprocessor`] and /// It's possible to pass the same functions to [`Exporter::add_postprocessor`] and
/// [`Exporter::add_embed_postprocessor`]. The [`Context::note_depth`] method may be used to determine /// [`Exporter::add_embed_postprocessor`]. The [`Context::note_depth`] method may be used to
/// whether a note is a root note or an embedded note in this situation. /// determine whether a note is a root note or an embedded note in this situation.
/// ///
/// # Examples /// # Examples
/// ///
@ -104,8 +103,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// ///
/// ## Change note contents /// ## Change note contents
/// ///
/// In this example a note's markdown content is changed by iterating over the [`MarkdownEvents`] and /// In this example a note's markdown content is changed by iterating over the [`MarkdownEvents`]
/// changing the text when we encounter a [text element][Event::Text]. /// and changing the text when we encounter a [text element][Event::Text].
/// ///
/// Instead of using a closure like above, this example shows how to use a separate function /// Instead of using a closure like above, this example shows how to use a separate function
/// definition. /// definition.
@ -282,8 +281,9 @@ impl<'a> Exporter<'a> {
/// Set a custom starting point for the export. /// Set a custom starting point for the export.
/// ///
/// Normally all notes under `root` (except for notes excluded by ignore rules) will be exported. /// Normally all notes under `root` (except for notes excluded by ignore rules) will be
/// When `start_at` is set, only notes under this path will be exported to the target destination. /// exported. When `start_at` is set, only notes under this path will be exported to the
/// target destination.
pub fn start_at(&mut self, start_at: PathBuf) -> &mut Self { pub fn start_at(&mut self, start_at: PathBuf) -> &mut Self {
self.start_at = start_at; self.start_at = start_at;
self self
@ -305,7 +305,8 @@ impl<'a> Exporter<'a> {
/// ///
/// When `recursive` is true (the default), emdeds are always processed recursively. This may /// When `recursive` is true (the default), emdeds are always processed recursively. This may
/// lead to infinite recursion when note A embeds B, but B also embeds A. /// lead to infinite recursion when note A embeds B, but B also embeds A.
/// (When this happens, [`ExportError::RecursionLimitExceeded`] will be returned by [`Exporter::run`]). /// (When this happens, [`ExportError::RecursionLimitExceeded`] will be returned by
/// [`Exporter::run`]).
/// ///
/// When `recursive` is false, if a note is encountered for a second time while processing the /// When `recursive` is false, if a note is encountered for a second time while processing the
/// original note, instead of embedding it again a link to the note is inserted instead. /// original note, instead of embedding it again a link to the note is inserted instead.
@ -314,7 +315,8 @@ impl<'a> Exporter<'a> {
self self
} }
/// Append a function to the chain of [postprocessors][Postprocessor] to run on exported Obsidian Markdown notes. /// Append a function to the chain of [postprocessors][Postprocessor] to run on exported
/// Obsidian Markdown notes.
pub fn add_postprocessor(&mut self, processor: &'a Postprocessor<'_>) -> &mut Self { pub fn add_postprocessor(&mut self, processor: &'a Postprocessor<'_>) -> &mut Self {
self.postprocessors.push(processor); self.postprocessors.push(processor);
self self
@ -718,8 +720,7 @@ impl<'a> Exporter<'a> {
/// ///
/// 1. Standard Obsidian note references not including a .md extension. /// 1. Standard Obsidian note references not including a .md extension.
/// 2. Case-insensitive matching /// 2. Case-insensitive matching
/// 3. Unicode normalization rules using normalization form C /// 3. Unicode normalization rules using normalization form C (<https://www.w3.org/TR/charmod-norm/#unicodeNormalization>)
/// (<https://www.w3.org/TR/charmod-norm/#unicodeNormalization>)
fn lookup_filename_in_vault<'a>( fn lookup_filename_in_vault<'a>(
filename: &str, filename: &str,
vault_contents: &'a [PathBuf], vault_contents: &'a [PathBuf],
@ -897,10 +898,11 @@ fn codeblock_kind_to_owned<'a>(codeblock_kind: CodeBlockKind<'_>) -> CodeBlockKi
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest; use rstest::rstest;
use super::*;
lazy_static! { lazy_static! {
static ref VAULT: Vec<PathBuf> = vec![ static ref VAULT: Vec<PathBuf> = vec![
PathBuf::from("NoteA.md"), PathBuf::from("NoteA.md"),

View File

@ -1,8 +1,10 @@
use std::env;
use std::path::PathBuf;
use eyre::{eyre, Result}; use eyre::{eyre, Result};
use gumdrop::Options; use gumdrop::Options;
use obsidian_export::postprocessors::{filter_by_tags, softbreaks_to_hardbreaks}; use obsidian_export::postprocessors::{filter_by_tags, softbreaks_to_hardbreaks};
use obsidian_export::{ExportError, Exporter, FrontmatterStrategy, WalkOptions}; use obsidian_export::{ExportError, Exporter, FrontmatterStrategy, WalkOptions};
use std::{env, path::PathBuf};
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@ -1,9 +1,10 @@
//! A collection of officially maintained [postprocessors][crate::Postprocessor]. //! A collection of officially maintained [postprocessors][crate::Postprocessor].
use super::{Context, MarkdownEvents, PostprocessorResult};
use pulldown_cmark::Event; use pulldown_cmark::Event;
use serde_yaml::Value; use serde_yaml::Value;
use super::{Context, MarkdownEvents, PostprocessorResult};
/// This postprocessor converts all soft line breaks to hard line breaks. Enabling this mimics /// This postprocessor converts all soft line breaks to hard line breaks. Enabling this mimics
/// Obsidian's _'Strict line breaks'_ setting. /// Obsidian's _'Strict line breaks'_ setting.
pub fn softbreaks_to_hardbreaks( pub fn softbreaks_to_hardbreaks(

View File

@ -1,6 +1,7 @@
use regex::Regex;
use std::fmt; use std::fmt;
use regex::Regex;
lazy_static! { lazy_static! {
static ref OBSIDIAN_NOTE_LINK_RE: Regex = static ref OBSIDIAN_NOTE_LINK_RE: Regex =
Regex::new(r"^(?P<file>[^#|]+)??(#(?P<section>.+?))??(\|(?P<label>.+?))??$").unwrap(); Regex::new(r"^(?P<file>[^#|]+)??(#(?P<section>.+?))??(\|(?P<label>.+?))??$").unwrap();

View File

@ -1,9 +1,11 @@
use crate::{ExportError, WalkDirSnafu};
use ignore::{DirEntry, Walk, WalkBuilder};
use snafu::ResultExt;
use std::fmt; use std::fmt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use ignore::{DirEntry, Walk, WalkBuilder};
use snafu::ResultExt;
use crate::{ExportError, WalkDirSnafu};
type Result<T, E = ExportError> = std::result::Result<T, E>; type Result<T, E = ExportError> = std::result::Result<T, E>;
type FilterFn = dyn Fn(&DirEntry) -> bool + Send + Sync + 'static; type FilterFn = dyn Fn(&DirEntry) -> bool + Send + Sync + 'static;

View File

@ -1,16 +1,16 @@
#![allow(clippy::shadow_unrelated)] #![allow(clippy::shadow_unrelated)]
use std::fs::{create_dir, read_to_string, set_permissions, File, Permissions};
use std::io::prelude::*;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use obsidian_export::{ExportError, Exporter, FrontmatterStrategy}; use obsidian_export::{ExportError, Exporter, FrontmatterStrategy};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::fs::{create_dir, read_to_string, set_permissions, File, Permissions};
use std::io::prelude::*;
use std::path::PathBuf;
use tempfile::TempDir; use tempfile::TempDir;
use walkdir::WalkDir; use walkdir::WalkDir;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::PermissionsExt;
#[test] #[test]
fn test_main_variants_with_default_options() { fn test_main_variants_with_default_options() {
let tmp_dir = TempDir::new().expect("failed to make tempdir"); let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -262,9 +262,9 @@ fn test_not_existing_destination_with_source_dir() {
} }
#[test] #[test]
// This test ensures that when source is a file, but destination points to a regular file // This test ensures that when source is a file, but destination points to a
// inside of a non-existent directory, an error is raised instead of that directory path being // regular file inside of a non-existent directory, an error is raised instead
// created (like `mkdir -p`) // of that directory path being created (like `mkdir -p`)
fn test_not_existing_destination_with_source_file() { fn test_not_existing_destination_with_source_file() {
let tmp_dir = TempDir::new().expect("failed to make tempdir"); let tmp_dir = TempDir::new().expect("failed to make tempdir");

View File

@ -1,16 +1,18 @@
use std::collections::HashSet;
use std::fs::{read_to_string, remove_file};
use std::path::PathBuf;
use std::sync::Mutex;
use obsidian_export::postprocessors::{filter_by_tags, softbreaks_to_hardbreaks}; use obsidian_export::postprocessors::{filter_by_tags, softbreaks_to_hardbreaks};
use obsidian_export::{Context, Exporter, MarkdownEvents, PostprocessorResult}; use obsidian_export::{Context, Exporter, MarkdownEvents, PostprocessorResult};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use pulldown_cmark::{CowStr, Event}; use pulldown_cmark::{CowStr, Event};
use serde_yaml::Value; use serde_yaml::Value;
use std::collections::HashSet;
use std::fs::{read_to_string, remove_file};
use std::path::PathBuf;
use std::sync::Mutex;
use tempfile::TempDir; use tempfile::TempDir;
use walkdir::WalkDir; use walkdir::WalkDir;
/// This postprocessor replaces any instance of "foo" with "bar" in the note body. /// This postprocessor replaces any instance of "foo" with "bar" in the note
/// body.
fn foo_to_bar(_ctx: &mut Context, events: &mut MarkdownEvents<'_>) -> PostprocessorResult { fn foo_to_bar(_ctx: &mut Context, events: &mut MarkdownEvents<'_>) -> PostprocessorResult {
for event in events.iter_mut() { for event in events.iter_mut() {
if let Event::Text(text) = event { if let Event::Text(text) = event {
@ -27,9 +29,9 @@ fn append_frontmatter(ctx: &mut Context, _events: &mut MarkdownEvents<'_>) -> Po
PostprocessorResult::Continue PostprocessorResult::Continue
} }
// The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend // The purpose of this test to verify the `append_frontmatter` postprocessor is
// the frontmatter, and the `foo_to_bar` postprocessor is called to replace instances of "foo" with // called to extend the frontmatter, and the `foo_to_bar` postprocessor is
// "bar" (only in the note body). // called to replace instances of "foo" with "bar" (only in the note body).
#[test] #[test]
fn test_postprocessors() { fn test_postprocessors() {
let tmp_dir = TempDir::new().expect("failed to make tempdir"); let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -106,8 +108,8 @@ fn test_postprocessor_change_destination() {
assert!(new_note_path.exists()); assert!(new_note_path.exists());
} }
// Ensure postprocessor type definition has proper lifetimes to allow state (here: `parents`) // Ensure postprocessor type definition has proper lifetimes to allow state
// to be passed in. Otherwise, this fails with an error like: // (here: `parents`) to be passed in. Otherwise, this fails with an error like:
// error[E0597]: `parents` does not live long enough // error[E0597]: `parents` does not live long enough
// cast requires that `parents` is borrowed for `'static` // cast requires that `parents` is borrowed for `'static`
#[test] #[test]
@ -139,9 +141,9 @@ fn test_postprocessor_stateful_callback() {
assert!(parents.contains(expected)); assert!(parents.contains(expected));
} }
// The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend // The purpose of this test to verify the `append_frontmatter` postprocessor is
// the frontmatter, and the `foo_to_bar` postprocessor is called to replace instances of "foo" with // called to extend the frontmatter, and the `foo_to_bar` postprocessor is
// "bar" (only in the note body). // called to replace instances of "foo" with "bar" (only in the note body).
#[test] #[test]
fn test_embed_postprocessors() { fn test_embed_postprocessors() {
let tmp_dir = TempDir::new().expect("failed to make tempdir"); let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -162,8 +164,8 @@ fn test_embed_postprocessors() {
assert_eq!(expected, actual); assert_eq!(expected, actual);
} }
// When StopAndSkipNote is used with an embed_preprocessor, it should skip the embedded note but // When StopAndSkipNote is used with an embed_preprocessor, it should skip the
// continue with the rest of the note. // embedded note but continue with the rest of the note.
#[test] #[test]
fn test_embed_postprocessors_stop_and_skip() { fn test_embed_postprocessors_stop_and_skip() {
let tmp_dir = TempDir::new().expect("failed to make tempdir"); let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -182,9 +184,10 @@ fn test_embed_postprocessors_stop_and_skip() {
assert_eq!(expected, actual); assert_eq!(expected, actual);
} }
// This test verifies that the context which is passed to an embed postprocessor is actually // This test verifies that the context which is passed to an embed postprocessor
// correct. Primarily, this means the frontmatter should reflect that of the note being embedded as // is actually correct. Primarily, this means the frontmatter should reflect
// opposed to the frontmatter of the root note. // that of the note being embedded as opposed to the frontmatter of the root
// note.
#[test] #[test]
#[allow(clippy::manual_assert)] #[allow(clippy::manual_assert)]
fn test_embed_postprocessors_context() { fn test_embed_postprocessors_context() {
@ -203,9 +206,10 @@ fn test_embed_postprocessors_context() {
.get(&Value::String("is_root_note".into())) .get(&Value::String("is_root_note".into()))
.unwrap(); .unwrap();
if is_root_note != &Value::Bool(true) { if is_root_note != &Value::Bool(true) {
// NOTE: Test failure may not give output consistently because the test binary affects // NOTE: Test failure may not give output consistently because the test binary
// how output is captured and printed in the thread running this postprocessor. Just // affects how output is captured and printed in the thread running
// run the test a couple times until the error shows up. // this postprocessor. Just run the test a couple times until the
// error shows up.
panic!( panic!(
"postprocessor: expected is_root_note in {} to be true, got false", "postprocessor: expected is_root_note in {} to be true, got false",
&ctx.current_file().display() &ctx.current_file().display()
@ -219,9 +223,10 @@ fn test_embed_postprocessors_context() {
.get(&Value::String("is_root_note".into())) .get(&Value::String("is_root_note".into()))
.unwrap(); .unwrap();
if is_root_note == &Value::Bool(true) { if is_root_note == &Value::Bool(true) {
// NOTE: Test failure may not give output consistently because the test binary affects // NOTE: Test failure may not give output consistently because the test binary
// how output is captured and printed in the thread running this postprocessor. Just // affects how output is captured and printed in the thread running
// run the test a couple times until the error shows up. // this postprocessor. Just run the test a couple times until the
// error shows up.
panic!( panic!(
"embed_postprocessor: expected is_root_note in {} to be false, got true", "embed_postprocessor: expected is_root_note in {} to be false, got true",
&ctx.current_file().display() &ctx.current_file().display()