I need to customize the `assert` fail message in my project, but it
doesn't seems to work except the error message is a string literal.
Minimal demo code:
```hare
@test fn just_a_test() void = {
const expected_hex_string = "FAB0AA000000CAFA";
const assert_err_buf: [1024]u8 = [0x00...];
const assert_err_msg = fmt::bsprintf(
&assert_err_buf,
"encoded frame bytes not equal to '{}'",
expected_hex_string);
// fmt::printfln(">>> assert_err_msg: {}", assert_err_msg)!;
assert("FAB0AA000000CA" == expected_hex_string, assert_err_msg);
};
```
Output:
```bash
Running 1/1 tests:
::just_a_test...FAIL in 0.000012761s
Failures:
::just_a_test: /home/wison/hare/hare-temp/src/main.ha:136:15: q}TaYU]UaYUq
```
I expected the `q}TaYU]UaYUq` should be `encoded frame bytes not equal
to 'FAB0AA000000CAFA'`
Is it a bug or something I did in the wrong way?:)
The short answer is that in my opinion assert should only accept string
literals (or rather, translation-compatible expressions), and in fact I
started writing a patch to this effect yesterday.
The long answer is that you have a stack escape bug here. In the test
context, an assertion failure stores the assertion message string and
longjmps back to the test harness code, which does some bookkeeping and
prints it later. Your assert_err_buf is stack allocated so this string
escapes the stack when assert longjmp's and is eventually mangled.
Ok, thanks for the quick reply, very appreciated:) 2 things:
1. Yes, if I allocate `assert_err_buf` on the heap, it works:
Minimal demo code:
```hare
@test fn just_a_test() void = {
const expected_hex_string = "FAB0AA000000CAFA";
let assert_err_buf: memio::stream = memio::dynamic();
// Can't close it, as it needs to live outside the function scope.
// defer io::close(&assert_err_buf)!;
fmt::fprintf(
&assert_err_buf,
"encoded frame bytes not equal to '{}'",
expected_hex_string)!;
const assert_err_msg = memio::string(&assert_err_buf)!;
// fmt::printfln("{}", assert_err_msg)!;
assert("FAB0AA000000CA" == expected_hex_string, assert_err_msg);
};
```
2. About the `assert should only accept string literal`, I agree
basically but it doesn't fit my need in the following situations:
I'm working on a custom protocol on top of TCP, that says I need to
decode "XXXXXXXXXXXXXXXX" (bytes) back to a complicated struct
instance and encode the struct instance to hex bytes.
I wrote a 700 lines long test case for the decode case (already
separated into different files in the "+test/" folder), as a lot of
situations need to be covered.
Then I rewrote the test code into some reusable function like this to
remove the duplicated boilerplate (parse HEX, decode frame, match
stuff and assertions):
```hare
fn test_decode_frame(
frame_str: const str,
expected_command: ucp::Command,
expected_algo_type: ucp::AlgorithmType,
expected_key_len: u16,
expected_key_body: (void | []const u8),
expected_body_len: u16,
expected_body: (void | ExpectedBody),
expected_crc: u8,
expected_frame_begin_index: i16,
expected_frame_end_index: i16,
) void = {
match (hex::hex_from_string(frame_str)) {
case let buf: []u8 =>
defer free(buf);
const frame = decode_frame(buf, 0);
match (frame) {
case let f: ucp::Frame =>
debug_print(&f);
//
// A lot of `assert()` from here (30 lines)
//
case let e: DecodeError =>
fmt::printfln(
"\n>>> frame error: {}",
ucp::strerror(e.frame_error)
)!;
};
assert(frame is ucp::Frame);
case hex::ParseHexError => void;
};
};
```
So, I can write test code like this:
```hare
@test fn decode_a_single_frame_should_work() void = {
test_decode_frame(
"FAB1AA000600FFFF02002C01CAFB",
ucp::Command::HeartbeatSetting, // expected_command
ucp::AlgorithmType::PlainText, // expected_algo_type
0, // expected_key_len
void, // expected_key_body
6, // expected_body_len
ExpectedBody { // expected_body
project_id = 0xFFFF,
project_data_len = 2,
project_body_payload = [0x2C, 0x01],
},
0xCA, // expected_crc
0, // expected_frame_begin_index
13, // expected_frame_end_index
);
//
// Repeat the `test_decode_frame` call with all possible protocols (
// around 50 cases)
//
test_decode_frame(...);
......
};
```
Now, my problem shows up: When I modify the protocol implementation,
re-run all test functions, only some of them fail, and I DON'T KNOW
which one failed!!!
As `hare test` only tells me which `assert()` fail (e.g.
`assert(f.body_len == expected_body_len);` this line fails), I have no
idea which "XXXXX" parameter causes to fail, that increase my debug
time (a lot).
That's why I want to add customized `assert_err_message` to the
`assert()` to help me locate the fail case without printing a lot of
extra information:)
On Sun, Dec 29, 2024 at 10:52 PM Drew DeVault <sir@cmpwn.com> wrote:
>
> The short answer is that in my opinion assert should only accept string
> literals (or rather, translation-compatible expressions), and in fact I
> started writing a patch to this effect yesterday.
>
> The long answer is that you have a stack escape bug here. In the test
> context, an assertion failure stores the assertion message string and
> longjmps back to the test harness code, which does some bookkeeping and
> prints it later. Your assert_err_buf is stack allocated so this string
> escapes the stack when assert longjmp's and is eventually mangled.
On Sun Dec 29, 2024 at 10:39 PM CET, Ye Wison wrote:
> As `hare test` only tells me which `assert()` fail (e.g.
> `assert(f.body_len == expected_body_len);` this line fails), I have no
> idea which "XXXXX" parameter causes to fail, that increase my debug
> time (a lot).
>
> That's why I want to add customized `assert_err_message` to the
> `assert()` to help me locate the fail case without printing a lot of
> extra information:)
The solution I've been leaning towards recommending in this case is to
decorate your tests with fmt::print(f)ln to add some logging. The
stdout/stderr of your test function is captured by the test driver and
included in the test summary when the tests fail.