use std::num::IntErrorKind;
#[cfg(test)]
use std::{
env,
fmt::{self, Display, Formatter},
io::Read,
panic,
process::{Command, ExitStatus, Stdio},
thread::{self, JoinHandle},
time::{Duration, Instant}
};
use crate::{
Function, Optimizer as _, Passes, StandardOptimizer, compile_unoptimized
};
pub type TestCase = (&'static str, &'static str);
pub type CompilationTestCase = TestCase;
pub fn read_compilation_test_cases(
source: &'static str
) -> Vec<CompilationTestCase>
{
let mut test_cases = Vec::new();
let blocks = source.split("\n\n");
for block in blocks
{
let parts = block.split("\n=\n").collect::<Vec<_>>();
for part in parts[..].chunks(2)
{
let source = part[0].trim();
let expected = part[1].trim();
test_cases.push((source, expected));
}
}
test_cases
}
pub fn compile_valid(source: &str) -> Function
{
match compile_unoptimized(source)
{
Ok(function) => function,
Err(e) => panic!("compilation error: {e}")
}
}
pub fn optimize(function: Function, passes: Passes) -> Function
{
StandardOptimizer::new(passes).optimize(function).unwrap()
}
pub type EvaluationTestCase = (
&'static str,
Vec<i32>,
Vec<(&'static str, i32)>,
&'static str
);
pub fn read_evaluation_test_cases(
source: &'static str
) -> Vec<EvaluationTestCase>
{
let mut test_cases = Vec::new();
let blocks = source.split("\n\n");
for block in blocks
{
let parts: Vec<&str> = block.splitn(2, "\n=\n").collect();
let source = parts[0].trim();
let cases: Vec<&str> = parts[1].trim().split("\n=\n").collect();
for case in cases
{
let mut lines: Vec<&str> = case.lines().collect();
let expected = lines.pop().expect("missing expected result");
let expected = expected.trim();
let mut args = Vec::new();
let mut externs = Vec::new();
for line in lines
{
let line = line.trim();
if let Some(line) = line.strip_prefix("args:")
{
args = line
.split(',')
.map(|s| match s.trim().parse::<i32>()
{
Ok(i) => i,
Err(e)
if e.kind() == &IntErrorKind::PosOverflow =>
{
i32::MAX
},
Err(e)
if e.kind() == &IntErrorKind::NegOverflow =>
{
i32::MIN
},
_ => unreachable!()
})
.collect();
}
else if let Some(line) = line.strip_prefix("externs:")
{
externs = line
.split(',')
.map(|s| {
let parts: Vec<&str> =
s.trim().splitn(2, '=').collect();
(parts[0].trim(), parts[1].trim().parse().unwrap())
})
.collect();
}
}
test_cases.push((source, args, externs, expected));
}
}
test_cases
}
pub type HistogramTestCase = (
&'static str,
Vec<i32>,
Vec<(&'static str, i32)>,
Vec<(i32, usize)>
);
pub fn read_histogram_test_cases(source: &'static str)
-> Vec<HistogramTestCase>
{
let mut test_cases = Vec::new();
let blocks = source.split("\n\n");
for block in blocks
{
let parts: Vec<&str> = block.splitn(2, "\n=\n").collect();
let source = parts[0].trim();
let cases: Vec<&str> = parts[1].trim().split("\n=\n").collect();
for case in cases
{
let mut expected = Vec::new();
let mut args = Vec::new();
let mut externs = Vec::new();
for line in case.lines()
{
let line = line.trim();
if let Some(line) = line.strip_prefix("args:")
{
args = line
.split(',')
.map(|s| match s.trim().parse::<i32>()
{
Ok(i) => i,
Err(e)
if e.kind() == &IntErrorKind::PosOverflow =>
{
i32::MAX
},
Err(e)
if e.kind() == &IntErrorKind::NegOverflow =>
{
i32::MIN
},
_ => unreachable!()
})
.collect();
}
else if let Some(line) = line.strip_prefix("externs:")
{
externs = line
.split(',')
.map(|s| {
let parts: Vec<&str> =
s.trim().splitn(2, '=').collect();
(parts[0].trim(), parts[1].trim().parse().unwrap())
})
.collect();
}
else
{
let parts: Vec<&str> = line.splitn(2, ':').collect();
let outcome = parts[0].trim().parse::<i32>().unwrap();
let count = parts[1].trim().parse::<usize>().unwrap();
expected.push((outcome, count));
}
}
test_cases.push((source, args, externs, expected));
}
}
test_cases
}
#[cfg(test)]
#[derive(Debug)]
pub struct ExpectedPlaceholder
{
pub span: (usize, usize),
pub description: &'static str,
pub valid_kinds: Vec<&'static str>
}
#[cfg(test)]
#[derive(Debug)]
pub struct ExpectedSuggestion
{
pub corrected_source: &'static str,
pub placeholders: Vec<ExpectedPlaceholder>
}
#[cfg(test)]
#[derive(Debug)]
pub struct ExpectedRelated
{
pub span: (usize, usize),
pub message: &'static str
}
#[cfg(test)]
#[derive(Debug)]
pub struct ExpectedDiagnostic
{
pub kind: &'static str,
pub span: (usize, usize),
pub message: &'static str,
pub rendered: &'static str,
pub related: Vec<ExpectedRelated>,
pub suggestions: Vec<ExpectedSuggestion>
}
#[cfg(test)]
#[derive(Debug)]
pub struct ErrorTestCase
{
pub source: &'static str,
pub expected_diagnostics: Vec<ExpectedDiagnostic>
}
#[cfg(test)]
pub fn read_error_test_cases(source: &'static str) -> Vec<ErrorTestCase>
{
let mut test_cases = Vec::new();
for block in source.split("\n\n")
{
let block = block.trim();
if block.is_empty()
{
continue;
}
let parts: Vec<&str> = block.splitn(2, "\n=\n").collect();
assert!(parts.len() == 2, "malformed test case block: {:?}", block);
let test_source = match parts[0]
{
"<empty>" => "",
other => other
};
let diagnostics_text = parts[1];
let mut expected_diagnostics = Vec::new();
for diag_block in diagnostics_text.split("\n---\n")
{
expected_diagnostics
.push(parse_expected_diagnostic(diag_block.trim()));
}
test_cases.push(ErrorTestCase {
source: test_source,
expected_diagnostics
});
}
test_cases
}
#[cfg(test)]
fn parse_expected_diagnostic(text: &'static str) -> ExpectedDiagnostic
{
let mut kind = None;
let mut span = None;
let mut message = None;
let mut rendered = None;
let mut related: Vec<ExpectedRelated> = Vec::new();
let mut suggestions: Vec<ExpectedSuggestion> = Vec::new();
let mut current_suggestion: Option<&'static str> = None;
let mut current_placeholders: Vec<ExpectedPlaceholder> = Vec::new();
for line in text.lines()
{
let line = line.trim();
if let Some(rest) = line.strip_prefix("kind:")
{
kind = Some(rest.trim());
}
else if let Some(rest) = line.strip_prefix("span:")
{
let parts: Vec<&str> = rest.trim().split("..").collect();
span = Some((
parts[0].parse::<usize>().unwrap(),
parts[1].parse::<usize>().unwrap()
));
}
else if let Some(rest) = line.strip_prefix("message:")
{
message = Some(rest.trim());
}
else if let Some(rest) = line.strip_prefix("rendered:")
{
rendered = Some(rest.trim());
}
else if let Some(rest) = line.strip_prefix("related:")
{
assert!(
current_suggestion.is_none(),
"`related:` line must appear before any `suggestion:` line: {:?}",
line
);
related.push(parse_expected_related(rest.trim()));
}
else if let Some(rest) = line.strip_prefix("suggestion:")
{
if let Some(src) = current_suggestion
{
suggestions.push(ExpectedSuggestion {
corrected_source: src,
placeholders: std::mem::take(&mut current_placeholders)
});
}
current_suggestion = Some(rest.trim());
}
else if let Some(rest) = line.strip_prefix("placeholder:")
{
current_placeholders.push(parse_expected_placeholder(rest.trim()));
}
}
if let Some(src) = current_suggestion
{
suggestions.push(ExpectedSuggestion {
corrected_source: src,
placeholders: std::mem::take(&mut current_placeholders)
});
}
ExpectedDiagnostic {
kind: kind.expect("missing kind"),
span: span.expect("missing span"),
message: message.expect("missing message"),
rendered: rendered.expect("missing rendered"),
related,
suggestions
}
}
#[cfg(test)]
fn parse_expected_related(text: &'static str) -> ExpectedRelated
{
let span_end = text.find(' ').unwrap();
let span_parts: Vec<&str> = text[..span_end].split("..").collect();
let span = (
span_parts[0].parse::<usize>().unwrap(),
span_parts[1].parse::<usize>().unwrap()
);
let rest = text[span_end..].trim();
let msg_start = rest.find('"').unwrap() + 1;
let msg_end = rest[msg_start..].rfind('"').unwrap() + msg_start;
let message = &rest[msg_start..msg_end];
ExpectedRelated { span, message }
}
#[cfg(test)]
fn parse_expected_placeholder(text: &'static str) -> ExpectedPlaceholder
{
let span_end = text.find(' ').unwrap();
let span_parts: Vec<&str> = text[..span_end].split("..").collect();
let span = (
span_parts[0].parse::<usize>().unwrap(),
span_parts[1].parse::<usize>().unwrap()
);
let rest = text[span_end..].trim();
let desc_start = rest.find('"').unwrap() + 1;
let desc_end = rest[desc_start..].find('"').unwrap() + desc_start;
let description = &rest[desc_start..desc_end];
let kinds_start = rest.find('[').unwrap() + 1;
let kinds_end = rest.find(']').unwrap();
let valid_kinds: Vec<&str> = rest[kinds_start..kinds_end]
.split(',')
.map(|s| s.trim())
.collect();
ExpectedPlaceholder {
span,
description,
valid_kinds
}
}
#[cfg(test)]
pub const SMALL_STACK_SIZE: usize = 2 * 1024 * 1024;
#[cfg(test)]
pub const SMALL_STACK_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(test)]
const SMALL_STACK_CHILD: &str = "XDY_SMALL_STACK_CHILD";
#[cfg(test)]
const SMALL_STACK_MARKER: &str = "xdy-small-stack: closure returned";
#[cfg(test)]
#[derive(Debug)]
pub enum SmallStackFailure
{
Panicked
{
transcript: String
},
Overflowed
{
transcript: String
},
TimedOut
{
timeout: Duration,
transcript: String
},
Abnormal
{
status: ExitStatus,
transcript: String
},
NotRun
{
transcript: String
}
}
#[cfg(test)]
impl Display for SmallStackFailure
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
let transcript = match self
{
SmallStackFailure::Panicked { transcript } =>
{
writeln!(f, "the closure panicked on the small stack")?;
transcript
},
SmallStackFailure::Overflowed { transcript } =>
{
writeln!(
f,
"the closure overflowed the small stack ({} bytes)",
SMALL_STACK_SIZE
)?;
transcript
},
SmallStackFailure::TimedOut {
timeout,
transcript
} =>
{
writeln!(
f,
"the closure did not finish within {:?}: either it runs in \
super-linear time, or it overflowed the stack on a host \
where an overflow hangs instead of aborting",
timeout
)?;
transcript
},
SmallStackFailure::Abnormal { status, transcript } =>
{
writeln!(f, "the child terminated abnormally: {}", status)?;
transcript
},
SmallStackFailure::NotRun { transcript } =>
{
writeln!(
f,
"the child exited successfully without running the \
closure; did the test filter match nothing?"
)?;
transcript
}
};
write!(f, "{}", transcript)
}
}
#[cfg(test)]
pub fn on_small_stack<F>(f: F)
where
F: FnOnce() + Send
{
on_small_stack_within(SMALL_STACK_TIMEOUT, f)
}
#[cfg(test)]
pub fn on_small_stack_within<F>(timeout: Duration, f: F)
where
F: FnOnce() + Send
{
if let Err(failure) = try_on_small_stack(timeout, f)
{
panic!("{}", failure)
}
}
#[cfg(test)]
#[cfg_attr(doc, aquamarine::aquamarine)]
pub fn try_on_small_stack<F>(
timeout: Duration,
f: F
) -> Result<(), SmallStackFailure>
where
F: FnOnce() + Send
{
let test = thread::current()
.name()
.filter(|name| *name != "main")
.expect("the caller must be a libtest test thread")
.to_owned();
match env::var(SMALL_STACK_CHILD)
{
Ok(child) if child == test =>
{
run_small_stack_child(f);
Ok(())
},
Ok(child) => panic!(
"small-stack child for `{}` reached `{}` instead",
child, test
),
Err(_) => run_small_stack_parent(&test, timeout)
}
}
#[cfg(test)]
fn run_small_stack_child<F>(f: F)
where
F: FnOnce() + Send
{
let outcome = thread::scope(|scope| {
thread::Builder::new()
.name("xdy-small-stack".to_owned())
.stack_size(SMALL_STACK_SIZE)
.spawn_scoped(scope, f)
.expect("failed to spawn the small-stack thread")
.join()
});
match outcome
{
Ok(()) => println!("{}", SMALL_STACK_MARKER),
Err(payload) => panic::resume_unwind(payload)
}
}
#[cfg(test)]
fn run_small_stack_parent(
test: &str,
timeout: Duration
) -> Result<(), SmallStackFailure>
{
let executable =
env::current_exe().expect("failed to locate the test executable");
let mut child = Command::new(executable)
.args([
test,
"--exact",
"--nocapture",
"--include-ignored",
"--test-threads=1"
])
.env(SMALL_STACK_CHILD, test)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn the small-stack child");
let stdout = drain(child.stdout.take().expect("stdout is piped"));
let stderr = drain(child.stderr.take().expect("stderr is piped"));
let deadline = Instant::now() + timeout;
let status = loop
{
if let Some(status) = child
.try_wait()
.expect("failed to await the small-stack child")
{
break Some(status)
}
if Instant::now() >= deadline
{
let _ = child.kill();
let _ = child.wait();
break None
}
thread::sleep(Duration::from_millis(10));
};
let stdout = stdout.join().expect("the stdout reader panicked");
let stderr = stderr.join().expect("the stderr reader panicked");
let transcript = format!(
"--- child stdout ---\n{}\n--- child stderr ---\n{}",
stdout, stderr
);
match status
{
None => Err(SmallStackFailure::TimedOut {
timeout,
transcript
}),
Some(status) if status.success() =>
{
if stdout.contains(SMALL_STACK_MARKER)
{
Ok(())
}
else
{
Err(SmallStackFailure::NotRun { transcript })
}
},
Some(_) if stderr.contains("has overflowed its stack") =>
{
Err(SmallStackFailure::Overflowed { transcript })
},
Some(status) if status.code() == Some(101) =>
{
Err(SmallStackFailure::Panicked { transcript })
},
Some(status) => Err(SmallStackFailure::Abnormal { status, transcript })
}
}
#[cfg(test)]
fn drain(mut pipe: impl Read + Send + 'static) -> JoinHandle<String>
{
thread::spawn(move || {
let mut bytes = Vec::new();
let _ = pipe.read_to_end(&mut bytes);
String::from_utf8_lossy(&bytes).into_owned()
})
}