Skip to content

Commit 423ddbc

Browse files
committed
Add tests asserting that no memory is allocated for certain use cases
Resolves #5
1 parent 7298322 commit 423ddbc

File tree

5 files changed

+317
-4
lines changed

5 files changed

+317
-4
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ criterion = { version = "0.5.1", features = ["html_reports"] }
2323
# Serde is used for comparison in benchmarks and for tests
2424
serde = "1.0.159"
2525
serde_json = "1.0.95"
26+
# Used for verifying in allocation tests that no allocations occur in certain situations
27+
# Specify Git revision because version with "backtrace" feature has not been released yet
28+
assert_no_alloc = { git = "https://github.com/Windfisch/rust-assert-no-alloc.git", rev = "d31f2d5f550ce339d1c2f0c1ab7da951224b20df", features = [
29+
"backtrace",
30+
] }
2631

2732
# docs.rs specific configuration
2833
[package.metadata.docs.rs]

src/reader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1990,7 +1990,7 @@ impl<R: Read> JsonStreamReader<R> {
19901990
/// The settings can be used to customize which JSON data the reader accepts and to allow
19911991
/// JSON data which is considered invalid by the JSON specification.
19921992
pub fn new_custom(reader: R, reader_settings: ReaderSettings) -> Self {
1993-
let initial_nesting_capacity = 32;
1993+
let initial_nesting_capacity = 16;
19941994
Self {
19951995
reader,
19961996
buf: [0; READER_BUF_SIZE],

src/writer.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ impl<W: Write> JsonStreamWriter<W> {
753753
buf_write_pos: 0,
754754
is_empty: true,
755755
expects_member_name: false,
756-
stack: Vec::new(),
756+
stack: Vec::with_capacity(16),
757757
is_string_value_writer_active: false,
758758
indentation_level: 0,
759759
writer_settings,
@@ -897,6 +897,26 @@ impl<W: Write> JsonStreamWriter<W> {
897897
}
898898

899899
fn write_escaped_char(&mut self, c: char) -> Result<(), IoError> {
900+
fn get_unicode_escape(value: u32) -> [u8; 4] {
901+
// For convenience `value` is u32, but it is actually u16
902+
debug_assert!(value <= u16::MAX as u32);
903+
904+
fn to_hex(i: u32) -> u8 {
905+
match i {
906+
0..=9 => b'0' + i as u8,
907+
10..=15 => b'A' + (i - 10) as u8,
908+
_ => unreachable!("Unexpected value {i}"),
909+
}
910+
}
911+
912+
[
913+
to_hex(value >> 12 & 15),
914+
to_hex(value >> 8 & 15),
915+
to_hex(value >> 4 & 15),
916+
to_hex(value & 15),
917+
]
918+
}
919+
900920
let escape = match c {
901921
'"' => "\\\"",
902922
'\\' => "\\\\",
@@ -907,15 +927,21 @@ impl<W: Write> JsonStreamWriter<W> {
907927
'\r' => "\\r",
908928
'\t' => "\\t",
909929
'\0'..='\u{FFFF}' => {
910-
self.write_bytes(format!("\\u{:04X}", c as u32).as_bytes())?;
930+
self.write_bytes(b"\\u")?;
931+
self.write_bytes(&get_unicode_escape(c as u32))?;
911932
return Ok(());
912933
}
913934
_ => {
914935
// Encode as surrogate pair
915936
let temp = (c as u32) - 0x10000;
916937
let high = (temp >> 10) + 0xD800;
917938
let low = (temp & ((1 << 10) - 1)) + 0xDC00;
918-
self.write_bytes(format!("\\u{:04X}\\u{:04X}", high, low).as_bytes())?;
939+
940+
self.write_bytes(b"\\u")?;
941+
self.write_bytes(&get_unicode_escape(high))?;
942+
943+
self.write_bytes(b"\\u")?;
944+
self.write_bytes(&get_unicode_escape(low))?;
919945
return Ok(());
920946
}
921947
};

tests/reader_alloc_test.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Allow `assert_eq!(true, ...)` because it is used to check a bool value and not
2+
// a 'flag' / 'state', and `assert_eq!` makes that more explicit
3+
#![allow(clippy::bool_assert_comparison)]
4+
5+
use std::error::Error;
6+
7+
use assert_no_alloc::permit_alloc;
8+
// Only use import when creating debug builds, see also configuration below
9+
#[cfg(debug_assertions)]
10+
use assert_no_alloc::AllocDisabler;
11+
use struson::{
12+
json_path,
13+
reader::{JsonReader, JsonStreamReader, ReaderSettings},
14+
writer::{JsonStreamWriter, JsonWriter},
15+
};
16+
17+
// Only enable when creating debug builds
18+
#[cfg(debug_assertions)]
19+
#[global_allocator]
20+
static A: AllocDisabler = AllocDisabler;
21+
22+
fn assert_no_alloc<F: FnOnce() -> Result<(), Box<dyn Error>>>(func: F) {
23+
assert_no_alloc::assert_no_alloc(func).unwrap()
24+
}
25+
26+
fn new_reader(json: &str) -> JsonStreamReader<&[u8]> {
27+
JsonStreamReader::new_custom(
28+
json.as_bytes(),
29+
ReaderSettings {
30+
// Disable path tracking because that causes allocations
31+
track_path: false,
32+
..Default::default()
33+
},
34+
)
35+
}
36+
37+
#[test]
38+
fn skip() {
39+
let json =
40+
r#"{"a": [{"b": 1, "c": [[], [2, {"d": 3, "e": "some string value"}]]}], "f": true}"#;
41+
let mut json_reader = new_reader(json);
42+
43+
assert_no_alloc(|| {
44+
json_reader.skip_value()?;
45+
46+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
47+
permit_alloc(|| json_reader.consume_trailing_whitespace())?;
48+
Ok(())
49+
});
50+
}
51+
52+
#[test]
53+
fn read_values() {
54+
let json = "{\"a\": [\"string\", \"\u{1234}\u{10FFFF}\", 1234.5e+6, true, false, null]}";
55+
let mut json_reader = new_reader(json);
56+
57+
assert_no_alloc(|| {
58+
json_reader.begin_object()?;
59+
assert_eq!("a", json_reader.next_name()?);
60+
json_reader.begin_array()?;
61+
62+
assert_eq!("string", json_reader.next_str()?);
63+
assert_eq!("\u{1234}\u{10FFFF}", json_reader.next_str()?);
64+
assert_eq!("1234.5e+6", json_reader.next_number_as_str()?);
65+
assert_eq!(true, json_reader.next_bool()?);
66+
assert_eq!(false, json_reader.next_bool()?);
67+
json_reader.next_null()?;
68+
69+
json_reader.end_array()?;
70+
json_reader.end_object()?;
71+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
72+
permit_alloc(|| json_reader.consume_trailing_whitespace())?;
73+
Ok(())
74+
});
75+
}
76+
77+
#[test]
78+
fn read_string_escape_sequences() {
79+
let json = r#"["\n", "\t a", "a \u1234", "a \uDBFF\uDFFF b"]"#;
80+
let mut json_reader = new_reader(json);
81+
82+
assert_no_alloc(|| {
83+
json_reader.begin_array()?;
84+
// These don't cause allocation because the internal value buffer has already
85+
// been allocated when the JSON reader was created, and is then reused
86+
assert_eq!("\n", json_reader.next_str()?);
87+
assert_eq!("\t a", json_reader.next_str()?);
88+
assert_eq!("a \u{1234}", json_reader.next_str()?);
89+
assert_eq!("a \u{10FFFF} b", json_reader.next_str()?);
90+
91+
json_reader.end_array()?;
92+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
93+
permit_alloc(|| json_reader.consume_trailing_whitespace())?;
94+
Ok(())
95+
});
96+
}
97+
98+
#[test]
99+
fn string_value_reader() -> Result<(), Box<dyn Error>> {
100+
let repetition_count = 100;
101+
let json_string_value = "\\n \\t a \\u1234 \\uDBFF\\uDFFF \u{10FFFF}".repeat(repetition_count);
102+
let expected_string_value = "\n \t a \u{1234} \u{10FFFF} \u{10FFFF}".repeat(repetition_count);
103+
let json = format!("\"{json_string_value}\"");
104+
let mut json_reader = new_reader(&json);
105+
106+
// Pre-allocate with expected size to avoid allocations during test execution
107+
let mut string_output = String::with_capacity(expected_string_value.len());
108+
109+
// Obtain this here because return value is `Box<dyn ...>` and therefore performs allocation
110+
let mut string_value_reader = json_reader.next_string_reader()?;
111+
112+
assert_no_alloc(|| {
113+
string_value_reader.read_to_string(&mut string_output)?;
114+
Ok(())
115+
});
116+
drop(string_value_reader);
117+
// TODO: Ideally would call this inside assert_no_alloc, but is not possible because `string_value_reader`
118+
// is currently created outside of it and prevents moving `json_reader` into closure
119+
json_reader.consume_trailing_whitespace()?;
120+
121+
assert_eq!(expected_string_value, string_output);
122+
Ok(())
123+
}
124+
125+
#[test]
126+
#[ignore = "transfer_to calls JsonWriter.string_value_writer and that returns a `Box<dyn ...>`"]
127+
fn transfer_to() -> Result<(), Box<dyn Error>> {
128+
let inner_json = r#"{"a":[{"b":1,"c":[[],[2,{"d":3,"e":"some string value"}]]}],"f":true}"#;
129+
let json = "{\"outer-ignored\": 1, \"outer\":[\"ignored\", ".to_owned() + inner_json + "]}";
130+
let mut json_reader = new_reader(&json);
131+
132+
// Pre-allocate with expected size to avoid allocations during test execution
133+
let mut writer = Vec::<u8>::with_capacity(inner_json.len());
134+
let mut json_writer = JsonStreamWriter::new(&mut writer);
135+
136+
let json_path = json_path!["outer", 1];
137+
138+
assert_no_alloc(|| {
139+
json_reader.seek_to(&json_path)?;
140+
141+
json_reader.transfer_to(&mut json_writer)?;
142+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
143+
permit_alloc(|| json_writer.finish_document())?;
144+
145+
json_reader.skip_to_top_level()?;
146+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
147+
permit_alloc(|| json_reader.consume_trailing_whitespace())?;
148+
Ok(())
149+
});
150+
151+
assert_eq!(inner_json, std::str::from_utf8(&writer)?);
152+
Ok(())
153+
}

tests/writer_alloc_test.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use std::error::Error;
2+
3+
use assert_no_alloc::permit_alloc;
4+
// Only use import when creating debug builds, see also configuration below
5+
#[cfg(debug_assertions)]
6+
use assert_no_alloc::AllocDisabler;
7+
use struson::writer::{JsonStreamWriter, JsonWriter, WriterSettings};
8+
9+
// Only enable when creating debug builds
10+
#[cfg(debug_assertions)]
11+
#[global_allocator]
12+
static A: AllocDisabler = AllocDisabler;
13+
14+
fn assert_no_alloc<F: FnOnce() -> Result<(), Box<dyn Error>>>(func: F) {
15+
assert_no_alloc::assert_no_alloc(func).unwrap()
16+
}
17+
18+
fn new_byte_writer() -> Vec<u8> {
19+
// Pre-allocate to avoid allocations during test execution
20+
Vec::with_capacity(4096)
21+
}
22+
23+
#[test]
24+
fn write_values() {
25+
let mut writer = new_byte_writer();
26+
let mut json_writer = JsonStreamWriter::new_custom(
27+
&mut writer,
28+
WriterSettings {
29+
// To test creation of surrogate pair escape sequences for supplementary code points
30+
escape_all_non_ascii: true,
31+
..Default::default()
32+
},
33+
);
34+
35+
let large_string = "abcd".repeat(500);
36+
37+
assert_no_alloc(|| {
38+
json_writer.begin_object()?;
39+
json_writer.name("a")?;
40+
41+
json_writer.begin_array()?;
42+
// Write string which has to be escaped
43+
json_writer.string_value("\0\n\t \u{10FFFF}")?;
44+
json_writer.string_value(&large_string)?;
45+
// Note: Cannot use non-string number methods because they perform allocation
46+
json_writer.number_value_from_string("1234.56e-7")?;
47+
json_writer.bool_value(true)?;
48+
json_writer.bool_value(false)?;
49+
json_writer.null_value()?;
50+
json_writer.end_array()?;
51+
52+
// Write string which has to be escaped
53+
json_writer.name("\0\n\t \u{10FFFF}")?;
54+
json_writer.bool_value(true)?;
55+
56+
json_writer.end_object()?;
57+
58+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
59+
permit_alloc(|| json_writer.finish_document())?;
60+
Ok(())
61+
});
62+
63+
let expected_json = "{\"a\":[\"\\u0000\\n\\t \\uDBFF\\uDFFF\",\"".to_owned()
64+
+ &large_string
65+
+ "\",1234.56e-7,true,false,null],\"\\u0000\\n\\t \\uDBFF\\uDFFF\":true}";
66+
assert_eq!(expected_json, std::str::from_utf8(&writer).unwrap());
67+
}
68+
69+
#[test]
70+
fn pretty_print() {
71+
let mut writer = new_byte_writer();
72+
let mut json_writer = JsonStreamWriter::new_custom(
73+
&mut writer,
74+
WriterSettings {
75+
pretty_print: true,
76+
..Default::default()
77+
},
78+
);
79+
80+
assert_no_alloc(|| {
81+
json_writer.begin_object()?;
82+
json_writer.name("a")?;
83+
json_writer.begin_array()?;
84+
85+
json_writer.begin_array()?;
86+
json_writer.end_array()?;
87+
json_writer.begin_object()?;
88+
json_writer.end_object()?;
89+
json_writer.bool_value(true)?;
90+
91+
json_writer.end_array()?;
92+
json_writer.end_object()?;
93+
94+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
95+
permit_alloc(|| json_writer.finish_document())?;
96+
Ok(())
97+
});
98+
99+
let expected_json = "{\n \"a\": [\n [],\n {},\n true\n ]\n}";
100+
assert_eq!(expected_json, std::str::from_utf8(&writer).unwrap());
101+
}
102+
103+
#[test]
104+
fn string_value_writer() -> Result<(), Box<dyn Error>> {
105+
let mut writer = new_byte_writer();
106+
let mut json_writer = JsonStreamWriter::new(&mut writer);
107+
let large_string = "abcd".repeat(500);
108+
109+
// Obtain this here because return value is `Box<dyn ...>` and therefore performs allocation
110+
let mut string_value_writer = json_writer.string_value_writer()?;
111+
assert_no_alloc(|| {
112+
string_value_writer.write_all(b"a")?;
113+
string_value_writer.write_all(b"\0")?;
114+
string_value_writer.write_all(b"\n\t")?;
115+
string_value_writer.write_all(large_string.as_bytes())?;
116+
string_value_writer.write_all(b"test")?;
117+
118+
// Use permit_alloc because assert_no_alloc currently also forbids dealloc
119+
permit_alloc(|| string_value_writer.finish_value())?;
120+
Ok(())
121+
});
122+
// TODO: Ideally would call this inside assert_no_alloc, but is not possible because `string_value_writer`
123+
// is currently created outside of it and prevents moving `json_writer` into closure
124+
json_writer.finish_document()?;
125+
126+
let expected_json = format!("\"a\\u0000\\n\\t{large_string}test\"");
127+
assert_eq!(expected_json, std::str::from_utf8(&writer).unwrap());
128+
Ok(())
129+
}

0 commit comments

Comments
 (0)