Files
ubicloud/spec/monitor_smoke_test.rb
Jeremy Evans 6629284336 Add monitor smoke test
Unlike the respirate smoke test, where we can run test strands,
monitor is hard coded to use specific models. Change this so that
when it runs in test mode, it uses a stubbed model, a new class
named MonitorResourceStub. MonitorResourceStub stubs methods for both
monitoring and metric exporting.  It's designed to exercise most of
monitor's surface area.

This runs using four stubbed resources:

* up: resource that always reports pulse as up
* down: resource that always reports pulse as down
* evloop: resource that uses an event loop
* mc2: resource that reports a metric count of 2

For each resource, it checks for expected logged output.

It runs 4 separate processes in different partitions.  The stub doesn't
respect the parititioning, and doesn't use the database. The smoke
test does check that the logged messages show expected partitioning.

Similar to the respirate smoke test, run the monitor smoke test
in CI whenever a related file changes.
2025-07-30 07:00:03 +09:00

127 lines
4.0 KiB
Ruby

# frozen_string_literal: true
ENV["RACK_ENV"] = "test"
require "json"
require_relative "../ubid"
r, w = IO.pipe
output = +""
Thread.new do
until (s = r.read(4096).to_s).empty?
output << s
end
rescue
p $!
end
fd_map = {:in => :close, [:out, :err] => w}
monitor_pids = [
Process.spawn({"DYNO" => "monitor.2"}, "bin/monitor", **fd_map),
Process.spawn({"PS" => "monitor.3"}, "bin/monitor", **fd_map),
Process.spawn("bin/monitor", "4", **fd_map),
Process.spawn("bin/monitor", **fd_map)
]
w.close
print("monitor smoke test: ")
10.times do
print "."
sleep 1
end
puts "finished, shutting down processes"
Process.kill(:TERM, *monitor_pids)
clean = nil
Thread.new do
monitor_pids.each do
Process.waitpid(it)
clean = false unless $?.success?
end
clean = true if clean.nil?
end.join(3)
unless clean
warn "Not all monitor processes shutdown cleanly within 3 seconds"
exit 1
end
required_ranges = [
["00000000-0000-0000-0000-000000000000", "40000000-0000-0000-0000-000000000000"], # 1/4
["40000000-0000-0000-0000-000000000000", "80000000-0000-0000-0000-000000000000"], # 2/4
["80000000-0000-0000-0000-000000000000", "c0000000-0000-0000-0000-000000000000"], # 3/4
["c0000000-0000-0000-0000-000000000000", "ffffffff-ffff-ffff-ffff-ffffffffffff"] # 4/4
]
possible_ranges = required_ranges + [
["00000000-0000-0000-0000-000000000000", "ffffffff-ffff-ffff-ffff-ffffffffffff"], # 1/1
["00000000-0000-0000-0000-000000000000", "55555555-0000-0000-0000-000000000000"], # 1/3
["00000000-0000-0000-0000-000000000000", "80000000-0000-0000-0000-000000000000"], # 1/2
["55555555-0000-0000-0000-000000000000", "aaaaaaaa-0000-0000-0000-000000000000"], # 2/3
["80000000-0000-0000-0000-000000000000", "ffffffff-ffff-ffff-ffff-ffffffffffff"], # 2/2
["aaaaaaaa-0000-0000-0000-000000000000", "ffffffff-ffff-ffff-ffff-ffffffffffff"] # 3/3
]
ranges = output.scan(/"range":"([-0-9a-f]+)\.\.\.?([-0-9a-f]+)"/)
ranges.each do
next if possible_ranges.include?(it)
warn "unexpected monitor repartition range: #{it}"
exit 1
end
unless ranges.length.between?(4, 10)
warn "unexpected number of monitor repartitions (should be 4-10): #{ranges.length}"
warn output
exit 1
end
unless (missing_ranges = required_ranges - ranges).empty?
warn "not all required monitor repartition ranges present: #{missing_ranges}"
exit 1
end
up, down, evloop, mc2 = resources = %w[vp down evloop mc2].map { UBID.generate_vanity("et", "mr", it).to_s }
lines = {}
output.split("\n").each do |line|
next if line.include?("monitor_repartition")
resource = resources.find { line.include?(it) } || :other
data = JSON.parse(line)
data.delete("time")
(lines[resource] ||= []) << data
end
lines.each_value(&:uniq!).each_value { it.sort_by!(&:inspect) }
[up, evloop].each do |r|
expected_lines = [
{"got_pulse" => {"ubid" => r, "pulse" => {"reading" => "up", "reading_rpt" => 1}}, "message" => "Got new pulse."},
{"metrics_export_success" => {"ubid" => r, "count" => 1}, "message" => "Metrics export has finished."}
]
unless lines[r] == expected_lines
warn "unexpected lines for #{r}: #{lines[r]}"
exit 1
end
end
expected_lines = [
{"got_pulse" => {"ubid" => mc2, "pulse" => {"reading" => "up", "reading_rpt" => 1}}, "message" => "Got new pulse."},
{"metrics_export_success" => {"ubid" => mc2, "count" => 2}, "message" => "Metrics export has finished."}
]
unless lines[mc2] == expected_lines
warn "unexpected lines for #{mc2}: #{lines[mc2]}"
exit 1
end
expected_lines = Array.new(lines[down].size - 1) do
{"got_pulse" => {"ubid" => down, "pulse" => {"reading" => "down", "reading_rpt" => it + 1}}, "message" => "Got new pulse."}
end
expected_lines << {"metrics_export_success" => {"ubid" => down, "count" => 1}, "message" => "Metrics export has finished."}
unless lines[down] == expected_lines
warn "unexpected lines for #{down}: #{lines[down]}"
exit 1
end
unless lines[:other].flat_map(&:keys).uniq.sort == ["message", "monitor_metrics"]
warn "unexpected other lines: #{lines[:other]}"
exit 1
end
puts "all checks passed!"