Files
sx/examples/0713-modules-json-writer.sx
agra 1d311b871e test(json): pin s64 MIN/MAX writer bytes; move scratch to .sx-tmp
Close the coverage gap from attempt 1: example 0713 now builds integer
fields holding s64 MIN (-9223372036854775808) and s64 MAX
(9223372036854775807) — plus zero, a small negative, and a small positive —
and asserts the EXACT emitted bytes. This permanently pins the edge that
write_int is specifically engineered for (folding positives into negative
space so MIN's non-representable-positive magnitude serializes correctly).

s64 MIN is expressed as (0 - 9223372036854775807 - 1) because its magnitude
is not a representable positive s64 literal.

Test hygiene: stream to a repo-local, gitignored .sx-tmp/ path (created if
missing) instead of a fixed /tmp name, and unlink it right after read-back
so nothing leaks. Writer/model logic and src/ are untouched.
2026-06-04 01:08:14 +03:00

107 lines
4.5 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// JSON value model + writer from `modules/std/json.sx`.
//
// Builds a representative value — a nested object holding a string with
// every escape kind (quote, newline, tab, backslash, a raw control byte),
// integers spanning zero / a small negative / a small positive / s64 MIN
// (-9223372036854775808) / s64 MAX (9223372036854775807), a bool, null, an
// array, and a nested object — then serializes it two ways and asserts the
// EXACT bytes:
//
// 1. into a caller-owned `[]u8` buffer (returns bytes written),
// 2. streaming straight to a file through an 8-byte staging buffer
// (small on purpose, so the writer flushes many times and no
// whole-document string is ever held).
//
// Both must yield byte-for-byte the same pinned document, with keys in
// INSERTION ORDER. A too-small buffer must raise `error.Overflow` rather
// than truncate. The model is built through an explicit Arena allocator
// and freed in one `deinit`; the writer path allocates nothing.
#import "modules/std.sx";
#import "modules/std/json.sx";
#import "modules/fs.sx";
// The exact document the writer must produce (insertion order, escaping).
EXPECT :: "{\"name\":\"a\\\"b\\n\",\"tab\":\"x\\ty\",\"bs\":\"c\\\\d\",\"ctrl\":\"\\u0001\",\"n\":-7,\"zero\":0,\"pos\":7,\"min\":-9223372036854775808,\"max\":9223372036854775807,\"ok\":true,\"nil\":null,\"xs\":[1,-2,3],\"nested\":{\"k\":\"v\"}}";
report :: (label: string, ok: bool) {
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
}
build :: (alloc: Allocator) -> Value {
// A raw control byte (0x01) viewed as a 1-byte string — exercises the
// `\u00XX` path that has no named shorthand. String values are VIEWS,
// so the bytes must outlive the writes: back them with `alloc` (the
// arena), not a local that dies when `build` returns.
cbytes : [*]u8 = xx alloc.alloc(1);
cbytes[0] = 1;
ctrl := string.{ ptr = cbytes, len = 1 };
nested : Object = .{};
nested.put("k", .str("v"), alloc);
xs : Array = .{};
xs.add(.int_(1), alloc);
xs.add(.int_(0 - 2), alloc);
xs.add(.int_(3), alloc);
obj : Object = .{};
obj.put("name", .str("a\"b\n"), alloc); // quote + newline
obj.put("tab", .str("x\ty"), alloc); // tab
obj.put("bs", .str("c\\d"), alloc); // backslash
obj.put("ctrl", .str(ctrl), alloc); // raw control byte -> 
obj.put("n", .int_(0 - 7), alloc); // small negative int
obj.put("zero", .int_(0), alloc); // zero
obj.put("pos", .int_(7), alloc); // small positive int
// s64 MIN: its magnitude (9223372036854775808) is not a representable
// positive s64 literal, so build it from MAX-positive minus one.
obj.put("min", .int_(0 - 9223372036854775807 - 1), alloc);
obj.put("max", .int_(9223372036854775807), alloc); // s64 MAX
obj.put("ok", .bool_(true), alloc);
obj.put("nil", .null_, alloc);
obj.put("xs", .array(xs), alloc);
obj.put("nested", .object(nested), alloc);
return .object(obj);
}
main :: () -> ! {
gpa := GPA.init();
arena := Arena.init(xx gpa, 4096);
defer arena.deinit();
root := build(xx arena);
// 1. Write into a caller buffer; assert exact bytes + byte count.
buf : [512]u8 = ---;
n := try write_to_buffer(root, string.{ ptr = @buf[0], len = 512 });
view := string.{ ptr = @buf[0], len = n };
print("doc: {}\n", view);
report("buffer-exact", view == EXPECT);
report("buffer-len", n == EXPECT.len);
// 2. A buffer that is one byte too small must raise Overflow.
tight : []u8 = string.{ ptr = @buf[256], len = EXPECT.len - 1 };
_, oerr := write_to_buffer(root, tight);
report("overflow-raised", oerr == error.Overflow);
// 3. Stream to a file through a tiny staging buffer (forces flushes);
// read it back and assert it equals the same document. Write into the
// repo-local, gitignored scratch dir and unlink afterwards so nothing
// leaks and concurrent runs don't fight over a shared /tmp name.
if !create_dir_all(".sx-tmp") { print("mkdir: FAIL\n"); return; }
path := ".sx-tmp/sx_0713_json.json";
fh := open_file(path, .write);
if fh == null { print("open: FAIL\n"); return; }
f := fh!;
stage : [8]u8 = ---;
try write_to_file(root, @f, string.{ ptr = @stage[0], len = 8 });
f.close();
back := read_file(path);
delete_file(path);
if back == null { print("file-read: FAIL\n"); return; }
report("file-exact", back! == EXPECT);
return;
}