Skip to content

Commit b2bdb43

Browse files
authored
test: Add integration tests for pstack (#107)
1 parent 44c2b4b commit b2bdb43

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

examples/pstack_process.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Copyright (c) 2026 Basil Crow
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
use std::env;
18+
use std::fs::File;
19+
use std::thread;
20+
use std::time::Duration;
21+
22+
fn allow_ptrace_for_tests() {
23+
// On Linux with Yama ptrace_scope=1 (the Ubuntu default), only a
24+
// process's descendants may ptrace it. Since the test harness spawns
25+
// this process and the ptool as siblings, we opt in to tracing by any
26+
// process so the tests work without elevated privileges.
27+
unsafe {
28+
nix::libc::prctl(
29+
nix::libc::PR_SET_PTRACER,
30+
nix::libc::PR_SET_PTRACER_ANY,
31+
0,
32+
0,
33+
0,
34+
);
35+
}
36+
}
37+
38+
fn main() {
39+
allow_ptrace_for_tests();
40+
41+
let signal_path =
42+
env::var("PTOOLS_TEST_READY_FILE").expect("PTOOLS_TEST_READY_FILE must be set");
43+
44+
// Signal parent process (the test process) that this process is ready to be observed by the
45+
// ptool being tested.
46+
File::create(signal_path).unwrap();
47+
48+
// Wait for the parent to finish running the ptool and then kill us.
49+
loop {
50+
thread::sleep(Duration::from_millis(100));
51+
}
52+
}

tests/pstack_test.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//
2+
// Copyright (c) 2026 Basil Crow
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
mod common;
18+
19+
#[test]
20+
fn pstack_prints_stack_trace() {
21+
let output = common::run_ptool("pstack", &[], "examples/pstack_process", &[], &[], false);
22+
let stdout = common::assert_success_and_get_stdout(output);
23+
24+
let mut lines = stdout.lines();
25+
let summary = lines
26+
.next()
27+
.unwrap_or_else(|| panic!("Expected process summary line in pstack output:\n{stdout}"));
28+
assert!(
29+
summary.contains("pstack_process"),
30+
"Expected summary to contain pstack_process executable name:\n{stdout}"
31+
);
32+
33+
// At least one frame line should be present with a hex instruction pointer.
34+
let frame_lines: Vec<&str> = stdout
35+
.lines()
36+
.filter(|line| line.starts_with("0x"))
37+
.collect();
38+
assert!(
39+
!frame_lines.is_empty(),
40+
"Expected at least one stack frame with hex address:\n{stdout}"
41+
);
42+
43+
// Each frame line should have a symbol or "???" marker.
44+
for frame in &frame_lines {
45+
assert!(
46+
frame.contains('+') || frame.contains("???"),
47+
"Expected frame to contain symbol+offset or ???:\n{frame}"
48+
);
49+
}
50+
}
51+
52+
#[test]
53+
fn pstack_module_flag_shows_module_paths() {
54+
let output = common::run_ptool(
55+
"pstack",
56+
&["-m"],
57+
"examples/pstack_process",
58+
&[],
59+
&[],
60+
false,
61+
);
62+
let stdout = common::assert_success_and_get_stdout(output);
63+
64+
let frame_lines: Vec<&str> = stdout
65+
.lines()
66+
.filter(|line| line.starts_with("0x"))
67+
.collect();
68+
assert!(
69+
!frame_lines.is_empty(),
70+
"Expected at least one stack frame:\n{stdout}"
71+
);
72+
73+
// With -m, at least one frame should show an "in" clause with a module path.
74+
let has_module = frame_lines.iter().any(|line| line.contains(" in "));
75+
assert!(
76+
has_module,
77+
"Expected at least one frame to show a module path with -m:\n{stdout}"
78+
);
79+
}
80+
81+
#[test]
82+
fn pstack_verbose_shows_source_locations() {
83+
let output = common::run_ptool(
84+
"pstack",
85+
&["-v"],
86+
"examples/pstack_process",
87+
&[],
88+
&[],
89+
false,
90+
);
91+
let stdout = common::assert_success_and_get_stdout(output);
92+
93+
let frame_lines: Vec<&str> = stdout
94+
.lines()
95+
.filter(|line| line.starts_with("0x"))
96+
.collect();
97+
assert!(
98+
!frame_lines.is_empty(),
99+
"Expected at least one stack frame:\n{stdout}"
100+
);
101+
102+
// With -v, frames with debug info should show source locations as (file:line).
103+
let has_source = frame_lines
104+
.iter()
105+
.any(|line| line.contains("pstack_process.rs:"));
106+
assert!(
107+
has_source,
108+
"Expected at least one frame to show a source location from pstack_process.rs:\n{stdout}"
109+
);
110+
}
111+
112+
#[test]
113+
fn pstack_n_limits_frame_count() {
114+
let output = common::run_ptool(
115+
"pstack",
116+
&["-n", "2"],
117+
"examples/pstack_process",
118+
&[],
119+
&[],
120+
false,
121+
);
122+
assert!(output.status.success(), "pstack should exit 0");
123+
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
124+
125+
let frame_count = stdout.lines().filter(|line| line.starts_with("0x")).count();
126+
assert!(
127+
frame_count <= 2,
128+
"Expected at most 2 frames with -n 2, got {frame_count}:\n{stdout}"
129+
);
130+
}

0 commit comments

Comments
 (0)