Skip to content

Commit 5ab47fc

Browse files
authored
Add unit tests for string escape logic (#844)
1 parent e7330e8 commit 5ab47fc

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

librubyfmt/src/string_escape.rs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ pub fn escape_word_array_content(
115115
output.push(next);
116116
}
117117
bytes.next();
118+
} else {
119+
output.push(b'\\');
118120
}
119121
} else if c == TARGET_OPEN || c == TARGET_CLOSE {
120122
// Unescaped target delimiter needs escaping
@@ -212,3 +214,229 @@ fn escape_string(content: &[u8], opening_delim: u8, closing_delim: u8) -> Vec<u8
212214

213215
output
214216
}
217+
218+
#[cfg(test)]
219+
mod tests {
220+
use super::*;
221+
222+
// --- escape_string with single-quoted strings ---
223+
224+
#[test]
225+
fn single_quoted_empty() {
226+
assert_eq!(single_to_double_quoted(b"", b"'", b"'").as_ref(), b"");
227+
}
228+
229+
#[test]
230+
fn single_quoted_plain_content() {
231+
assert_eq!(
232+
single_to_double_quoted(b"hello", b"'", b"'").as_ref(),
233+
b"hello"
234+
);
235+
}
236+
237+
#[test]
238+
fn single_quoted_escapes_double_quote() {
239+
assert_eq!(
240+
single_to_double_quoted(b"say \"hi\"", b"'", b"'").as_ref(),
241+
b"say \\\"hi\\\""
242+
);
243+
}
244+
245+
#[test]
246+
fn single_quoted_strips_backslash_before_single_quote() {
247+
// \' is valid in single-quoted strings but meaningless in double-quoted; strip it
248+
assert_eq!(single_to_double_quoted(b"\\'", b"'", b"'").as_ref(), b"'");
249+
}
250+
251+
#[test]
252+
fn single_quoted_preserves_escaped_backslash() {
253+
assert_eq!(
254+
single_to_double_quoted(b"\\\\", b"'", b"'").as_ref(),
255+
b"\\\\"
256+
);
257+
}
258+
259+
#[test]
260+
fn single_quoted_backslash_before_double_quote() {
261+
// '\"' is a literal backslash + double-quote (not an escape sequence in single-quoted).
262+
// In double-quoted, that must be \\\" (escaped backslash, then escaped quote).
263+
assert_eq!(
264+
single_to_double_quoted(b"\\\"", b"'", b"'").as_ref(),
265+
b"\\\\\\\""
266+
);
267+
}
268+
269+
#[test]
270+
fn single_quoted_doubles_backslash_before_unknown_escape() {
271+
// '\n' in single-quoted is a literal backslash + n (not a newline).
272+
// In double-quoted, \n would be a newline, so the backslash must be doubled.
273+
assert_eq!(
274+
single_to_double_quoted(b"\\n", b"'", b"'").as_ref(),
275+
b"\\\\n"
276+
);
277+
}
278+
279+
#[test]
280+
fn single_quoted_escapes_hash_dollar_interpolation() {
281+
// '#$foo' in double-quoted would interpolate $foo — escape the #
282+
assert_eq!(
283+
single_to_double_quoted(b"#$foo", b"'", b"'").as_ref(),
284+
b"\\#$foo"
285+
);
286+
}
287+
288+
#[test]
289+
fn single_quoted_escapes_hash_at_interpolation() {
290+
assert_eq!(
291+
single_to_double_quoted(b"#@foo", b"'", b"'").as_ref(),
292+
b"\\#@foo"
293+
);
294+
}
295+
296+
#[test]
297+
fn single_quoted_escapes_hash_brace_interpolation() {
298+
assert_eq!(
299+
single_to_double_quoted(b"#{foo}", b"'", b"'").as_ref(),
300+
b"\\#{foo}"
301+
);
302+
}
303+
304+
#[test]
305+
fn single_quoted_hash_before_regular_char_not_escaped() {
306+
assert_eq!(
307+
single_to_double_quoted(b"#foo", b"'", b"'").as_ref(),
308+
b"#foo"
309+
);
310+
}
311+
312+
// --- escape_string with %q(...) strings ---
313+
314+
#[test]
315+
fn percent_q_unescapes_open_delimiter() {
316+
// \( was escaping the %q( delimiter; in double-quoted it's just (
317+
assert_eq!(single_to_double_quoted(b"\\(", b"%q(", b")").as_ref(), b"(");
318+
}
319+
320+
#[test]
321+
fn percent_q_unescapes_close_delimiter() {
322+
assert_eq!(single_to_double_quoted(b"\\)", b"%q(", b")").as_ref(), b")");
323+
}
324+
325+
#[test]
326+
fn percent_q_escapes_double_quote() {
327+
assert_eq!(
328+
single_to_double_quoted(b"\"", b"%q(", b")").as_ref(),
329+
b"\\\""
330+
);
331+
}
332+
333+
// --- convert_percent_literal_escapes with %Q(...) strings ---
334+
335+
#[test]
336+
fn percent_q_upper_no_changes_returns_borrowed() {
337+
// No " and no escaped delimiters: should return the original slice
338+
let result = single_to_double_quoted(b"hello world", b"%Q(", b")");
339+
assert!(matches!(result, Cow::Borrowed(_)));
340+
assert_eq!(result.as_ref(), b"hello world");
341+
}
342+
343+
#[test]
344+
fn percent_q_upper_double_quote_triggers_owned() {
345+
let result = single_to_double_quoted(b"say \"hi\"", b"%Q(", b")");
346+
assert!(matches!(result, Cow::Owned(_)));
347+
assert_eq!(result.as_ref(), b"say \\\"hi\\\"");
348+
}
349+
350+
#[test]
351+
fn percent_q_upper_unescapes_open_delimiter() {
352+
assert_eq!(single_to_double_quoted(b"\\(", b"%Q(", b")").as_ref(), b"(");
353+
}
354+
355+
#[test]
356+
fn percent_q_upper_unescapes_close_delimiter() {
357+
assert_eq!(single_to_double_quoted(b"\\)", b"%Q(", b")").as_ref(), b")");
358+
}
359+
360+
#[test]
361+
fn percent_q_upper_preserves_other_backslash_sequences() {
362+
let result = single_to_double_quoted(b"\\n", b"%Q(", b")");
363+
assert!(matches!(result, Cow::Borrowed(_)));
364+
assert_eq!(result.as_ref(), b"\\n");
365+
}
366+
367+
#[test]
368+
fn percent_q_upper_preserves_escaped_backslash() {
369+
let result = single_to_double_quoted(b"\\\\", b"%Q(", b")");
370+
assert!(matches!(result, Cow::Borrowed(_)));
371+
assert_eq!(result.as_ref(), b"\\\\");
372+
}
373+
374+
#[test]
375+
fn percent_q_upper_trailing_backslash_preserved() {
376+
let result = single_to_double_quoted(b"foo\\", b"%Q(", b")");
377+
assert!(matches!(result, Cow::Borrowed(_)));
378+
assert_eq!(result.as_ref(), b"foo\\");
379+
}
380+
381+
// --- escape_word_array_content ---
382+
383+
#[test]
384+
fn word_array_plain_content() {
385+
assert_eq!(escape_word_array_content(b"hello", b'(', b')'), b"hello");
386+
}
387+
388+
#[test]
389+
fn word_array_escapes_open_bracket() {
390+
assert_eq!(escape_word_array_content(b"[", b'(', b')'), b"\\[");
391+
}
392+
393+
#[test]
394+
fn word_array_escapes_close_bracket() {
395+
assert_eq!(escape_word_array_content(b"]", b'(', b')'), b"\\]");
396+
}
397+
398+
#[test]
399+
fn word_array_unescapes_original_open_delim() {
400+
// \( was needed to escape the %w( delimiter; in %w[...] it's just (
401+
assert_eq!(escape_word_array_content(b"\\(", b'(', b')'), b"(");
402+
}
403+
404+
#[test]
405+
fn word_array_unescapes_original_close_delim() {
406+
assert_eq!(escape_word_array_content(b"\\)", b'(', b')'), b")");
407+
}
408+
409+
#[test]
410+
fn word_array_preserves_escaped_backslash() {
411+
assert_eq!(escape_word_array_content(b"\\\\", b'(', b')'), b"\\\\");
412+
}
413+
414+
#[test]
415+
fn word_array_keeps_already_escaped_open_bracket() {
416+
assert_eq!(escape_word_array_content(b"\\[", b'(', b')'), b"\\[");
417+
}
418+
419+
#[test]
420+
fn word_array_keeps_already_escaped_close_bracket() {
421+
assert_eq!(escape_word_array_content(b"\\]", b'(', b')'), b"\\]");
422+
}
423+
424+
#[test]
425+
fn word_array_preserves_other_backslash_sequences() {
426+
assert_eq!(escape_word_array_content(b"\\n", b'(', b')'), b"\\n");
427+
}
428+
429+
#[test]
430+
fn word_array_orig_delim_is_target_stays_escaped() {
431+
// When orig delim is [ (same as the target), \[ must remain \[ rather than being unescaped
432+
assert_eq!(escape_word_array_content(b"\\[", b'[', b']'), b"\\[");
433+
}
434+
435+
#[test]
436+
fn word_array_trailing_backslash_preserved() {
437+
// Technically this probably isn't reachable in valid Ruby code,
438+
// e.g. `%w(foo\)` is not valid, but we have it here defensively to
439+
// prevent accidentally dropping a slash in an edge case
440+
assert_eq!(escape_word_array_content(b"foo\\", b'(', b')'), b"foo\\");
441+
}
442+
}

0 commit comments

Comments
 (0)