Skip to content

Commit 8021fee

Browse files
committed
test!: regression coverage for (?i:UPPER) literal mismatch
`inlined_memicmp` requires the `Condition::PrefixInsensitive` needle to be ascii-lowercase: it lowercases the haystack byte and compares it raw against the needle. `emit_literal` currently interns the literal verbatim, so any uppercase byte makes the prefix check mismatch unconditionally and the `if` branch silently never fires. three failing tests cover the cases: - single-literal `(?i:FOO)` against uppercase input - single-literal `(?i:FOO)` against lowercase input (sanity check that case-insensitivity itself works) - alternation `(?i:FROM|RUN|CMD)` against mixed-case inputs these will pass once `emit_literal` lowercases the needle when `case_insensitive` is set. failing on purpose -- hence `test!:`.
1 parent cc5f7ef commit 8021fee

2 files changed

Lines changed: 103 additions & 0 deletions

File tree

crates/lsh/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ rust-version.workspace = true
99

1010
[dependencies]
1111
stdext.workspace = true
12+
13+
[dev-dependencies]
14+
stdext.workspace = true
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Regression test for the `(?i:...)` literal bug.
5+
//!
6+
//! `inlined_memicmp` (in `runtime.rs`) requires the needle for
7+
//! `Condition::PrefixInsensitive` to be ASCII-lowercase: it lowercases each
8+
//! haystack byte and compares it raw against the needle. Before the fix in
9+
//! `emit_literal`, the literal was interned verbatim, so any uppercase byte
10+
//! made the prefix check mismatch unconditionally and the `if` branch
11+
//! silently never fired.
12+
13+
use lsh::compiler::Compiler;
14+
use lsh::runtime::Runtime;
15+
use stdext::arena::{self, Arena, scratch_arena};
16+
17+
fn ensure_scratch() {
18+
use std::sync::Once;
19+
static ONCE: Once = Once::new();
20+
ONCE.call_once(|| {
21+
arena::init(1 << 20).unwrap();
22+
});
23+
}
24+
25+
fn compile_and_run(src: &str, line: &[u8]) -> (Vec<(usize, u32)>, Vec<String>) {
26+
ensure_scratch();
27+
let arena = Arena::new(1 << 20).unwrap();
28+
let mut compiler = Compiler::new(&arena);
29+
compiler.parse("test.lsh", src).unwrap();
30+
let assembly = compiler.assemble().unwrap();
31+
32+
let charsets: Vec<[u16; 16]> = assembly.charsets.iter().map(|c| c.serialize()).collect();
33+
let kind_names: Vec<String> = {
34+
let max = assembly.highlight_kinds.iter().map(|hk| hk.value).max().unwrap_or(0);
35+
let mut names = vec![String::new(); max as usize + 1];
36+
for hk in &assembly.highlight_kinds {
37+
names[hk.value as usize] = hk.identifier.to_string();
38+
}
39+
names
40+
};
41+
let entry = assembly.entrypoints[0].address as u32;
42+
let mut runtime =
43+
Runtime::new(&assembly.instructions, &assembly.strings, &charsets, entry);
44+
45+
let scratch = scratch_arena(None);
46+
let highlights = runtime.parse_next_line::<u32>(&scratch, line);
47+
let pairs = highlights.iter().map(|h| (h.start, h.kind)).collect();
48+
(pairs, kind_names)
49+
}
50+
51+
fn assert_kind_present(src: &str, line: &[u8], expected_kind: &str) {
52+
let (highlights, kind_names) = compile_and_run(src, line);
53+
let present: Vec<&str> = highlights
54+
.iter()
55+
.filter_map(|(_, k)| kind_names.get(*k as usize).map(String::as_str))
56+
.collect();
57+
assert!(
58+
present.contains(&expected_kind),
59+
"expected `{expected_kind}` span on input {:?}, got kinds: {:?} (raw: {:?})",
60+
std::str::from_utf8(line).unwrap_or("<non-utf8>"),
61+
present,
62+
highlights,
63+
);
64+
}
65+
66+
const GRAMMAR: &str = r#"
67+
#[display_name = "Test"]
68+
#[path = "**/test.txt"]
69+
pub fn test() {
70+
if /(?i:FOO)\>/ {
71+
yield keyword.control;
72+
}
73+
}
74+
"#;
75+
76+
#[test]
77+
fn case_insensitive_uppercase_literal_matches_uppercase_input() {
78+
assert_kind_present(GRAMMAR, b"FOO", "keyword.control");
79+
}
80+
81+
#[test]
82+
fn case_insensitive_uppercase_literal_matches_lowercase_input() {
83+
assert_kind_present(GRAMMAR, b"foo", "keyword.control");
84+
}
85+
86+
#[test]
87+
fn case_insensitive_alternation_with_uppercase_alternatives() {
88+
let src = r#"
89+
#[display_name = "Test"]
90+
#[path = "**/test.txt"]
91+
pub fn test() {
92+
if /(?i:FROM|RUN|CMD)\>/ {
93+
yield keyword.control;
94+
}
95+
}
96+
"#;
97+
assert_kind_present(src, b"FROM", "keyword.control");
98+
assert_kind_present(src, b"RUN", "keyword.control");
99+
assert_kind_present(src, b"cmd", "keyword.control");
100+
}

0 commit comments

Comments
 (0)