1

I have following struct and want to test implementation of Display trait:

use core::fmt::{Display, Formatter, Result};

struct B {}

impl Display for B {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "A {} C", "B")
    }
}

#[test]
fn it_works() {
    assert_eq!(format!("{}", B {}), "A B C")
}

it works in general case, however in #![no_std] environment it yields an error due to the fact that format! macro is missing (it allocates, so cannot be used in no_std).

Is there idiomatic way to test core::std::Display trait in no_std scenarios? I've tried to create my own core::std::Formatter instance with custom buffer implementation to somehow circumvent it, and test fmt method directly, but instancing this type is considered compiler internal.

  • Don't you have `alloc` available? Don't you have some string type you can use? – Chayim Friedman Jun 23 '22 at 09:17
  • I don't want to use `alloc`, I have gut hunch that it should be possible without `alloc`. – Michał Zabielski Jun 23 '22 at 09:20
  • You can use a [`Cursor`](https://doc.rust-lang.org/1.54.0/std/io/struct.Cursor.html) to write into a static buffer then assert that the buffer contains what you expect. – Jmb Jun 23 '22 at 09:29
  • You'll probably have to implement a custom writer that writes to fixed length buffer, see https://stackoverflow.com/questions/50200268/how-can-i-use-the-format-macro-in-a-no-std-environment. – Dogbert Jun 23 '22 at 09:29
  • Turns out it's possible to do without any buffer at all. See my answer – Michał Zabielski Jun 23 '22 at 10:37

3 Answers3

2

Why not just implement core::fmt::Write on your custom buffer and use core::fmt::write! to it?

format! is basically a write! to a string buffer, both really call Write::write_fmt, with format! having the conveniences that it provides its own (string) buffer and panics on error.

macro_rules! write {
    ($dst:expr, $($arg:tt)*) => {
        $dst.write_fmt($crate::format_args!($($arg)*))
    };
}
macro_rules! format {
    ($($arg:tt)*) => {{
        let res = $crate::fmt::format($crate::__export::format_args!($($arg)*));
        res
    }}
}
pub fn format(args: Arguments<'_>) -> string::String {
    let capacity = args.estimated_capacity();
    let mut output = string::String::with_capacity(capacity);
    output.write_fmt(args).expect("a formatting trait implementation returned an error");
    output
}
Masklinn
  • 34,759
  • 3
  • 38
  • 57
1

The best option is to enable std for tests:

#![cfg_attr(no_std, not(test))]

If you cannot do it, and you know an upper limit for the format size, you can implement core::fmt::Write for a u8 array wrapper and using it. The arrayvec has already got you covered with ArrayString:

let mut formatted = ArrayString::<10>::new();
use core::fmt::Write;
write!(formatted, "{}", B {}).expect("failed to format, probably buffer too small");
assert_eq!(&formatted, "A B C")

Playground.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
1

It's possible to do it without allocation, and without any buffer, just by implementing core::fmt::Write trait, as @Masklinn mentioned.

Here is sample implementation:

#![no_std]
use core::fmt::{Display, Formatter, Result, Write};

struct B {}

impl Display for B {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "A {} C", "B")
    }
}

struct Comparator<'a> {
    valid: bool,
    to_compare: &'a str
}

impl<'a> Comparator<'a> {
    fn new(s: &'a str) -> Self {
        Self { valid: true, to_compare: s }
    }

    fn is_valid(self) -> bool {
        self.valid && self.to_compare.is_empty()
    }
}


impl<'a> Write for Comparator<'a> {
    fn write_str(&mut self, s: &str) -> Result {
        if s.eq(self.to_compare) {
            self.valid = self.valid && true;
            self.to_compare = "";
            return Ok(());
        }

        if self.to_compare.starts_with(s) && self.to_compare.len() >= s.len() {
            self.to_compare = &self.to_compare[s.len()..];
        } else {
            self.valid = false
        }
        Ok(())
    }
}

#[test]
fn it_works() {
    let mut cmp = Comparator::new("A B C");
    let _ = write!(&mut cmp, "{}", B{});
    assert!(cmp.is_valid());
}

#[test]
fn too_short() {
    let mut cmp = Comparator::new("A B");
    let _ = write!(&mut cmp, "{}", B{});
    assert!(!cmp.is_valid());
}

#[test]
fn too_long() {
    let mut cmp = Comparator::new("A B C D");
    let _ = write!(&mut cmp, "{}", B{});
    assert!(!cmp.is_valid());
}

EDIT: fixed bug in the code, was:

 if self.to_compare.starts_with(s) && self.to_compare.len() >= s.len() {
            self.to_compare = &self.to_compare[s.len()..];
            self.valid = true
        } else {
            self.valid = false
        }

fixed:

 if self.to_compare.starts_with(s) && self.to_compare.len() >= s.len() {
            self.to_compare = &self.to_compare[s.len()..];
        } else {
            self.valid = false
        }
  • 1
    Nice idea, however I think it will give the wrong result if the test outputs garbage first followed by the right data in multiple chunks. You should probably remove all the `self.valid = true` lines, keeping only the `valid: true` in `new`. – Jmb Jun 24 '22 at 06:26
  • ‘startsWith’ function should take care of that, since then I rewrote the code, so ‘valid’ bool is no longer needed. Code is here: https://github.com/kosciej/fmt_compare_nostd – Michał Zabielski Jun 26 '22 at 06:05
  • 1
    `starts_with` isn't enough. See the `failing` test in [this playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=dc5f8ed8e58dac602310dd50286ca0dd). – Jmb Jun 26 '22 at 21:31
  • you are right, this implementation is wrong. But IMO `starts_with()` should be sufficient. The problem is in setting `self.valid = true` each time `starts_with()` yields correct result. When this line is removed, then your test passes. See [playground here](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=339197601f64dcc416c4a004a557cdc9) – Michał Zabielski Jun 27 '22 at 06:02