From fef1a13343cab14e763a621b748441d9ce8de081 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 21:06:07 +0200 Subject: [PATCH] test: cover CliJobsEchoJob and log --- test/test_cli_jobs.rb | 158 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/test_cli_jobs.rb diff --git a/test/test_cli_jobs.rb b/test/test_cli_jobs.rb new file mode 100644 index 0000000..ea46253 --- /dev/null +++ b/test/test_cli_jobs.rb @@ -0,0 +1,158 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' +require 'fileutils' +require 'stringio' + +class CliJobsEchoJob + def self.log + @log ||= [] + end + + def perform(value) + self.class.log << value + end +end + +class CliJobsAlwaysFailsJob + def perform + raise 'boom' + end +end + +class CliJobsTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-cli-jobs') + @cli = Prism::Cli.new(@dir) + @queue_path = File.join(@dir, 'db', 'jobs.pstore') + CliJobsEchoJob.log.clear + end + + def teardown + FileUtils.remove_entry(@dir) + end + + def queue + Prism::Jobs::Queue.new(@queue_path) + end + + def test_jobs_work_once_drains_ready_jobs_and_returns_a_single_cycle + queue.enqueue(job_class: 'CliJobsEchoJob', args: ['a']) + queue.enqueue(job_class: 'CliJobsEchoJob', args: ['b']) + + out = StringIO.new + cycles = @cli.jobs_work(['--once', '--verbose'], out: out) + + assert_equal 1, cycles + assert_equal %w[a b], CliJobsEchoJob.log.sort + assert_equal 0, queue.size + assert_match(/processed 2 job\(s\)/, out.string) + end + + def test_jobs_work_respects_custom_file_option + custom_path = File.join(@dir, 'custom_jobs.pstore') + Prism::Jobs::Queue.new(custom_path).enqueue(job_class: 'CliJobsEchoJob', args: ['custom']) + + @cli.jobs_work(['--once', '--file=custom_jobs.pstore']) + + assert_equal ['custom'], CliJobsEchoJob.log + assert_equal 0, Prism::Jobs::Queue.new(custom_path).size + end + + def test_jobs_work_scopes_to_queue_name + queue.enqueue(job_class: 'CliJobsEchoJob', args: ['default']) + queue.enqueue(job_class: 'CliJobsEchoJob', args: ['mailers'], queue_name: 'mailers') + + @cli.jobs_work(['--once', '--queue=mailers']) + + assert_equal ['mailers'], CliJobsEchoJob.log + assert_equal 1, queue.size + end + + def test_jobs_work_stops_after_max_cycles_without_sleeping_forever + cycles = @cli.jobs_work(['--max-cycles=3', '--interval=0']) + + assert_equal 3, cycles + end + + def test_jobs_work_does_not_crash_on_failing_jobs_and_reports_via_error_output + queue.enqueue(job_class: 'CliJobsAlwaysFailsJob', args: [], max_attempts: 1) + + _out, err = capture_io { @cli.jobs_work(['--once']) } + + assert_match(/CliJobsAlwaysFailsJob.*failed: boom/, err) + assert_equal 1, queue.dead_letter_size + end + + def test_jobs_work_requires_extra_files_before_running + job_file = File.join(@dir, 'extra_job.rb') + File.write(job_file, <<~RUBY) + class CliJobsRequiredJob + def perform(marker_path) + File.write(marker_path, 'ran') + end + end + RUBY + marker_path = File.join(@dir, 'marker.txt') + queue.enqueue(job_class: 'CliJobsRequiredJob', args: [marker_path]) + + @cli.jobs_work(['--once', '--require=extra_job.rb']) + + assert_equal 'ran', File.read(marker_path) + end + + def test_jobs_work_rejects_unknown_options + assert_raises(Prism::Cli::UsageError) { @cli.jobs_work(['--bogus']) } + end + + def test_run_dispatches_jobs_work_through_the_public_cli_entrypoint + queue.enqueue(job_class: 'CliJobsEchoJob', args: ['via-run']) + + @cli.run(['jobs', 'work', '--once']) + + assert_equal ['via-run'], CliJobsEchoJob.log + end + + def test_run_rejects_unknown_jobs_subcommand + assert_raises(Prism::Cli::UsageError) { @cli.run(['jobs', 'bogus']) } + end + + def test_jobs_work_writes_its_own_pid_while_running_and_removes_it_on_exit + pidfile = File.join(@dir, 'jobs.pid') + + thread = Thread.new { @cli.jobs_work(['--max-cycles=3', '--interval=0.2', "--pidfile=#{pidfile}"]) } + sleep 0.05 until File.exist?(pidfile) + assert_equal Process.pid.to_s, File.read(pidfile).strip + + thread.join + + refute File.exist?(pidfile), 'pidfile should be removed once the worker stops' + end + + def test_jobs_work_refuses_to_start_while_a_live_process_holds_the_pidfile + pidfile = File.join(@dir, 'jobs.pid') + # A genuinely different, currently-alive OS process (a plain child that + # sleeps) - the same scenario a second real daemon would face, unlike + # writing our own pid, which the cleanup step would then be entitled to + # remove because it would truly be ours. + other_pid = Process.spawn('sleep', '5') + File.write(pidfile, other_pid.to_s) + + error = assert_raises(Prism::Cli::UsageError) { @cli.jobs_work(['--once', "--pidfile=#{pidfile}"]) } + + assert_match(/already running/, error.message) + assert_equal other_pid.to_s, File.read(pidfile).strip, "the other process's pidfile must be left untouched" + ensure + Process.kill('TERM', other_pid) + Process.wait(other_pid) + end + + def test_jobs_work_reclaims_a_stale_pidfile_left_by_a_dead_process + pidfile = File.join(@dir, 'jobs.pid') + dead_pid = fork { exit } + Process.wait(dead_pid) + File.write(pidfile, dead_pid.to_s) + + queue.enqueue(job_class: 'CliJobsEchoJob', args: ['after-stale-pid']) + @cli.jobs_work(['--once', "--pidfile=#{pidfile}"]) +