Skip to content

Commit c566d91

Browse files
Linux specific tests.
1 parent 086a334 commit c566d91

3 files changed

Lines changed: 111 additions & 12 deletions

File tree

lib/process/metrics/general/linux.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ def self.capture(pid: nil, ppid: nil, memory: Memory.supported?)
5151

5252
comm = stat[1...rparen]
5353
fields = stat[(rparen + 2)..].split(/\s+/)
54-
# After comm: state(3), ppid(4), pgrp(5), ... utime(14), stime(15), ... starttime(22), vsz(23), rss(24). 0-based: ppid=1, pgrp=2, utime=11, stime=12, starttime=20, vsz=21, rss=22
54+
# After comm: state(3), ppid(4), pgrp(5), ... utime(14), stime(15), ... starttime(22), vsz(23), rss(24). 0-based: ppid=1, pgrp=2, utime=11, stime=12, starttime=19, vsz=20, rss=21.
5555
ppid_val = fields[1].to_i
5656
pgrp = fields[2].to_i
5757
utime = fields[11].to_i
5858
stime = fields[12].to_i
59-
starttime = fields[20].to_i
60-
vsz = fields[21].to_i
61-
rss_pages = fields[22].to_i
59+
starttime = fields[19].to_i
60+
vsz = fields[20].to_i
61+
rss_pages = fields[21].to_i
6262

6363
# Read /proc/uptime once per capture and reuse for every process (starttime is in jiffies since boot).
6464
uptime_jiffies ||= begin

test/process/general.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,29 @@
2828
it "can extract memory usage" do
2929
expect(capture[pid].memory_usage).to be > 0.0
3030
end
31-
31+
3232
it "sets parent_process_id and process_group_id" do
3333
process = capture[pid]
3434
expect(process.process_id).to be == pid
3535
expect(process.parent_process_id).to be_a(Integer)
3636
expect(process.process_group_id).to be_a(Integer)
3737
end
3838
end
39-
39+
4040
with ".capture with ppid only" do
4141
def before
4242
super
4343
@child_pid = Process.spawn("sleep 10")
4444
end
45-
45+
4646
def after(error = nil)
4747
super
4848
Process.kill(:TERM, @child_pid) if @child_pid
4949
Process.wait(@child_pid) if @child_pid
5050
end
51-
52-
let(:capture) { Process::Metrics::General.capture(ppid: Process.pid) }
53-
51+
52+
let(:capture) {Process::Metrics::General.capture(ppid: Process.pid)}
53+
5454
it "includes descendants of the given ppid" do
5555
expect(capture).to be(:include?, @child_pid)
5656
child = capture[@child_pid]
@@ -59,7 +59,7 @@ def after(error = nil)
5959
expect(child.parent_process_id).to be == Process.pid
6060
end
6161
end
62-
62+
6363
with ".capture with parent pid" do
6464
def before
6565
super
@@ -92,7 +92,7 @@ def after(error = nil)
9292
expect(command[:processor_time]).to be >= 0.0
9393
expect(command[:processor_utilization]).to be >= 0.0
9494
end
95-
95+
9696
it "sets parent_process_id and process_group_id on child" do
9797
child = capture[@pid]
9898
expect(child).not.to be_nil

test/process/general/linux.rb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024-2025, by Samuel Williams.
5+
6+
require "process/metrics"
7+
# Load ProcessStatus backend so we can compare General (Linux) vs General::ProcessStatus.
8+
require "process/metrics/general/process_status"
9+
10+
describe Process::Metrics::General do
11+
with "Linux backend matches ProcessStatus backend" do
12+
def log_backend_comparison(linux, ps)
13+
$stderr.puts "[process-metrics] RUBY_PLATFORM=#{RUBY_PLATFORM.inspect}"
14+
$stderr.puts "[process-metrics] Linux keys: #{linux.keys.sort.inspect}"
15+
$stderr.puts "[process-metrics] ps keys: #{ps.keys.sort.inspect}"
16+
first = true
17+
linux.each_key do |p|
18+
l = linux[p]
19+
ps_process = ps[p]
20+
$stderr.puts "[process-metrics] pid=#{p}"
21+
$stderr.puts "[process-metrics] Linux: process_id=#{l.process_id} ppid=#{l.parent_process_id} pgid=#{l.process_group_id} vsz=#{l.virtual_size} rss=#{l.resident_size} command=#{l.command.inspect} processor_time=#{l.processor_time} elapsed_time=#{l.elapsed_time}"
22+
if ps_process
23+
$stderr.puts "[process-metrics] ps: process_id=#{ps_process.process_id} ppid=#{ps_process.parent_process_id} pgid=#{ps_process.process_group_id} vsz=#{ps_process.virtual_size} rss=#{ps_process.resident_size} command=#{ps_process.command.inspect} processor_time=#{ps_process.processor_time} elapsed_time=#{ps_process.elapsed_time}"
24+
# Per-field match/mismatch (proc(5) stat uses indices 19,20,21 for starttime,vsz,rss after comm).
25+
%i[process_id parent_process_id process_group_id virtual_size resident_size command processor_time elapsed_time].each do |field|
26+
lv = l[field]
27+
pv = ps_process[field]
28+
match = lv == pv
29+
if !match && field == :resident_size && lv.is_a?(Integer) && pv.is_a?(Integer)
30+
rss_tol = [lv * 0.05, 512 * 1024].max
31+
match = (lv - pv).abs <= rss_tol
32+
end
33+
if !match && (field == :processor_time || field == :elapsed_time) && lv.is_a?(Numeric) && pv.is_a?(Numeric)
34+
match = (lv - pv).abs < 1.0
35+
end
36+
$stderr.puts "[process-metrics] #{field}: #{match ? 'ok' : "MISMATCH linux=#{lv.inspect} ps=#{pv.inspect}"}"
37+
end
38+
else
39+
$stderr.puts "[process-metrics] ps: (nil)"
40+
end
41+
# Log raw /proc/pid/stat tail (fields after ') ') for first process to verify field indices.
42+
if first && File.readable?("/proc/#{p}/stat")
43+
stat = File.read("/proc/#{p}/stat")
44+
rparen = stat.rindex(")")
45+
if rparen
46+
tail = stat[(rparen + 2)..]
47+
fields = tail.split(/\s+/)
48+
$stderr.puts "[process-metrics] /proc/#{p}/stat: #{fields.size} fields after ') '; indices 19,20,21 => starttime=#{fields[19].inspect} vsz=#{fields[20].inspect} rss=#{fields[21].inspect}"
49+
end
50+
first = false
51+
end
52+
end
53+
end
54+
55+
def assert_backends_match(linux, ps)
56+
log_backend_comparison(linux, ps)
57+
expect(linux.keys.sort).to be == ps.keys.sort
58+
linux.each_key do |p|
59+
l = linux[p]
60+
ps_process = ps[p]
61+
expect(ps_process.nil?).to be == false
62+
expect(l.process_id).to be == ps_process.process_id
63+
expect(l.parent_process_id).to be == ps_process.parent_process_id
64+
expect(l.process_group_id).to be == ps_process.process_group_id
65+
expect(l.virtual_size).to be == ps_process.virtual_size
66+
# RSS can differ slightly: /proc uses pages, ps may use KiB; read at different times.
67+
rss_delta = (l.resident_size - ps_process.resident_size).abs
68+
rss_tolerance = [l.resident_size * 0.05, 512 * 1024].max
69+
expect(rss_delta).to be <= rss_tolerance
70+
expect(l.command).to be == ps_process.command
71+
expect((l.processor_time - ps_process.processor_time).abs).to be < 1.0
72+
expect((l.elapsed_time - ps_process.elapsed_time).abs).to be < 1.0
73+
end
74+
end
75+
76+
it "single pid capture matches" do
77+
skip "Linux with ps required" unless RUBY_PLATFORM.include?("linux") && Process::Metrics::General::ProcessStatus.supported?
78+
79+
pid = Process.pid
80+
linux = Process::Metrics::General.capture(pid: pid, memory: false)
81+
ps = Process::Metrics::General::ProcessStatus.capture(pid: pid, memory: false)
82+
assert_backends_match(linux, ps)
83+
end
84+
85+
it "pid and ppid capture matches" do
86+
skip "Linux with ps required" unless RUBY_PLATFORM.include?("linux") && Process::Metrics::General::ProcessStatus.supported?
87+
88+
child_pid = Process.spawn("sleep 10")
89+
begin
90+
linux = Process::Metrics::General.capture(pid: child_pid, ppid: child_pid, memory: false)
91+
ps = Process::Metrics::General::ProcessStatus.capture(pid: child_pid, ppid: child_pid, memory: false)
92+
assert_backends_match(linux, ps)
93+
ensure
94+
Process.kill(:TERM, child_pid)
95+
Process.wait(child_pid)
96+
end
97+
end
98+
end
99+
end

0 commit comments

Comments
 (0)