1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00

chore(tests): ability to pattern match unordered lines (#20488)

This adds the ability to pattern match unordered lines. For example, the
downloading messages may appear in any order

```
[UNORDERED_START]
Download https://localhost:4546/a.ts
Download https://localhost:4546/b.ts
[UNORDERED_END]
Hello!
```

Additionally, I've made the pattern matching slightly more strict and the output better.
This commit is contained in:
David Sherret 2023-09-14 12:21:57 -04:00 committed by GitHub
parent 851f795001
commit 54890ee98b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 405 additions and 108 deletions

2
Cargo.lock generated
View file

@ -5455,6 +5455,7 @@ dependencies = [
"lazy-regex",
"libc",
"lsp-types",
"monch",
"nix 0.26.2",
"once_cell",
"os_pipe",
@ -5471,6 +5472,7 @@ dependencies = [
"serde_json",
"tar",
"tempfile",
"termcolor",
"tokio",
"tokio-rustls",
"url",

View file

@ -105,6 +105,7 @@ libc = "0.2.126"
log = "=0.4.20"
lsp-types = "=0.93.2" # used by tower-lsp and "proposed" feature is unstable in patch releases
memmem = "0.1.1"
monch = "=0.4.3"
notify = "=5.0.0"
num-bigint = { version = "0.4", features = ["rand"] }
once_cell = "1.17.1"

View file

@ -93,7 +93,7 @@ lazy-regex.workspace = true
libc.workspace = true
log = { workspace = true, features = ["serde"] }
lsp-types.workspace = true
monch = "=0.4.3"
monch.workspace = true
notify.workspace = true
once_cell.workspace = true
os_pipe.workspace = true

View file

@ -870,7 +870,7 @@ itest!(lock_write_fetch {
itest!(lock_check_ok {
args:
"run --lock=run/lock_check_ok.json http://127.0.0.1:4545/run/003_relative_import.ts",
"run --quiet --lock=run/lock_check_ok.json http://127.0.0.1:4545/run/003_relative_import.ts",
output: "run/003_relative_import.ts.out",
http_server: true,
});
@ -917,7 +917,7 @@ itest!(lock_flag_overrides_config_file_lock_path {
itest!(lock_v2_check_ok {
args:
"run --lock=run/lock_v2_check_ok.json http://127.0.0.1:4545/run/003_relative_import.ts",
"run --quiet --lock=run/lock_v2_check_ok.json http://127.0.0.1:4545/run/003_relative_import.ts",
output: "run/003_relative_import.ts.out",
http_server: true,
});

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Uncaught AggregateError
Error: bar <ref *1>
at file:///[WILDCARD]/error_cause_recursive_aggregate.ts:2:13

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Uncaught Error: baz
const baz = new Error("baz", { cause: bar });
^
@ -8,4 +7,3 @@ Caused by: Error: bar <ref *1>
Caused by: Error: foo
at file:///[WILDCARD]/error_cause_recursive_tail.ts:1:13
Caused by: [Circular *1]
[WILDCARD]

View file

@ -1,2 +1 @@
[WILDCARD]
[Module: null prototype] { isMod4: true }

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Uncaught (in promise) TypeError: Expected a "JavaScriptOrWasm" module but loaded a "JSON" module.
const data = await import("./data.json");
^

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Expected a JavaScript or TypeScript module, but identified a Json module. Consider importing Json modules with an import attribute with the type of "json".
Specifier: [WILDCARD]/data.json
at [WILDCARD]static_error.ts:1:18

View file

@ -1,2 +1 @@
[WILDCARD]
{ a: "b", c: { d: 10 } }

View file

@ -1,3 +1,2 @@
[WILDCARD]
{ a: "b", c: { d: 10 } }
{ a: "b", c: { d: 10 } }

View file

@ -1,4 +1,3 @@
[WILDCARD]
local: [WILDCARD]031_info_ts_error.ts
type: TypeScript
dependencies: 0 unique

View file

@ -1,4 +1,3 @@
[WILDCARD]
local: [WILDCARD]test.ts
type: TypeScript
dependencies: 7 unique

View file

@ -1,2 +1 @@
[WILDCARD]
Hello

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Expected a JavaScript or TypeScript module, but identified a Json module. Consider importing Json modules with an import attribute with the type of "json".
Specifier: [WILDCARD]/subdir/config.json
[WILDCARD]

View file

@ -1,4 +1,4 @@
[WILDCARD][class Location]
[class Location]
Object [Location] {}
Location {
hash: "#bat",
@ -13,4 +13,3 @@ Location {
}
NotSupportedError: Cannot set "location".
NotSupportedError: Cannot set "location.hostname".
[WILDCARD]

View file

@ -1,5 +1,4 @@
[WILDCARD][class Location]
[class Location]
Object [Location] {}
undefined
/bar
[WILDCARD]

View file

@ -1,3 +1 @@
[WILDCARD]
https://baz/qux
[WILDCARD]

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Uncaught (in promise) TypeError: Relative import path "unmapped" not prefixed with / or ./ or ../ and not in import map from "file://[WILDCARD]/092_import_map_unmapped_bare_specifier.ts"
at file://[WILDCARD]/092_import_map_unmapped_bare_specifier.ts:1:14

View file

@ -1,7 +1,7 @@
[WILDCARD]error: Uncaught Error: bad
[WILDCARD]
error: Uncaught Error: bad
throw Error("bad");
^
at foo (http://localhost:4545/run/error_001.ts:2:9)
at bar (http://localhost:4545/run/error_001.ts:6:3)
at http://localhost:4545/run/error_001.ts:9:1
[WILDCARD]

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Uncaught Error: foo
throw new Error("foo", { cause: new Error("bar", { cause: "deno" as any }) });
^
@ -12,4 +11,3 @@ Caused by: Error: bar
at c (file:///[WILDCARD]/error_cause.ts:11:3)
at file:///[WILDCARD]/error_cause.ts:14:1
Caused by: "deno"
[WILDCARD]

View file

@ -1,4 +1,3 @@
[WILDCARD]
error: Uncaught Error: bar <ref *1>
const y = new Error("bar", { cause: x });
^

View file

@ -1,3 +1,2 @@
[WILDCARD]
error: Module not found "file://[WILDCARD]/does_not_exist.js".
at file:///[WILDCARD]/error_missing_module_named_import.ts:[WILDCARD]

View file

@ -1,2 +1 @@
[WILDCARD]
[Function (anonymous)]

View file

@ -1,6 +1,5 @@
[WILDCARD]error: Uncaught (in promise) Error: Hello 2
error: Uncaught (in promise) Error: Hello 2
throw new Error(`Hello ${A.C}`);
^
at a (blob:null/[WILDCARD]:8:10)
at file:///[WILDCARD]/import_blob_url_error_stack.ts:13:1
[WILDCARD]

View file

@ -1,6 +1,5 @@
[WILDCARD]error: Uncaught Error: Hello 2
error: Uncaught Error: Hello 2
throw new Error(`Hello ${A.C}`);
^
at a (data:application/typescript;base64,ZW51bSBBIHsKICBBLAog......JHtBLkN9YCk7CiB9CiA=:8:10)
at file:///[WILDCARD]/import_data_url_error_stack.ts:3:1
[WILDCARD]

View file

@ -1,2 +1 @@
[WILDCARD]
Hello

View file

@ -1,2 +1 @@
[WILDCARD]
B { a: "a" }

View file

@ -1,2 +1 @@
[WILDCARD]
Hello

View file

@ -1,2 +1 @@
[WILDCARD]
a

View file

@ -1,3 +1,2 @@
[WILDCARD]
false
true

View file

@ -1,2 +1 @@
[WILDCARD]
x

View file

@ -1,3 +1,2 @@
[WILDCARD]
Storage {[WILDCARD]
Storage { length: 1, hello: "deno" }

View file

@ -1,2 +1 @@
[WILDCARD]
5

View file

@ -1,3 +1,2 @@
[WILDCARD]
Uint8Array(1) [ 49 ]
Uint8Array(1) [ 50 ]

View file

@ -1,4 +1,3 @@
[WILDCARD]
!
.
!

View file

@ -1,4 +1,3 @@
[WILDCARD]
,
.
.

View file

@ -1,2 +1 @@
[WILDCARD]ReadableStream { locked: false }
[WILDCARD]
ReadableStream { locked: false }

View file

@ -1,3 +1,2 @@
[WILDCARD]
error: Uncaught (in worker "") Requires read access to "[WILDCARD]worker_types.ts", run again with the --allow-read flag
[WILDCARD]

View file

@ -1,3 +1,2 @@
[WILDCARD]
error: Uncaught (in worker "") Requires net access to "localhost:4545", run again with the --allow-net flag
[WILDCARD]

View file

@ -26,6 +26,7 @@ hyper = { workspace = true, features = ["server", "http1", "http2", "runtime"] }
lazy-regex.workspace = true
libc.workspace = true
lsp-types.workspace = true
monch.workspace = true
nix.workspace = true
once_cell.workspace = true
os_pipe.workspace = true
@ -41,6 +42,7 @@ serde.workspace = true
serde_json.workspace = true
tar.workspace = true
tempfile.workspace = true
termcolor.workspace = true
tokio.workspace = true
tokio-rustls.workspace = true
url.workspace = true

View file

@ -1,5 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use crate::colors;
#[macro_export]
macro_rules! assert_starts_with {
($string:expr, $($test:expr),+) => {
@ -52,11 +54,40 @@ macro_rules! assert_not_contains {
#[track_caller]
pub fn assert_wildcard_match(actual: &str, expected: &str) {
if !expected.contains("[WILDCARD]") {
if !expected.contains("[WILDCARD]") && !expected.contains("[IGNORE_START]") {
pretty_assertions::assert_eq!(actual, expected);
} else if !crate::wildcard_match(expected, actual) {
println!("OUTPUT START\n{actual}\nOUTPUT END");
println!("EXPECTED START\n{expected}\nEXPECTED END");
} else {
match crate::wildcard_match_detailed(expected, actual) {
crate::WildcardMatchResult::Success => {
// ignore
}
crate::WildcardMatchResult::Fail(debug_output) => {
println!(
"{}{}{}",
colors::bold("-- "),
colors::bold_red("OUTPUT"),
colors::bold(" START --"),
);
println!("{}", actual);
println!("{}", colors::bold("-- OUTPUT END --"));
println!(
"{}{}{}",
colors::bold("-- "),
colors::bold_green("EXPECTED"),
colors::bold(" START --"),
);
println!("{}", expected);
println!("{}", colors::bold("-- EXPECTED END --"));
println!(
"{}{}{}",
colors::bold("-- "),
colors::bold_blue("DEBUG"),
colors::bold(" START --"),
);
println!("{debug_output}");
println!("{}", colors::bold("-- DEBUG END --"));
panic!("pattern match failed");
}
}
}
}

View file

@ -2410,57 +2410,266 @@ impl<'a> CheckOutputIntegrationTest<'a> {
}
}
pub fn wildcard_match(pattern: &str, s: &str) -> bool {
pattern_match(pattern, s, "[WILDCARD]")
pub fn wildcard_match(pattern: &str, text: &str) -> bool {
match wildcard_match_detailed(pattern, text) {
WildcardMatchResult::Success => true,
WildcardMatchResult::Fail(debug_output) => {
eprintln!("{}", debug_output);
false
}
}
}
pub fn pattern_match(pattern: &str, s: &str, wildcard: &str) -> bool {
pub enum WildcardMatchResult {
Success,
Fail(String),
}
pub fn wildcard_match_detailed(
pattern: &str,
text: &str,
) -> WildcardMatchResult {
fn annotate_whitespace(text: &str) -> String {
text.replace('\t', "\u{2192}").replace(' ', "\u{00B7}")
}
// Normalize line endings
let mut s = s.replace("\r\n", "\n");
let original_text = text.replace("\r\n", "\n");
let mut current_text = original_text.as_str();
let pattern = pattern.replace("\r\n", "\n");
let mut output_lines = Vec::new();
if pattern == wildcard {
return true;
}
let parts = pattern.split(wildcard).collect::<Vec<&str>>();
if parts.len() == 1 {
return pattern == s;
}
if !s.starts_with(parts[0]) {
return false;
}
// If the first line of the pattern is just a wildcard the newline character
// needs to be pre-pended so it can safely match anything or nothing and
// continue matching.
if pattern.lines().next() == Some(wildcard) {
s.insert(0, '\n');
}
let mut t = s.split_at(parts[0].len());
let parts = parse_wildcard_pattern_text(&pattern).unwrap();
let mut was_last_wildcard = false;
for (i, part) in parts.iter().enumerate() {
if i == 0 {
continue;
match part {
WildcardPatternPart::Wildcard => {
output_lines.push("<WILDCARD />".to_string());
}
dbg!(part, i);
if i == parts.len() - 1 && (part.is_empty() || *part == "\n") {
dbg!("exit 1 true", i);
return true;
}
if let Some(found) = t.1.find(*part) {
dbg!("found ", found);
t = t.1.split_at(found + part.len());
WildcardPatternPart::Text(search_text) => {
let is_last = i + 1 == parts.len();
let search_index = if is_last && was_last_wildcard {
// search from the end of the file
current_text.rfind(search_text)
} else {
dbg!("exit false ", i);
return false;
current_text.find(search_text)
};
match search_index {
Some(found_index) if was_last_wildcard || found_index == 0 => {
output_lines.push(format!(
"<FOUND>{}</FOUND>",
colors::gray(annotate_whitespace(search_text))
));
current_text = &current_text[found_index + search_text.len()..];
}
Some(index) => {
output_lines.push(
"==== FOUND SEARCH TEXT IN WRONG POSITION ====".to_string(),
);
output_lines.push(colors::gray(annotate_whitespace(search_text)));
output_lines
.push("==== HAD UNKNOWN PRECEEDING TEXT ====".to_string());
output_lines
.push(colors::red(annotate_whitespace(&current_text[..index])));
return WildcardMatchResult::Fail(output_lines.join("\n"));
}
None => {
let mut max_found_index = 0;
for (index, _) in search_text.char_indices() {
let sub_string = &search_text[..index];
if let Some(found_index) = current_text.find(sub_string) {
if was_last_wildcard || found_index == 0 {
max_found_index = index;
} else {
break;
}
} else {
break;
}
}
if !was_last_wildcard && max_found_index > 0 {
output_lines.push(format!(
"<FOUND>{}</FOUND>",
colors::gray(annotate_whitespace(
&search_text[..max_found_index]
))
));
}
output_lines
.push("==== COULD NOT FIND SEARCH TEXT ====".to_string());
output_lines.push(colors::green(annotate_whitespace(
if was_last_wildcard {
search_text
} else {
&search_text[max_found_index..]
},
)));
if was_last_wildcard && max_found_index > 0 {
output_lines.push(format!(
"==== MAX FOUND ====\n{}",
colors::red(annotate_whitespace(
&search_text[..max_found_index]
))
));
}
return WildcardMatchResult::Fail(output_lines.join("\n"));
}
}
}
WildcardPatternPart::UnorderedLines(expected_lines) => {
assert!(!was_last_wildcard, "unsupported");
let mut actual_lines = Vec::with_capacity(expected_lines.len());
for _ in 0..expected_lines.len() {
match current_text.find('\n') {
Some(end_line_index) => {
actual_lines.push(&current_text[..end_line_index]);
current_text = &current_text[end_line_index + 1..];
}
None => {
break;
}
}
}
actual_lines.sort_unstable();
let mut expected_lines = expected_lines.clone();
expected_lines.sort_unstable();
if actual_lines.len() != expected_lines.len() {
output_lines
.push("==== HAD WRONG NUMBER OF UNORDERED LINES ====".to_string());
output_lines.push("# ACTUAL".to_string());
output_lines.extend(
actual_lines
.iter()
.map(|l| colors::green(annotate_whitespace(l))),
);
output_lines.push("# EXPECTED".to_string());
output_lines.extend(
expected_lines
.iter()
.map(|l| colors::green(annotate_whitespace(l))),
);
return WildcardMatchResult::Fail(output_lines.join("\n"));
}
for (actual, expected) in actual_lines.iter().zip(expected_lines.iter())
{
if actual != expected {
output_lines
.push("==== UNORDERED LINE DID NOT MATCH ====".to_string());
output_lines.push(format!(
" ACTUAL: {}",
colors::red(annotate_whitespace(actual))
));
output_lines.push(format!(
"EXPECTED: {}",
colors::green(annotate_whitespace(expected))
));
return WildcardMatchResult::Fail(output_lines.join("\n"));
}
}
output_lines.push("# Found matching unordered lines".to_string());
}
}
was_last_wildcard = matches!(part, WildcardPatternPart::Wildcard);
}
if was_last_wildcard || current_text.is_empty() {
WildcardMatchResult::Success
} else {
output_lines.push("==== HAD TEXT AT END OF FILE ====".to_string());
output_lines.push(colors::red(annotate_whitespace(current_text)));
WildcardMatchResult::Fail(output_lines.join("\n"))
}
}
#[derive(Debug)]
enum WildcardPatternPart<'a> {
Wildcard,
Text(&'a str),
UnorderedLines(Vec<&'a str>),
}
fn parse_wildcard_pattern_text(
text: &str,
) -> Result<Vec<WildcardPatternPart>, monch::ParseErrorFailureError> {
use monch::*;
fn parse_unordered_lines(input: &str) -> ParseResult<Vec<&str>> {
const END_TEXT: &str = "\n[UNORDERED_END]\n";
let (input, _) = tag("[UNORDERED_START]\n")(input)?;
match input.find(END_TEXT) {
Some(end_index) => ParseResult::Ok((
&input[end_index + END_TEXT.len()..],
input[..end_index].lines().collect::<Vec<_>>(),
)),
None => ParseError::fail(input, "Could not find [UNORDERED_END]"),
}
}
dbg!("end ", t.1.len());
t.1.is_empty()
enum InnerPart<'a> {
Wildcard,
UnorderedLines(Vec<&'a str>),
Char,
}
struct Parser<'a> {
current_input: &'a str,
last_text_input: &'a str,
parts: Vec<WildcardPatternPart<'a>>,
}
impl<'a> Parser<'a> {
fn parse(mut self) -> ParseResult<'a, Vec<WildcardPatternPart<'a>>> {
while !self.current_input.is_empty() {
let (next_input, inner_part) = or3(
map(tag("[WILDCARD]"), |_| InnerPart::Wildcard),
map(parse_unordered_lines, |lines| {
InnerPart::UnorderedLines(lines)
}),
map(next_char, |_| InnerPart::Char),
)(self.current_input)?;
match inner_part {
InnerPart::Wildcard => {
self.queue_previous_text(next_input);
self.parts.push(WildcardPatternPart::Wildcard);
}
InnerPart::UnorderedLines(expected_lines) => {
self.queue_previous_text(next_input);
self
.parts
.push(WildcardPatternPart::UnorderedLines(expected_lines));
}
InnerPart::Char => {
// ignore
}
}
self.current_input = next_input;
}
self.queue_previous_text("");
ParseResult::Ok(("", self.parts))
}
fn queue_previous_text(&mut self, next_input: &'a str) {
let previous_text = &self.last_text_input
[..self.last_text_input.len() - self.current_input.len()];
if !previous_text.is_empty() {
self.parts.push(WildcardPatternPart::Text(previous_text));
}
self.last_text_input = next_input;
}
}
with_failure_handling(|input| {
Parser {
current_input: input,
last_text_input: input,
parts: Vec::new(),
}
.parse()
})(text)
}
pub fn with_pty(deno_args: &[&str], action: impl FnMut(Pty)) {
@ -2604,6 +2813,67 @@ pub fn parse_max_mem(output: &str) -> Option<u64> {
None
}
pub(crate) mod colors {
use std::io::Write;
use termcolor::Ansi;
use termcolor::Color;
use termcolor::ColorSpec;
use termcolor::WriteColor;
pub fn bold<S: AsRef<str>>(s: S) -> String {
let mut style_spec = ColorSpec::new();
style_spec.set_bold(true);
style(s, style_spec)
}
pub fn red<S: AsRef<str>>(s: S) -> String {
fg_color(s, Color::Red)
}
pub fn bold_red<S: AsRef<str>>(s: S) -> String {
bold_fg_color(s, Color::Red)
}
pub fn green<S: AsRef<str>>(s: S) -> String {
fg_color(s, Color::Green)
}
pub fn bold_green<S: AsRef<str>>(s: S) -> String {
bold_fg_color(s, Color::Green)
}
pub fn bold_blue<S: AsRef<str>>(s: S) -> String {
bold_fg_color(s, Color::Blue)
}
pub fn gray<S: AsRef<str>>(s: S) -> String {
fg_color(s, Color::Ansi256(245))
}
fn bold_fg_color<S: AsRef<str>>(s: S, color: Color) -> String {
let mut style_spec = ColorSpec::new();
style_spec.set_bold(true);
style_spec.set_fg(Some(color));
style(s, style_spec)
}
fn fg_color<S: AsRef<str>>(s: S, color: Color) -> String {
let mut style_spec = ColorSpec::new();
style_spec.set_fg(Some(color));
style(s, style_spec)
}
fn style<S: AsRef<str>>(s: S, colorspec: ColorSpec) -> String {
let mut v = Vec::new();
let mut ansi_writer = Ansi::new(&mut v);
ansi_writer.set_color(&colorspec).unwrap();
ansi_writer.write_all(s.as_ref().as_bytes()).unwrap();
ansi_writer.reset().unwrap();
String::from_utf8_lossy(&v).into_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -2689,6 +2959,15 @@ mod tests {
assert_eq!(strace.get("total").unwrap().usecs_per_call, Some(6));
}
#[test]
fn parse_parse_wildcard_match_text() {
let result =
parse_wildcard_pattern_text("[UNORDERED_START]\ntesting\ntesting")
.err()
.unwrap();
assert_contains!(result.to_string(), "Could not find [UNORDERED_END]");
}
#[test]
fn test_wildcard_match() {
let fixtures = vec![
@ -2733,16 +3012,15 @@ mod tests {
}
#[test]
fn test_pattern_match() {
fn test_wildcard_match2() {
// foo, bar, baz, qux, quux, quuz, corge, grault, garply, waldo, fred, plugh, xyzzy
let wildcard = "[BAR]";
assert!(pattern_match("foo[BAR]baz", "foobarbaz", wildcard));
assert!(!pattern_match("foo[BAR]baz", "foobazbar", wildcard));
assert!(wildcard_match("foo[WILDCARD]baz", "foobarbaz"));
assert!(!wildcard_match("foo[WILDCARD]baz", "foobazbar"));
let multiline_pattern = "[BAR]
let multiline_pattern = "[WILDCARD]
foo:
[BAR]baz[BAR]";
[WILDCARD]baz[WILDCARD]";
fn multi_line_builder(input: &str, leading_text: Option<&str>) -> String {
// If there is leading text add a newline so it's on it's own line
@ -2767,31 +3045,52 @@ grault",
);
// Correct input & leading line
assert!(pattern_match(
assert!(wildcard_match(
multiline_pattern,
&multi_line_builder("baz", Some("QUX=quux")),
wildcard
));
// Correct input & no leading line
assert!(pattern_match(
// Should fail when leading line
assert!(!wildcard_match(
multiline_pattern,
&multi_line_builder("baz", None),
wildcard
));
// Incorrect input & leading line
assert!(!pattern_match(
assert!(!wildcard_match(
multiline_pattern,
&multi_line_builder("garply", Some("QUX=quux")),
wildcard
));
// Incorrect input & no leading line
assert!(!pattern_match(
assert!(!wildcard_match(
multiline_pattern,
&multi_line_builder("garply", None),
wildcard
));
}
#[test]
fn test_wildcard_match_unordered_lines() {
// matching
assert!(wildcard_match(
concat!("[UNORDERED_START]\n", "B\n", "A\n", "[UNORDERED_END]\n"),
concat!("A\n", "B\n",)
));
// different line
assert!(!wildcard_match(
concat!("[UNORDERED_START]\n", "Ba\n", "A\n", "[UNORDERED_END]\n"),
concat!("A\n", "B\n",)
));
// different number of lines
assert!(!wildcard_match(
concat!(
"[UNORDERED_START]\n",
"B\n",
"A\n",
"C\n",
"[UNORDERED_END]\n"
),
concat!("A\n", "B\n",)
));
}