use super::{Backend, Dependency, SpawnConfig, shell_escape};
use crate::check::find_bin;
use crate::daemon::session_tmp;
pub struct ImportBackend;
impl Backend for ImportBackend {
fn name(&self) -> &'static str {
"import"
}
fn description(&self) -> &'static str {
"import an existing profile snapshot (.nettrace, .speedscope.json, .cpuprofile, perf-script, pprof-traces)"
}
fn types(&self) -> &'static [&'static str] {
&["import"]
}
fn spawn_config(&self, target: &str, _args: &[String]) -> anyhow::Result<SpawnConfig> {
let speedscope_out = session_tmp("imported.speedscope.json");
let speedscope_str = speedscope_out.display().to_string();
let prep_cmd = if extension_matches(target, "nettrace") {
let convert_base = session_tmp("imported");
let trace_bin = find_bin("dotnet-trace");
format!(
"{} convert --format Speedscope {} -o {}",
shell_escape(&trace_bin),
shell_escape(target),
shell_escape(&convert_base.display().to_string()),
)
} else {
format!("cp {} {}", shell_escape(target), shell_escape(&speedscope_str))
};
Ok(SpawnConfig {
bin: "bash".into(),
args: vec!["--norc".into(), "--noprofile".into()],
env: vec![("PS1".into(), "$ ".into())],
init_commands: vec![prep_cmd, "echo '--- import ready ---'".into()],
})
}
fn prompt_pattern(&self) -> &str {
r"\$ $"
}
fn init_timeout(&self) -> std::time::Duration {
std::time::Duration::from_secs(600)
}
fn dependencies(&self) -> Vec<Dependency> {
vec![]
}
fn run_command(&self) -> &'static str {
"top"
}
fn quit_command(&self) -> &'static str {
"exit"
}
fn parse_help(&self, _raw: &str) -> String {
"commands: top [N] [--no-idle], callers <func>, callees <func>, traces [N], tree [N], hotpath, threads, stats, search <pattern>, focus <func>, ignore <func>, window <t0> <t1> | window clear, reset".to_string()
}
fn profile_output(&self) -> Option<String> {
Some(session_tmp("imported.speedscope.json").display().to_string())
}
fn adapters(&self) -> Vec<(&'static str, &'static str)> {
vec![("import.md", include_str!("../../skills/adapters/import.md"))]
}
}
pub(crate) fn extension_matches(path: &str, ext: &str) -> bool {
std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case(ext))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_nettrace_extension_case_insensitive() {
assert!(extension_matches("/tmp/foo.nettrace", "nettrace"));
assert!(extension_matches("/tmp/FOO.NETTRACE", "nettrace"));
assert!(extension_matches("foo.nettrace", "nettrace"));
}
#[test]
fn rejects_non_nettrace() {
assert!(!extension_matches("/tmp/foo.speedscope.json", "nettrace"));
assert!(!extension_matches("/tmp/foo.cpuprofile", "nettrace"));
assert!(!extension_matches("/tmp/foo", "nettrace"));
assert!(!extension_matches("/tmp/foo.txt", "nettrace"));
}
#[test]
fn spawn_config_for_nettrace_runs_convert() {
let cfg = ImportBackend
.spawn_config("/tmp/snap.nettrace", &[])
.unwrap();
assert_eq!(cfg.init_commands.len(), 2);
let cmd = &cfg.init_commands[0];
assert!(
cmd.contains("dotnet-trace"),
"expected dotnet-trace convert step, got: {cmd}"
);
assert!(cmd.contains("convert"));
assert!(cmd.contains("--format Speedscope"));
assert!(cmd.contains("/tmp/snap.nettrace"));
assert!(
cmd.contains("-o ") && cmd.contains("imported"),
"expected `-o <session>/imported`, got: {cmd}"
);
assert!(
!cmd.contains("imported.speedscope.json"),
"convert -o must not include the .speedscope.json suffix; dotnet-trace appends it. cmd: {cmd}"
);
}
#[test]
fn spawn_config_for_speedscope_just_copies() {
let cfg = ImportBackend
.spawn_config("/tmp/snap.speedscope.json", &[])
.unwrap();
assert!(
cfg.init_commands[0].starts_with("cp "),
"expected cp, got: {}",
cfg.init_commands[0]
);
assert!(cfg.init_commands[0].contains("/tmp/snap.speedscope.json"));
assert!(cfg.init_commands[0].contains("imported.speedscope.json"));
}
#[test]
fn profile_output_points_at_session_tmp_target() {
let out = ImportBackend.profile_output().unwrap();
assert!(out.ends_with("imported.speedscope.json"));
}
}