use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use blue_lang_runtime::observe::{
CallObserver, Definitions, Exit, Measured, Recorder, TraceEvent, Tracer,
};
use blue_lang_runtime::uses::Entry;
use crate::config::BlueConfig;
use crate::{read_compiled, CliError};
const VALUE_WIDTH: usize = 80;
struct FileRun {
path: PathBuf,
errors: String,
report: Option<blue_lang_test::Report>,
coverage: Option<Vec<Measured>>,
arms: Option<Arc<blue_lang_test::Arms>>,
identities: Option<blue_lang_mondou::def_hash::TestIdentities>,
passed_keys: Vec<String>,
cached_keys: Vec<String>,
}
#[derive(Clone, Copy)]
pub(crate) struct Watch<'a> {
pub filter: Option<&'a str>,
pub coverage: bool,
pub json: bool,
pub memo: crate::memo::Mode,
pub cache: Option<&'a crate::test_cache::TestCache>,
pub proofs: Option<&'a Path>,
pub junit: Option<&'a Path>,
pub shard: Option<(usize, usize)>,
pub rerun: usize,
}
pub(crate) fn shard_of(text: &str) -> Result<(usize, usize), CliError> {
let parsed = text
.split_once('/')
.and_then(|(i, n)| Some((i.parse::<usize>().ok()?, n.parse::<usize>().ok()?)))
.filter(|(i, n)| *n > 0 && (1..=*n).contains(i));
parsed.ok_or_else(|| {
CliError::Usage(format!(
"--shard is \"{text}\"; it takes I/N with 1 <= I <= N, as 2/4"
))
})
}
pub(crate) fn sharded(mut files: Vec<PathBuf>, shard: Option<(usize, usize)>) -> Vec<PathBuf> {
files.sort();
match shard {
None => files,
Some((i, n)) => files
.into_iter()
.enumerate()
.filter(|(k, _)| k % n == i - 1)
.map(|(_, f)| f)
.collect(),
}
}
fn today() -> String {
std::env::var("BLUE_TEST_TODAY").unwrap_or_else(|_| {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(0));
blue_lang_runtime::sys::format_rfc3339_utc(secs)[..10].to_string()
})
}
fn quarantine(run: &mut FileRun, today: &str) -> Vec<(String, String)> {
let entries = blue_lang_pkg::lock::quarantines_of(&run.path);
let Some(report) = run.report.as_mut() else {
return Vec::new();
};
let mut held = Vec::new();
for (name, q) in entries {
let failing = report.failures.iter().any(|f| f.test() == name);
if !failing {
if report.outcomes.iter().any(|o| o.name == name && o.passed) {
eprintln!(
"➖ {}: {name} passes; its quarantine (until {}) can go",
run.path.display(),
q.until
);
}
} else if today <= q.until.as_str() {
report.failures.retain(|f| f.test() != name);
eprintln!(
"⏸ {}: {name} is quarantined until {}: {}",
run.path.display(),
q.until,
q.why
);
held.push((name, q.why));
} else {
eprintln!(
"❌ {}: {name}'s quarantine ended on {}, so it fails again",
run.path.display(),
q.until
);
}
}
held
}
fn flaky(runs: &[Vec<FileRun>]) -> Vec<(PathBuf, String, usize)> {
let mut seen: std::collections::BTreeMap<(PathBuf, String), Vec<bool>> =
std::collections::BTreeMap::new();
for pass in runs {
for run in pass {
if let Some(r) = &run.report {
for o in &r.outcomes {
let failed = r.failures.iter().any(|f| f.test() == o.name);
seen.entry((run.path.clone(), o.name.clone()))
.or_default()
.push(o.passed && !failed);
}
}
}
}
seen.into_iter()
.filter(|(_, v)| v.iter().any(|p| *p) && v.iter().any(|p| !*p))
.map(|((p, n), v)| (p, n, v.iter().filter(|x| **x).count()))
.collect()
}
pub(crate) fn test(
path: &Path,
watch: &Watch<'_>,
hold: Option<blue_lang_runtime::Hold>,
facts: Option<&Path>,
) -> Result<ExitCode, CliError> {
hermetic()?;
let (filter, json) = (watch.filter, watch.json);
let tree = path.is_dir();
let files = if tree {
sharded(test_files(path), watch.shard)
} else {
vec![path.to_path_buf()]
};
let hold = match (hold, watch.cache) {
(None, Some(_)) => Some(blue_lang_runtime::Hold::from(blue_lang_waku::Waku::top())),
(hold, _) => hold,
};
let guard = blue_lang_test::Guard {
hold,
max_alloc_bytes: blue_lang_runtime::execution_bounds().max_alloc_bytes,
identify: Some(blue_lang_mondou::def_hash::identify),
..blue_lang_test::Guard::default()
};
let mut runs = run_files(&files, watch, &guard, facts.is_some())?;
let unstable = if watch.rerun > 1 {
let again = Watch {
cache: None,
coverage: false,
proofs: None,
..*watch
};
let mut passes = Vec::new();
for _ in 1..watch.rerun {
passes.push(run_files(&files, &again, &guard, false)?);
}
let mut all = vec![std::mem::take(&mut runs)];
all.extend(passes);
let found = flaky(&all);
runs = all.swap_remove(0);
for (p, n, k) in &found {
eprintln!(
"⚠️ flaky: {}: {n} passed {k} of {} runs",
p.display(),
watch.rerun
);
}
found.len()
} else {
0
};
let today = today();
let held: Vec<Vec<(String, String)>> = runs.iter_mut().map(|r| quarantine(r, &today)).collect();
if let Some(file) = watch.junit {
let suites: Vec<crate::junit::Suite<'_>> = runs
.iter()
.zip(&held)
.map(|(r, q)| crate::junit::Suite {
path: &r.path,
report: r.report.as_ref(),
errors: &r.errors,
quarantined: q,
})
.collect();
std::fs::write(file, crate::junit::render(&suites)).map_err(|source| CliError::Write {
path: file.display().to_string(),
source,
})?;
}
if let Some(cache) = watch.cache {
let keys: Vec<String> = runs.iter().flat_map(|r| r.passed_keys.clone()).collect();
cache.record(&keys);
}
if let Some(file) = watch.proofs {
write_proofs(file, watch.cache, &runs)?;
}
if let (Some(facts), Some(hold)) = (facts, &guard.hold) {
let evidence: Vec<crate::facts::FileEvidence<'_>> = runs
.iter()
.map(|r| crate::facts::FileEvidence {
path: &r.path,
report: r.report.as_ref(),
identities: r.identities.as_ref(),
})
.collect();
crate::facts::write(facts, hold, &evidence).map_err(CliError::Fmt)?;
}
let (mut total, mut passed, mut failed, mut skipped, mut broken) = (0, 0, 0, 0, 0);
let mut cached = 0;
for run in &runs {
eprint!("{}", run.errors);
match &run.report {
Some(r) => {
total += r.total();
passed += r.passed;
failed += r.failures.len();
skipped += r.skipped.len();
cached += r.cached.len();
if tree {
println!("{}: {}", run.path.display(), r.tally());
}
}
None => broken += 1,
}
if let Some(rows) = &run.coverage {
print_coverage(&run.path, rows, run.arms.as_deref(), json)?;
}
}
let skips = if skipped > 0 {
format!(", {skipped} skipped")
} else {
String::new()
};
let cached = if cached > 0 {
format!(", {cached} cached")
} else {
String::new()
};
if let Some(laws) = law_tally(&runs) {
println!("{laws}");
}
let tally = format!("{total} test(s): {passed} passed, {failed} failed{skips}{cached}");
if tree {
let files = runs.len();
let broken = if broken > 0 {
format!(", {broken} file(s) did not check")
} else {
String::new()
};
println!("{files} file(s), {tally}{broken}");
} else if broken == 0 {
println!("{tally}");
}
if total == 0 && broken == 0 {
match filter {
Some(f) => eprintln!("{}: no test name contains {f:?}", path.display()),
None => eprintln!(
"{}: no `test` blocks, so nothing was tested",
path.display()
),
}
return Ok(ExitCode::FAILURE);
}
let quarantined: usize = held.iter().map(Vec::len).sum();
if quarantined > 0 {
println!("{quarantined} quarantined test(s) held out of the verdict");
}
Ok(if failed == 0 && broken == 0 && unstable == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
const REPOSITORY_ENV: &[&str] = &[
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_PREFIX",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_COMMON_DIR",
"GIT_NAMESPACE",
];
fn law_tally(runs: &[FileRun]) -> Option<String> {
use blue_lang_check::laws::Tier;
let names = runs.iter().filter_map(|r| r.report.as_ref()).flat_map(|r| {
r.outcomes
.iter()
.map(|o| o.name.as_str())
.chain(r.cached.iter().map(String::as_str))
});
let (mut proved, mut refuted, mut tested, mut unchecked) = (0, 0, 0, 0);
for tier in names.filter_map(Tier::of_test) {
match tier {
Tier::Proved => proved += 1,
Tier::Refuted => refuted += 1,
Tier::Tested(_) => tested += 1,
Tier::Unchecked => unchecked += 1,
}
}
let total = proved + refuted + tested + unchecked;
(total > 0).then(|| {
format!("{total} law(s): {proved} proved, {refuted} refuted, {tested} tested, {unchecked} unchecked")
})
}
fn hermetic() -> Result<(), CliError> {
match std::env::var("BLUE_TEST_ENV").as_deref() {
Err(_) | Ok("scrub") => {
for name in REPOSITORY_ENV {
std::env::remove_var(name);
}
}
Ok("inherit") => {}
Ok(other) => {
return Err(CliError::Usage(format!(
"BLUE_TEST_ENV is \"{other}\"; it takes scrub (the default) or inherit"
)))
}
}
match std::env::var("BLUE_TEST_WRITES").as_deref() {
Err(_) | Ok("temp") => {
blue_lang_runtime::sys::confine_writes(Some(vec![std::env::temp_dir()]));
}
Ok("anywhere") => blue_lang_runtime::sys::confine_writes(None),
Ok(other) => {
return Err(CliError::Usage(format!(
"BLUE_TEST_WRITES is \"{other}\"; it takes temp (the default) or anywhere"
)))
}
}
Ok(())
}
fn refuse_skips() -> Result<bool, CliError> {
match std::env::var("BLUE_TEST_SKIP").as_deref() {
Err(_) | Ok("allow") => Ok(false),
Ok("refuse") => Ok(true),
Ok(other) => Err(CliError::Usage(format!(
"BLUE_TEST_SKIP is \"{other}\"; it takes allow (the default) or refuse"
))),
}
}
fn test_files(root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut dirs = vec![root.to_path_buf()];
while let Some(dir) = dirs.pop() {
if !std::fs::canonicalize(&dir).is_ok_and(|c| seen.insert(c)) {
continue;
}
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
let Ok(kind) = std::fs::metadata(&path).map(|m| m.file_type()) else {
continue;
};
if kind.is_dir() {
if !name.starts_with('.') && name != "target" && !name.starts_with("result") {
dirs.push(path);
}
} else if kind.is_file()
&& path.extension().is_some_and(|e| e == "b")
&& std::fs::read_to_string(&path).is_ok_and(|t| {
t.lines().any(|l| {
l.starts_with("test \"")
|| l.starts_with("@doc ")
|| l.starts_with("@moduledoc ")
})
})
{
out.push(path);
}
}
}
out.sort();
out
}
fn longest_first(files: &[PathBuf]) -> Vec<usize> {
let size = |p: &PathBuf| std::fs::metadata(p).map_or(0, |m| m.len());
let mut order: Vec<usize> = (0..files.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(size(&files[i])));
order
}
fn run_files(
files: &[PathBuf],
watch: &Watch<'_>,
guard: &blue_lang_test::Guard,
identify: bool,
) -> Result<Vec<FileRun>, CliError> {
let workers = std::thread::available_parallelism()
.map_or(1, std::num::NonZero::get)
.min(files.len().max(1));
let order = longest_first(files);
let next = AtomicUsize::new(0);
let mut slots: Vec<Option<Result<FileRun, CliError>>> =
std::iter::repeat_with(|| None).take(files.len()).collect();
let results = std::sync::Mutex::new(&mut slots);
std::thread::scope(|scope| {
for _ in 0..workers {
std::thread::Builder::new()
.stack_size(64 << 20)
.spawn_scoped(scope, || loop {
let Some(&i) = order.get(next.fetch_add(1, Ordering::Relaxed)) else {
break;
};
let path = &files[i];
let run = test_file(path, watch, guard, identify).or_else(|e| {
Ok(FileRun {
path: path.clone(),
errors: format!("blue: {}: {e}\n", path.display()),
report: None,
coverage: None,
arms: None,
identities: None,
passed_keys: Vec::new(),
cached_keys: Vec::new(),
})
});
if let Ok(mut slots) = results.lock() {
slots[i] = Some(run);
}
})
.expect("spawn a test worker");
}
});
slots.into_iter().flatten().collect()
}
fn test_file(
path: &Path,
watch: &Watch<'_>,
guard: &blue_lang_test::Guard,
identify: bool,
) -> Result<FileRun, CliError> {
use std::fmt::Write as _;
let own;
let guard = match blue_lang_pkg::capabilities::bounds_for(path) {
Some(bounds) => {
own = blue_lang_test::Guard {
bounds: Some(bounds),
..guard.clone()
};
&own
}
None => guard,
};
let (filter, coverage) = (watch.filter, watch.coverage);
let memo = crate::memo::unless_bounded(watch.memo, guard.bounds);
let src = read_compiled(path)?;
let checked = blue_lang_runtime::pipeline::check_entry(
Entry {
path: Some(path),
text: &src,
},
&blue_lang_pkg::load_path::LoadPath::for_entry(path),
None,
blue_lang_runtime::pipeline::Checking::WithTests,
)?;
let mut errors = String::new();
for d in &checked.outcome.diagnostics {
let _ = writeln!(
errors,
"{}",
blue_lang_runtime::pipeline::render(&checked.program, d)
);
}
if !checked.outcome.ok() {
let _ = writeln!(
errors,
"blue: {}: the check stage rejected the file, so no test ran",
path.display()
);
return Ok(FileRun {
path: path.to_path_buf(),
errors,
report: None,
coverage: None,
arms: None,
identities: None,
passed_keys: Vec::new(),
cached_keys: Vec::new(),
});
}
let memoized = memo != crate::memo::Mode::Off && !coverage;
let witnessing = crate::witness::enabled();
let lowered = (coverage || memoized || witnessing).then(|| checked.lowered());
let watched = lowered
.as_ref()
.filter(|_| coverage || memoized)
.map(|lowered| {
let erased = blue_lang_runtime::erase_types(lowered);
let defs = Arc::new(Definitions::of(&checked.program, &erased));
(lowered, erased, defs)
});
let recorder = watched
.as_ref()
.filter(|_| coverage)
.map(|(_, _, defs)| Arc::new(Recorder::new(Arc::clone(defs), false, None)));
let recall = watched
.as_ref()
.filter(|_| memoized)
.map(|(lowered, erased, defs)| {
crate::memo::over(memo.persist(), defs, lowered, erased, None)
});
let watching = match (&recall, &recorder) {
(Some(m), _) => Some(Arc::clone(m) as Arc<dyn CallObserver>),
(None, Some(r)) => Some(Arc::clone(r) as Arc<dyn CallObserver>),
(None, None) => None,
};
let witness = lowered
.as_ref()
.filter(|_| witnessing)
.map(|lowered| crate::witness::over(lowered, &checked.program, watching.clone()));
let observer = match &witness {
Some(w) => Some(Arc::clone(w) as Arc<dyn CallObserver>),
None => watching,
};
let identities = (identify || watch.cache.is_some()).then(|| {
let lowered = checked.lowered();
blue_lang_mondou::def_hash::test_identities(
&lowered,
&blue_lang_mondou::program_docs(&checked.program),
)
});
let bounds = format!(
"{:?} {:?} {:?}",
guard.bounds,
blue_lang_runtime::execution_bounds(),
guard.max_alloc_bytes
);
let key_of = |test: usize| {
watch
.cache
.zip(identities.as_ref())
.and_then(|(cache, ids)| cache.key(ids, test, &bounds))
};
let own;
let mut cached_keys = Vec::new();
let guard = match (watch.cache, &identities) {
(Some(cache), Some(ids)) => {
let hits: Vec<(usize, String)> = ids
.tests
.keys()
.filter_map(|&t| key_of(t).filter(|k| cache.passed(k)).map(|k| (t, k)))
.collect();
own = blue_lang_test::Guard {
cached: hits.iter().map(|(t, _)| *t).collect(),
..guard.clone()
};
cached_keys = hits.into_iter().map(|(_, k)| k).collect();
&own
}
_ => guard,
};
let arms = recorder
.as_ref()
.map(|_| Arc::new(blue_lang_test::Arms::default()));
let armed;
let guard = match &arms {
Some(a) => {
armed = blue_lang_test::Guard {
arms: Some(Arc::clone(a)),
..guard.clone()
};
&armed
}
None => guard,
};
let report = blue_lang_test::run_guarded(&checked, filter, observer, guard);
let passed_keys = if report.preamble_host_io == 0 {
report
.outcomes
.iter()
.filter(|o| o.passed && o.host_io == 0)
.filter_map(|o| o.top_level.and_then(key_of))
.collect()
} else {
Vec::new()
};
let identities = identities.filter(|_| identify);
if let Some(m) = &recall {
crate::memo::finish(m);
}
if let Some(w) = &witness {
crate::witness::finish(w);
}
let mut report = report;
if refuse_skips()? {
report.refuse_skips();
}
for failure in &report.failures {
let _ = writeln!(errors, "{failure}");
}
for skip in &report.skipped {
let _ = writeln!(errors, "{skip}");
}
Ok(FileRun {
path: path.to_path_buf(),
errors,
report: Some(report),
coverage: recorder.map(|r| r.report()),
arms,
identities,
passed_keys,
cached_keys,
})
}
fn b3(bytes: &[u8; 32]) -> String {
format!("b3:{}", blue_lang_mondou::def_hash::DefHash(*bytes).hex())
}
fn write_proofs(
file: &Path,
cache: Option<&crate::test_cache::TestCache>,
runs: &[FileRun],
) -> Result<(), CliError> {
let cached: Vec<serde_json::Value> = runs
.iter()
.flat_map(|r| r.cached_keys.iter().map(move |k| (r, k)))
.filter_map(|(r, k)| {
let proof = cache?.prove(k)?;
Some(serde_json::json!({
"file": r.path.display().to_string(),
"key": format!("b3:{k}"),
"index": proof.index,
"path": proof.path.iter().map(b3).collect::<Vec<_>>(),
}))
})
.collect();
let doc = serde_json::json!({
"root": cache.map(|c| b3(&c.root())),
"total": cache.map_or(0, crate::test_cache::TestCache::total),
"cached": cached,
});
std::fs::write(file, format!("{doc}\n"))
.map_err(|e| CliError::Fmt(format!("{}: {e}", file.display())))
}
fn print_coverage(
path: &Path,
rows: &[Measured],
arms: Option<&blue_lang_test::Arms>,
json: bool,
) -> Result<(), CliError> {
let defs: Vec<&Measured> = rows
.iter()
.filter(|m| m.entry && m.definition.is_some())
.collect();
let covered = defs.iter().filter(|m| m.counts.calls > 0).count();
let untaken = arms.map(blue_lang_test::Arms::untaken).unwrap_or_default();
if json {
let line = serde_json::json!({
"file": path.display().to_string(),
"covered": covered,
"total": defs.len(),
"definitions": defs.iter().map(|m| serde_json::json!({
"name": m.name,
"line": m.place.line,
"calls": m.counts.calls,
})).collect::<Vec<_>>(),
"uncovered": defs.iter().filter(|m| m.counts.calls == 0)
.map(|m| m.name.clone()).collect::<Vec<_>>(),
"arms": arms.map_or(0, blue_lang_test::Arms::total),
"untaken": untaken,
});
println!("{}", serde_json::to_string(&line)?);
return Ok(());
}
for arm in &untaken {
println!(" untaken arm {arm}");
}
println!("coverage: {}", path.display());
println!(" {:>8} {:<32} defined at", "calls", "definition");
for m in &defs {
let mark = if m.counts.calls == 0 {
" uncovered"
} else {
""
};
println!(" {:>8} {:<32} {}{mark}", m.counts.calls, m.name, m.place);
}
let pct = if defs.is_empty() {
100.0
} else {
#[allow(clippy::cast_precision_loss)]
let p = covered as f64 * 100.0 / defs.len() as f64;
p
};
println!(" {covered}/{} definitions covered ({pct:.1}%)", defs.len());
Ok(())
}
fn run_watched(
file: &Path,
args: Vec<String>,
cfg: &BlueConfig,
watch: impl FnOnce(&blue_lang_runtime::pipeline::Watched<'_>) -> Arc<dyn CallObserver>,
) -> Result<blue_lang_runtime::pipeline::Observed, CliError> {
blue_lang_runtime::sys::set_program_args(args);
let src = crate::read_run(file)?;
let inputs = crate::bind_inputs(&src, &[], cfg)?;
Ok(blue_lang_runtime::pipeline::run_observed_accounted(
Entry {
path: Some(file),
text: &src,
},
inputs,
&blue_lang_pkg::load_path::LoadPath::for_entry(file),
None,
None,
&crate::receipt::account(None),
watch,
)?)
}
#[allow(clippy::cast_precision_loss)]
fn ms(d: Duration) -> f64 {
d.as_secs_f64() * 1000.0
}
pub(crate) fn profile(
file: &Path,
top: usize,
json: bool,
memo: crate::memo::Mode,
args: Vec<String>,
cfg: &BlueConfig,
) -> Result<ExitCode, CliError> {
let mut recorder = None;
let mut memoized = None;
let started = Instant::now();
let observed = run_watched(file, args, cfg, |w| {
let r = Arc::new(Recorder::new(Arc::clone(w.definitions), true, None));
recorder = Some(Arc::clone(&r));
if memo == crate::memo::Mode::Off {
return r;
}
let m = crate::memo::over(memo.persist(), w.definitions, w.lowered, w.erased, Some(r));
memoized = Some(Arc::clone(&m));
m
})?;
let total = started.elapsed();
if let Some(m) = &memoized {
crate::memo::finish(m);
}
let memo_report = memoized.as_ref().map(|m| m.report());
let counted = recorder.as_ref().is_some_and(|r| r.counts_allocations());
let mut rows: Vec<Measured> = recorder
.map(|r| r.report())
.unwrap_or_default()
.into_iter()
.filter(|m| m.counts.calls > 0)
.collect();
rows.sort_by(|a, b| {
b.counts
.exclusive
.cmp(&a.counts.exclusive)
.then_with(|| a.name.cmp(&b.name))
});
let failed = report_outcome(&observed.outcome);
if json {
let mut line = serde_json::json!({
"file": file.display().to_string(),
"total_ms": ms(total),
"functions": rows.iter().take(top).map(|m| serde_json::json!({
"name": m.name,
"file": m.place.file,
"line": m.place.line,
"calls": m.counts.calls,
"inclusive_ms": ms(m.counts.inclusive),
"exclusive_ms": ms(m.counts.exclusive),
"inclusive_allocations": counted.then_some(m.counts.allocated.count),
"inclusive_bytes": counted.then_some(m.counts.allocated.bytes),
"exclusive_allocations": counted.then_some(m.counts.allocated_exclusive.count),
"exclusive_bytes": counted.then_some(m.counts.allocated_exclusive.bytes),
})).collect::<Vec<_>>(),
});
if let Some(r) = &memo_report {
line["memo"] = crate::memo::json(r);
}
println!("{}", serde_json::to_string(&line)?);
} else {
println!(
"profile: {} ({:.1} ms wall, parse and check included; {} function(s) called)",
file.display(),
ms(total),
rows.len()
);
println!(
" {:>10} {:>10} {:>10} {:>6} {:>10} {:>10} {:<32} defined at",
"calls", "incl ms", "excl ms", "excl%", "excl alloc", "excl KiB", "function"
);
for m in rows.iter().take(top) {
#[allow(clippy::cast_precision_loss)]
let share = ms(m.counts.exclusive) * 100.0 / ms(total).max(f64::MIN_POSITIVE);
let (allocs, kib) = if counted {
#[allow(clippy::cast_precision_loss)]
let kib = m.counts.allocated_exclusive.bytes as f64 / 1024.0;
(
m.counts.allocated_exclusive.count.to_string(),
format!("{kib:.1}"),
)
} else {
("-".to_string(), "-".to_string())
};
println!(
" {:>10} {:>10.2} {:>10.2} {:>5.1}% {:>10} {:>10} {:<32} {}",
m.counts.calls,
ms(m.counts.inclusive),
ms(m.counts.exclusive),
share,
allocs,
kib,
m.name,
m.place
);
}
if rows.len() > top {
println!(" … {} more (--top)", rows.len() - top);
}
if let Some(r) = &memo_report {
crate::memo::print(r);
}
}
Ok(if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
})
}
fn report_outcome(outcome: &Result<blue_lang_runtime::Run, blue_lang_runtime::RunError>) -> bool {
match outcome {
Ok(_) => false,
Err(e) => {
eprintln!("blue: {e}");
true
}
}
}
fn short(v: &tatara_lisp_eval::Value) -> String {
let text = blue_lang_runtime::literal::render_value(v);
if text.chars().count() <= VALUE_WIDTH {
return text;
}
let cut: String = text.chars().take(VALUE_WIDTH).collect();
format!("{cut}…")
}
pub(crate) fn trace(
def: &str,
file: &Path,
limit: usize,
args: Vec<String>,
cfg: &BlueConfig,
) -> Result<ExitCode, CliError> {
let src = crate::read_run(file)?;
let checked = blue_lang_runtime::pipeline::check_entry(
Entry {
path: Some(file),
text: &src,
},
&blue_lang_pkg::load_path::LoadPath::for_entry(file),
None,
blue_lang_runtime::pipeline::Checking::Program,
)?;
if checked.outcome.ok()
&& Definitions::of(&checked.program, &checked.erased())
.named(def)
.is_empty()
{
eprintln!(
"blue: {}: no definition named `{def}` in the file or what it imports",
file.display()
);
return Ok(ExitCode::FAILURE);
}
let lines = Arc::new(AtomicUsize::new(0));
let calls = Arc::new(AtomicUsize::new(0));
let observed = {
let lines = Arc::clone(&lines);
let calls = Arc::clone(&calls);
run_watched(file, args, cfg, |w| {
let defs = w.definitions;
let targets = defs.named(def);
let sink = move |event: &TraceEvent<'_>| {
if matches!(event, TraceEvent::Call { .. }) {
calls.fetch_add(1, Ordering::Relaxed);
}
let n = lines.fetch_add(1, Ordering::Relaxed);
if n == limit {
eprintln!("… trace stopped after {limit} line(s) (--limit)");
}
if n >= limit {
return;
}
let render = |args: &[tatara_lisp_eval::Value]| {
args.iter().map(short).collect::<Vec<_>>().join(", ")
};
match event {
TraceEvent::Call { depth, name, args } => {
eprintln!("{}-> {name}({})", " ".repeat(*depth), render(args));
}
TraceEvent::Exit {
depth,
name,
args,
exit,
} => {
let how = match exit {
Exit::Returned(v) => format!("= {}", short(v)),
Exit::Supplied(v) => format!("= {} (supplied)", short(v)),
Exit::TailCalled => "tail call".to_string(),
Exit::Raised(e) => format!("raised: {}", e.short_message()),
};
eprintln!("{}<- {name}({}) {how}", " ".repeat(*depth), render(args));
}
}
};
Arc::new(Recorder::new(
Arc::clone(defs),
false,
Some(Tracer {
targets,
sink: Box::new(sink),
}),
))
})?
};
let failed = report_outcome(&observed.outcome);
eprintln!("{def}: {} call(s)", calls.load(Ordering::Relaxed));
Ok(if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
})
}
pub(crate) fn error_json(e: &blue_lang_runtime::RunError) -> serde_json::Value {
use blue_lang_runtime::RunError;
let stage = |stage: &str, message: String| serde_json::json!({ "stage": stage, "severity": "error", "message": message });
match e {
RunError::Eval(fault) => fault.json(),
RunError::Parse(m) => stage("parse", m.clone()),
RunError::Types(lines) => stage("check", lines.join("\n")),
RunError::Import(m) => stage("import", m.clone()),
RunError::Lower(m) => stage("lower", m.clone()),
}
}
#[cfg(test)]
mod tests {
use super::{longest_first, test_files};
#[test]
fn the_largest_file_starts_first_and_ties_keep_their_order() {
let base = std::env::temp_dir().join(format!("blue-longest-{}", std::process::id()));
std::fs::create_dir_all(&base).unwrap();
let files: Vec<std::path::PathBuf> = [("a.b", 1), ("b.b", 30), ("c.b", 1), ("d.b", 7)]
.iter()
.map(|(n, len)| {
let p = base.join(n);
std::fs::write(&p, "x".repeat(*len)).unwrap();
p
})
.collect();
let order = longest_first(&files);
std::fs::remove_dir_all(&base).unwrap();
assert_eq!(order, vec![1, 3, 0, 2]);
assert!(longest_first(&[]).is_empty());
}
#[test]
fn a_bidama_of_symlinks_is_walked_as_its_files() {
let base = std::env::temp_dir().join(format!("blue-test-files-{}", std::process::id()));
let real = base.join("real");
let linked = base.join("linked");
std::fs::create_dir_all(&real).unwrap();
std::fs::create_dir_all(&linked).unwrap();
std::fs::write(real.join("k.b"), "test \"one\"\n assert true\nend\n").unwrap();
std::os::unix::fs::symlink(real.join("k.b"), linked.join("k.b")).unwrap();
std::os::unix::fs::symlink(&linked, linked.join("loop")).unwrap();
let found = test_files(&linked);
std::fs::remove_dir_all(&base).unwrap();
assert_eq!(found, vec![linked.join("k.b")]);
}
}