Skip to content

Commit b70cdbd

Browse files
committed
Improve test reliability.
1 parent 916f916 commit b70cdbd

File tree

3 files changed

+201
-11
lines changed

3 files changed

+201
-11
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "json"
7+
8+
module Process
9+
module Metrics
10+
class StableChild
11+
def initialize
12+
@io = IO.popen(["ruby", File.expand_path("stable_child.rb", __dir__)], "r+")
13+
@children = []
14+
end
15+
16+
def process_id
17+
@io.pid
18+
end
19+
20+
def children
21+
@children.dup
22+
end
23+
24+
def close
25+
if io = @io
26+
@io = nil
27+
io.close
28+
end
29+
end
30+
31+
def write_message(**message)
32+
@io.puts(JSON.dump(message))
33+
end
34+
35+
def read_message
36+
if line = @io.gets
37+
return JSON.parse(line, symbolize_names: true)
38+
end
39+
end
40+
41+
def wait_for_message(action)
42+
while message = read_message
43+
if message[:action] == action
44+
# Track forked children
45+
if action == "forked" && message[:child_pid]
46+
@children << message[:child_pid]
47+
end
48+
49+
return message
50+
end
51+
end
52+
end
53+
end
54+
55+
AStableProcess = Sus::Shared("a stable process") do
56+
around do |&block|
57+
begin
58+
@child = StableChild.new
59+
@pid = @child.process_id
60+
61+
# Wait for child to be ready
62+
@child.wait_for_message("ready")
63+
64+
super(&block)
65+
ensure
66+
@child&.close
67+
@child = nil
68+
@pid = nil
69+
end
70+
end
71+
end
72+
end
73+
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "securerandom"
7+
require "json"
8+
9+
def read_message
10+
if line = $stdin.gets
11+
return JSON.parse(line, symbolize_names: true)
12+
end
13+
end
14+
15+
def write_message(**message)
16+
$stdout.puts(JSON.dump(message))
17+
$stdout.flush
18+
end
19+
20+
begin
21+
write_message(action: "ready")
22+
23+
allocations = []
24+
children = []
25+
26+
while message = read_message
27+
case message[:action]
28+
when "allocate"
29+
allocations << SecureRandom.bytes(message[:size])
30+
write_message(action: "allocated", size: message[:size])
31+
when "free"
32+
allocations.pop
33+
write_message(action: "freed")
34+
when "clear"
35+
allocations.clear
36+
write_message(action: "cleared")
37+
when "fork"
38+
# Fork a child process in its own process group
39+
child_pid = fork do
40+
# Create a new process group for this child
41+
Process.setpgid(0, 0)
42+
43+
# Sleep forever - will be terminated by signal
44+
sleep
45+
end
46+
47+
children << child_pid
48+
write_message(action: "forked", child_pid: child_pid, children_count: children.size)
49+
when "stabilize"
50+
# Give the OS a moment to settle any page allocations
51+
sleep 0.1
52+
write_message(action: "stabilized")
53+
when "exit"
54+
break
55+
end
56+
end
57+
rescue Interrupt
58+
# Ignore - normal exit.
59+
ensure
60+
# Clean up any child processes using their process groups
61+
children.each do |child_pid|
62+
begin
63+
# Kill the process group (negative PID)
64+
Process.kill(:TERM, -child_pid)
65+
Process.wait(child_pid)
66+
rescue Errno::ESRCH, Errno::ECHILD
67+
# Child already exited
68+
end
69+
end
70+
end

test/process/general/linux.rb

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
require "process/metrics"
77
# Load ProcessStatus backend so we can compare General (Linux) vs General::ProcessStatus.
88
require "process/metrics/general/process_status"
9+
require_relative "../../../fixtures/process/metrics/a_stable_process"
910

1011
describe Process::Metrics::General do
1112
with "Linux backend matches ProcessStatus backend" do
13+
include_context Process::Metrics::AStableProcess
14+
1215
def assert_backends_match(linux_capture, process_status_capture)
1316
expect(linux_capture.keys.sort).to be == process_status_capture.keys.sort
1417

@@ -22,6 +25,7 @@ def assert_backends_match(linux_capture, process_status_capture)
2225
expect(linux_process.process_group_id).to be == process_status_process.process_group_id
2326

2427
# VSZ and RSS differ because ps excludes device mappings while /proc/stat includes them.
28+
# With a stable controlled process, RSS should be more consistent between measurements.
2529
expect(linux_process.virtual_size).to be_within(10.0).percent_of(process_status_process.virtual_size)
2630
expect(linux_process.resident_size).to be_within(10.0).percent_of(process_status_process.resident_size)
2731

@@ -34,24 +38,67 @@ def assert_backends_match(linux_capture, process_status_capture)
3438
it "single pid capture matches" do
3539
skip "Linux with ProcessStatus required" unless RUBY_PLATFORM.include?("linux") && Process::Metrics::General::ProcessStatus.supported?
3640

37-
pid = Process.pid
38-
linux_capture = Process::Metrics::General.capture(pid: pid, memory: false)
39-
process_status_capture = Process::Metrics::General::ProcessStatus.capture(pid: pid, memory: false)
41+
# Stabilize the child process before taking measurements
42+
@child.write_message(action: "stabilize")
43+
@child.wait_for_message("stabilized")
44+
45+
linux_capture = Process::Metrics::General.capture(pid: @pid, memory: false)
46+
process_status_capture = Process::Metrics::General::ProcessStatus.capture(pid: @pid, memory: false)
4047
assert_backends_match(linux_capture, process_status_capture)
4148
end
4249

4350
it "pid and ppid capture matches" do
4451
skip "Linux with ProcessStatus required" unless RUBY_PLATFORM.include?("linux") && Process::Metrics::General::ProcessStatus.supported?
4552

46-
child_pid = Process.spawn("sleep 10")
47-
begin
48-
linux_capture = Process::Metrics::General.capture(pid: child_pid, ppid: child_pid, memory: false)
49-
process_status_capture = Process::Metrics::General::ProcessStatus.capture(pid: child_pid, ppid: child_pid, memory: false)
50-
assert_backends_match(linux_capture, process_status_capture)
51-
ensure
52-
Process.kill(:TERM, child_pid)
53-
Process.wait(child_pid)
53+
# Stabilize the child process before taking measurements
54+
@child.write_message(action: "stabilize")
55+
@child.wait_for_message("stabilized")
56+
57+
linux_capture = Process::Metrics::General.capture(pid: @pid, ppid: @pid, memory: false)
58+
process_status_capture = Process::Metrics::General::ProcessStatus.capture(pid: @pid, ppid: @pid, memory: false)
59+
assert_backends_match(linux_capture, process_status_capture)
60+
end
61+
62+
it "captures child processes by ppid" do
63+
skip "Linux with ProcessStatus required" unless RUBY_PLATFORM.include?("linux") && Process::Metrics::General::ProcessStatus.supported?
64+
65+
# Fork 2 child processes
66+
@child.write_message(action: "fork")
67+
response1 = @child.wait_for_message("forked")
68+
69+
@child.write_message(action: "fork")
70+
response2 = @child.wait_for_message("forked")
71+
72+
child_pids = [response1[:child_pid], response2[:child_pid]]
73+
74+
# Stabilize before measuring
75+
@child.write_message(action: "stabilize")
76+
@child.wait_for_message("stabilized")
77+
78+
# Capture using ppid - should get parent + both children
79+
linux_capture = Process::Metrics::General.capture(ppid: @pid, memory: false)
80+
process_status_capture = Process::Metrics::General::ProcessStatus.capture(ppid: @pid, memory: false)
81+
82+
# Should have captured 3 processes: parent + 2 children
83+
expect(linux_capture.size).to be == 3
84+
expect(process_status_capture.size).to be == 3
85+
86+
# Verify parent is included
87+
expect(linux_capture.keys).to be(:include?, @pid)
88+
expect(process_status_capture.keys).to be(:include?, @pid)
89+
90+
# Verify both children are included
91+
child_pids.each do |child_pid|
92+
expect(linux_capture.keys).to be(:include?, child_pid)
93+
expect(process_status_capture.keys).to be(:include?, child_pid)
94+
95+
# Verify parent-child relationship
96+
expect(linux_capture[child_pid].parent_process_id).to be == @pid
97+
expect(process_status_capture[child_pid].parent_process_id).to be == @pid
5498
end
99+
100+
# Compare all processes
101+
assert_backends_match(linux_capture, process_status_capture)
55102
end
56103
end
57104
end

0 commit comments

Comments
 (0)