Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/examples_formatted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

The examples and the test programs are formatted, as the stdlib now is, and a test keeps them that way.
42 changes: 42 additions & 0 deletions crates/scarlet/tests/golden_examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,48 @@ suite! {
],
}

/// People read these to learn how Scarlet is written, so they stay exactly as
/// `scarlet fmt` writes them, as the stdlib does.
#[test]
fn every_example_and_program_is_formatted() {
let mut unformatted: Vec<String> = Vec::new();
for dir in [examples_dir(), programs_dir()] {
let mut entries: Vec<PathBuf> = walk(&dir);
entries.sort();
for path in entries {
let src = std::fs::read_to_string(&path).expect("a source file");
let same = matches!(
scarlet::formatter::format(&src),
scarlet::formatter::FormatResult::Formatted { output } if output == src
);
if !same {
unformatted.push(path.display().to_string());
}
}
}
assert!(
unformatted.is_empty(),
"not formatted; run `scarlet fmt examples crates/scarlet/tests/programs`: {unformatted:?}"
);
}

/// Every `.scrl` under `dir`, at any depth.
fn walk(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return out;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
out.extend(walk(&path));
} else if path.extension().is_some_and(|e| e == "scrl") {
out.push(path);
}
}
out
}

// The timing-free "it still runs" check the bench scripts depend on.
#[test]
fn bench_runs() {
Expand Down
6 changes: 3 additions & 3 deletions crates/scarlet/tests/programs/bitwise.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ pub fn main() {
println(
'de morgan: ${ok(
int.bitwise_not(int.bitwise_and(12345, 6789)) ==
int.bitwise_or(int.bitwise_not(12345), int.bitwise_not(6789)),
int.bitwise_or(int.bitwise_not(12345), int.bitwise_not(6789)),
)} ${ok(
int.bitwise_not(int.bitwise_or(-12345, 6789)) ==
int.bitwise_and(int.bitwise_not(-12345), int.bitwise_not(6789)),
int.bitwise_and(int.bitwise_not(-12345), int.bitwise_not(6789)),
)}',
)

Expand Down Expand Up @@ -279,7 +279,7 @@ pub fn main() {
int.bitwise_or(int.bitwise_shift_left(b2, 8), b3),
),
) ==
key,
key,
)}',
)
}
8 changes: 4 additions & 4 deletions crates/scarlet/tests/programs/crypto.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ fn hex_from(b Binary, at Int, size Int, acc Binary) Binary {
b,
at + 1,
size,
binary.append(
acc,
<<nibble(int.bitwise_shift_right(v, 4)):8, nibble(int.bitwise_and(v, 15)):8>>,
),
binary.append(acc, <<
nibble(int.bitwise_shift_right(v, 4)):8,
nibble(int.bitwise_and(v, 15)):8,
>>),
)
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/scarlet/tests/programs/effects.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ pub fn main() {
epoch_start = time.epoch_ms()
println('epoch is after 2024: ${epoch_start > 1704067200000}')
println('epoch is before 2100: ${epoch_start < 4102444800000}')
println('epoch is not the monotonic clock: ${epoch_start - time.to_deadline_ms(start) > 1704067200000}')
println(
'epoch is not the monotonic clock: ${epoch_start - time.to_deadline_ms(start) >
1704067200000}',
)

process.sleep(50)
println('slept at least 50ms: ${time.since_ms(time.monotonic(), start) >= 50}')
Expand Down
4 changes: 1 addition & 3 deletions crates/scarlet/tests/programs/hex_literals.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ pub fn main() {
println('hex: ${0xFF} ${0xff} ${0X10}')
println('bin: ${0b1010} ${0B11} ${0b0}')
println('sep: ${0xDE_AD_BE_EF} ${0b1111_0000}')
println(
'eq: ${ok(0xFF == 255)} ${ok(0b1111 == 15)} ${ok(0xDE_AD_BE_EF == 3735928559)}',
)
println('eq: ${ok(0xFF == 255)} ${ok(0b1111 == 15)} ${ok(0xDE_AD_BE_EF == 3735928559)}')
println('neg: ${-0xFF} ${ok(-0xFF == -255)} ${ok(-0b1010 == -10)}')
println(
'max: ${ok(
Expand Down
90 changes: 43 additions & 47 deletions crates/scarlet/tests/programs/http_parse.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,9 @@ pub fn main() {
// line was not the problem, so this is 431, not 414.
show_parse(
'header block past 64k',
binary.append(
binary.append(<<'GET / HTTP/1.1\r\n'>>, grow(<<'X-Pad: pad\r\n'>>, 70000)),
<<'\r\n'>>,
),
binary.append(binary.append(<<'GET / HTTP/1.1\r\n'>>, grow(<<'X-Pad: pad\r\n'>>, 70000)), <<
'\r\n',
>>),
)

println('')
Expand Down Expand Up @@ -209,7 +208,8 @@ pub fn main() {
nul_req = <<'GET / HTTP/1.1\r\nX-N: a\0b\r\n\r\n'>>
show_parse('nul in value parses', nul_req)
match h1.parse_request(nul_req, 0) {
h1.Done(_, _, _, hdrs, _, _) -> println('nul in value: headers.valid = ${headers.valid(hdrs)}')
h1.Done(_, _, _, hdrs, _, _) ->
println('nul in value: headers.valid = ${headers.valid(hdrs)}')
_ -> println('nul in value: parse failed')
}

Expand Down Expand Up @@ -283,18 +283,19 @@ pub fn main() {
show_field('lf in value', 'X-Ok', 'a\nb')
show_field('nul in value', 'X-Ok', 'a\0b')

show_valid = fn(label String, hs Array(headers.Header)) println('${label}: ${headers.valid(hs)}')
show_valid = fn(label String, hs Array(
headers.Header,
)) println('${label}: ${headers.valid(hs)}')
show_valid('valid: empty list', [])
show_valid(
'valid: token name + clean value',
[headers.Header(name: <<'X-Ok!#\$%*+-.^_|~'>>, value: <<'fine'>>)],
)
show_valid('valid: token name + clean value', [
headers.Header(name: <<'X-Ok!#\$%*+-.^_|~'>>, value: <<'fine'>>),
])
show_valid('invalid: colon in name', [headers.Header(name: <<'X:Bad'>>, value: <<'v'>>)])
show_valid('invalid: lf in value', [headers.Header(name: <<'X-Ok'>>, value: <<'a\nb'>>)])
show_valid(
'invalid: one bad field among good',
[headers.Header(name: <<'Good'>>, value: <<'1'>>), headers.Header(name: <<'Bad Name'>>, value: <<'2'>>)],
)
show_valid('invalid: one bad field among good', [
headers.Header(name: <<'Good'>>, value: <<'1'>>),
headers.Header(name: <<'Bad Name'>>, value: <<'2'>>),
])

println('')
println('== pipelined requests ==')
Expand Down Expand Up @@ -324,44 +325,36 @@ pub fn main() {
// `5 5` is the internal-space trick: a value a lenient parser reads as 5 and a
// stricter one as 55 — a length disagreement between two hops is a smuggle.
show_framing('spaced cl rejected', <<'POST / HTTP/1.1\r\nContent-Length: 5 5\r\n\r\n'>>)
show_framing(
'overflow rejected',
<<'POST / HTTP/1.1\r\nContent-Length: 99999999999999999999\r\n\r\n'>>,
)
show_framing(
'duplicate cl rejected',
<<'POST / HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\n'>>,
)
show_framing('overflow rejected', <<
'POST / HTTP/1.1\r\nContent-Length: 99999999999999999999\r\n\r\n',
>>)
show_framing('duplicate cl rejected', <<
'POST / HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\n',
>>)
show_framing('te chunked', <<'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n'>>)
show_framing(
'te chunked case-insensitive',
<<'POST / HTTP/1.1\r\ntransfer-encoding: CHUNKED\r\n\r\n'>>,
)
show_framing('te chunked case-insensitive', <<
'POST / HTTP/1.1\r\ntransfer-encoding: CHUNKED\r\n\r\n',
>>)
// The head parser trims OWS around a value, so a padded `chunked` still frames.
show_framing(
'te chunked with ows',
<<'POST / HTTP/1.1\r\nTransfer-Encoding: chunked \r\n\r\n'>>,
)
show_framing('te chunked with ows', <<
'POST / HTTP/1.1\r\nTransfer-Encoding: chunked \r\n\r\n',
>>)
show_framing('te gzip unimplemented', <<'POST / HTTP/1.1\r\nTransfer-Encoding: gzip\r\n\r\n'>>)
show_framing(
'te coding list unimplemented',
<<'POST / HTTP/1.1\r\nTransfer-Encoding: gzip, chunked\r\n\r\n'>>,
)
show_framing(
'te identity unimplemented',
<<'POST / HTTP/1.1\r\nTransfer-Encoding: identity\r\n\r\n'>>,
)
show_framing('te coding list unimplemented', <<
'POST / HTTP/1.1\r\nTransfer-Encoding: gzip, chunked\r\n\r\n',
>>)
show_framing('te identity unimplemented', <<
'POST / HTTP/1.1\r\nTransfer-Encoding: identity\r\n\r\n',
>>)
show_framing('empty te unimplemented', <<'POST / HTTP/1.1\r\nTransfer-Encoding:\r\n\r\n'>>)
// Even two IDENTICAL `chunked` fields are refused: a repeated Transfer-Encoding
// is exactly the shape a front-end/back-end framing disagreement is built from.
show_framing(
'repeated te chunked unimplemented',
<<'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n\r\n'>>,
)
show_framing(
'te + cl conflict',
<<'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n'>>,
)
show_framing('repeated te chunked unimplemented', <<
'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n\r\n',
>>)
show_framing('te + cl conflict', <<
'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n',
>>)

println('')
println('== chunked body decode ==')
Expand Down Expand Up @@ -509,7 +502,10 @@ pub fn main() {
show_collect('failing body propagates', fn() Err(error.UnexpectedEof), 1024)

three_chunks = fn() Ok(
body.Chunk(<<'ab'>>, fn() Ok(body.Chunk(<<'cd'>>, fn() Ok(body.Chunk(<<'ef'>>, body.empty()))))),
body.Chunk(
<<'ab'>>,
fn() Ok(body.Chunk(<<'cd'>>, fn() Ok(body.Chunk(<<'ef'>>, body.empty())))),
),
)
show_buffered = fn(label String, b body.Body) {
out = match body.take_buffered(b) {
Expand Down
Loading
Loading