Which syntax is better for assertion? assert_that!(v).xx vs v.must_xx.or_fail_with(msg!())

I'm currently building a assertion library.

https://github.com/kdy1997/must

docs: must - Rust

Gist for syntax comparison: https://gist.github.com/kdy1997/c79ab89585c748429cb244e63f5d2595

Current syntax is

result.must_be_ok_and(|val| {
  val.must_be(5) // returns assertion
}.or(fail!("lazy {}", "eval"));
// .or is optional and used to know source of panic.

// with customization (lazy evaluation is for this)
let parser: fn(&str) -> Result<Lit, ParseError> = parse_lit;
parser.must_parse("false").as_ok(Lit::Bool(false)); // .or(fail!()) is optional
parser.must_parse("true").as_ok(Lit::Bool(true)).or(fail!());
parser.must_parse("352").as_ok(Lit::Num(352)).or(fail!());

// below is available with current design, but not implemented in example
parser
 .must_parse("352")
 .as_ok(Lit::Num(352))
 .and_remainder("")
 .or(fail!());

Current design

  • Evaluated on drop or or(fail!()) (only once, or takes self)
  • Chaining assertion is not supported (available, but need some work)
  • Extends types directly based on trait it implements.
  • or(fail!()) is used to provide location of panic

And I'm considering changing it to assert_that!(). So before doing that, I want to hear some opinions about syntax, and know if it's available to make assert_that! optional. Then syntax would be

assert_that!(Some(5)).must_be(Some(5));
assert_that!(Some(5)).must_be_some_and(|val| {
  assert_that!(val).must_be(5)
}).with_msg("lazy evaluation");
Some(5).must_be_some_and(|val| {
  assert_that!(val).must_be(5)
});

// with customization
let parser: fn(&str) -> Result<Lit, ParseError> = parse_lit;
parser.must_parse("false").as_ok(Lit::Bool(false));
assert_that!(parser)
  .with_msg("")
  .must_parse("true")
  .as_ok(Lit::Bool(true))
  .with_remainder(""); // rustfmt

Because function name like with_msg can be used by user code, it cannot be added to other types.

and I'm considering changing name of fast-fail methods as an alternative option.

.take_or_fail(with_msg!())
.or_fail(with_msg!())

or
.take_or_fail_with(msg!()) //
.or_fail_with(msg!())
let parser: fn(&str) -> Result<Lit, ParseError> = parse_lit;
parser.must_parse("false").as_ok(Lit::Bool(false));

then syntax would be

parser
  .must_parse("true")
  .as_ok(Lit::Bool(true))
  .with_remainder("")
  .or_fail_with(msg!("")); // location of or_fail_with can be captured

What do you think about this ideas?
And which syntax do you prefer?
I mean, or_fail_with(msg!()) vs assert_that!().with_msg()

Edit: formatted source code

Edit: changed number to header as reddit render all of them as '1.'

Edit: added a link to gist

Edit: removed completely irrelevant stuffs (I can't understand why I wrote that here..)

Are you aware how py.test provides assertions (How to write and report assertions in tests — pytest documentation)?

TLDR is that you write just assert [1, 2, 3] == [1, 8, 3], and the library itself figures out that you are comparing lists and that the second element differs, so it is able to provide a nice error message. In my experience, this is hugely more convenient that hamcrest/assertj style of matches. I wonder if something like this is possible in Rust. There is no run time introspection, but there is a rich type system and macros...

2 Likes