extern crate py_spy;
use py_spy::{Config, PythonSpy, Pid};
struct ScriptRunner {
#[allow(dead_code)]
child: std::process::Child,
}
impl ScriptRunner {
fn new(filename: &str) -> ScriptRunner {
let child = std::process::Command::new("python").arg(filename).spawn().unwrap();
ScriptRunner{child}
}
fn id(&self) -> Pid { self.child.id() as _ }
}
impl Drop for ScriptRunner {
fn drop(&mut self) {
if let Err(err) = self.child.kill() {
eprintln!("Failed to kill child process {}", err);
}
}
}
struct TestRunner {
#[allow(dead_code)]
child: ScriptRunner,
spy: PythonSpy
}
impl TestRunner {
fn new(config: Config, filename: &str) -> TestRunner {
let child = ScriptRunner::new(filename);
std::thread::sleep(std::time::Duration::from_millis(400));
let spy = PythonSpy::retry_new(child.id(), &config, 20).unwrap();
TestRunner{child, spy}
}
}
#[test]
fn test_busy_loop() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let mut runner = TestRunner::new(Config::default(), "./tests/scripts/busyloop.py");
let traces = runner.spy.get_stack_traces().unwrap();
assert!(traces[0].active);
}
#[cfg(unwind)]
#[test]
fn test_thread_reuse() {
let config = Config{native: true, ..Default::default()};
let mut runner = TestRunner::new(config, "./tests/scripts/thread_reuse.py");
let mut errors = 0;
for _ in 0..100 {
if let Err(e) = runner.spy.get_stack_traces() {
println!("Failed to get traces {}", e);
errors += 1;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
assert!(errors <= 3);
}
#[test]
fn test_long_sleep() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let mut runner = TestRunner::new(Config::default(), "./tests/scripts/longsleep.py");
let traces = runner.spy.get_stack_traces().unwrap();
assert_eq!(traces.len(), 1);
let trace = &traces[0];
assert_eq!(trace.frames[0].name, "longsleep");
assert_eq!(trace.frames[0].filename, "./tests/scripts/longsleep.py");
assert_eq!(trace.frames[0].line, 5);
assert_eq!(trace.frames[1].name, "<module>");
assert_eq!(trace.frames[1].line, 9);
assert_eq!(trace.frames[0].filename, "./tests/scripts/longsleep.py");
assert!(!traces[0].owns_gil);
#[cfg(any(target_os="macos", target_os="windows"))]
assert!(!traces[0].active);
}
#[test]
fn test_recursive() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let mut runner = TestRunner::new(Config::default(), "./tests/scripts/recursive.py");
for _ in 0..100 {
let traces = runner.spy.get_stack_traces().unwrap();
assert_eq!(traces.len(), 1);
let trace = &traces[0];
assert!(trace.frames.len() <= 22);
let top_level_frame = &trace.frames[trace.frames.len()-1];
assert_eq!(top_level_frame.name, "<module>");
assert!((top_level_frame.line == 8) || (top_level_frame.line == 7));
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
#[test]
fn test_unicode() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let mut runner = TestRunner::new(Config::default(), "./tests/scripts/unicode💩.py");
let traces = runner.spy.get_stack_traces().unwrap();
assert_eq!(traces.len(), 1);
let trace = &traces[0];
assert_eq!(trace.frames[0].name, "function1");
assert_eq!(trace.frames[0].filename, "./tests/scripts/unicode💩.py");
assert_eq!(trace.frames[0].line, 6);
assert_eq!(trace.frames[1].name, "<module>");
assert_eq!(trace.frames[1].line, 9);
assert_eq!(trace.frames[0].filename, "./tests/scripts/unicode💩.py");
assert!(!traces[0].owns_gil);
}
#[test]
fn test_local_vars() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let config = Config{dump_locals: true, ..Default::default()};
let mut runner = TestRunner::new(config, "./tests/scripts/local_vars.py");
let traces = runner.spy.get_stack_traces().unwrap();
assert_eq!(traces.len(), 1);
let trace = &traces[0];
assert_eq!(trace.frames.len(), 2);
let frame = &trace.frames[0];
let locals = frame.locals.as_ref().unwrap();
assert_eq!(locals.len(), 9);
let arg1 = &locals[0];
assert_eq!(arg1.name, "arg1");
assert!(arg1.arg);
assert_eq!(arg1.repr, Some("\"foo\"".to_owned()));
let arg2 = &locals[1];
assert_eq!(arg2.name, "arg2");
assert!(arg2.arg);
assert_eq!(arg2.repr, Some("None".to_owned()));
let arg3 = &locals[2];
assert_eq!(arg3.name, "arg3");
assert!(arg3.arg);
assert_eq!(arg3.repr, Some("True".to_owned()));
let local1 = &locals[3];
assert_eq!(local1.name, "local1");
assert!(!local1.arg);
assert_eq!(local1.repr, Some("[-1234, 5678]".to_owned()));
let local2 = &locals[4];
assert_eq!(local2.name, "local2");
assert!(!local2.arg);
assert_eq!(local2.repr, Some("(\"a\", \"b\", \"c\")".to_owned()));
let local3 = &locals[5];
assert_eq!(local3.name, "local3");
assert!(!local3.arg);
assert_eq!(local3.repr, Some("123456789123456789".to_owned()));
let local4 = &locals[6];
assert_eq!(local4.name, "local4");
assert!(!local4.arg);
assert_eq!(local4.repr, Some("3.1415".to_owned()));
let local5 = &locals[7];
assert_eq!(local5.name, "local5");
assert!(!local5.arg);
let local6 = &locals[8];
assert_eq!(local6.name, "local6");
assert!(!local6.arg);
if runner.spy.version.major == 3 && runner.spy.version.minor >= 6 {
assert_eq!(local5.repr, Some("{\"a\": False, \"b\": (1, 2, 3)}".to_owned()));
}
}
#[cfg(not(target_os="freebsd"))]
#[test]
fn test_subprocesses() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let process = ScriptRunner::new("./tests/scripts/subprocesses.py");
std::thread::sleep(std::time::Duration::from_millis(1000));
let config = Config{subprocesses: true, ..Default::default()};
let sampler = py_spy::sampler::Sampler::new(process.id(), &config).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1000));
for sample in sampler {
let traces = sample.traces;
assert_eq!(traces.len(), 3);
assert!(traces[0].pid != traces[1].pid);
assert!(traces[1].pid != traces[2].pid);
break;
}
}
#[cfg(not(target_os="freebsd"))]
#[test]
fn test_subprocesses_zombiechild() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let process = ScriptRunner::new("./tests/scripts/subprocesses_zombie_child.py");
std::thread::sleep(std::time::Duration::from_millis(200));
let config = Config{subprocesses: true, ..Default::default()};
let _sampler = py_spy::sampler::Sampler::new(process.id(), &config).unwrap();
}
#[test]
fn test_negative_linenumber_increment() {
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
return;
}
}
let mut runner = TestRunner::new(Config::default(), "./tests/scripts/negative_linenumber_offsets.py");
let traces = runner.spy.get_stack_traces().unwrap();
assert_eq!(traces.len(), 1);
let trace = &traces[0];
match runner.spy.version.major {
3 => {
assert_eq!(trace.frames[0].name, "<listcomp>");
assert!(trace.frames[0].line >= 5 && trace.frames[0].line <= 10);
assert_eq!(trace.frames[1].name, "f");
assert!(trace.frames[1].line >= 5 && trace.frames[0].line <= 10);
assert_eq!(trace.frames[2].name, "<module>");
assert_eq!(trace.frames[2].line, 13)
},
2 => {
assert_eq!(trace.frames[0].name, "f");
assert!(trace.frames[0].line >= 5 && trace.frames[0].line <= 10);
assert_eq!(trace.frames[1].name, "<module>");
assert_eq!(trace.frames[1].line, 13);
},
_ => panic!("Unknown python major version")
}
}