From 0aa86bba07469a340ed595865d93076e75440093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20=C5=9Awi=C4=85tkowski?= Date: Thu, 22 Jan 2026 00:36:04 +0100 Subject: [PATCH 1/2] Replace exits with throws Commands calling `exit` directly cause instability in test runs, which also exit early. This leads to false positives. There was a mechanism injecting alternative exit mock, but you had to remember to use it every time. We already fixed all (?) the cases once, but the problem came back. --- exe/hanami | 17 +++--- lib/hanami/cli/commands/app/db/create.rb | 4 +- lib/hanami/cli/commands/app/db/drop.rb | 4 +- lib/hanami/cli/commands/app/db/migrate.rb | 5 +- lib/hanami/cli/commands/app/db/prepare.rb | 4 +- lib/hanami/cli/commands/app/db/rollback.rb | 48 +++++++---------- .../cli/commands/app/db/structure/dump.rb | 4 +- .../cli/commands/app/db/structure/load.rb | 4 +- lib/hanami/cli/commands/app/run.rb | 7 +-- .../hanami/cli/commands/app/db/create_spec.rb | 25 +++++---- .../hanami/cli/commands/app/db/drop_spec.rb | 21 +++++--- .../cli/commands/app/db/prepare_spec.rb | 7 ++- .../cli/commands/app/db/rollback_spec.rb | 52 +++++++++++-------- .../commands/app/db/structure/dump_spec.rb | 21 +++++--- .../commands/app/db/structure/load_spec.rb | 14 +++-- spec/unit/hanami/cli/commands/app/run_spec.rb | 26 ++++++---- 16 files changed, 147 insertions(+), 116 deletions(-) diff --git a/exe/hanami b/exe/hanami index 76aac463..b55e2f79 100755 --- a/exe/hanami +++ b/exe/hanami @@ -7,9 +7,14 @@ require "hanami/cli" cli = Dry::CLI.new(Hanami::CLI) Hanami::CLI::Bundler.require(:cli) -begin - cli.call -rescue Hanami::CLI::Error => exception - $stderr.puts(exception.message) # rubocop:disable Style/StderrPuts - exit(1) -end +code = catch(:exit) { + begin + cli.call + 0 + rescue Hanami::CLI::Error => exception + $stderr.puts(exception.message) # rubocop:disable Style/StderrPuts + 1 + end +} + +exit(code) diff --git a/lib/hanami/cli/commands/app/db/create.rb b/lib/hanami/cli/commands/app/db/create.rb index 385b08e3..564fa189 100644 --- a/lib/hanami/cli/commands/app/db/create.rb +++ b/lib/hanami/cli/commands/app/db/create.rb @@ -11,7 +11,7 @@ class Create < DB::Command option :gateway, required: false, desc: "Use database for gateway" - def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) + def call(app: false, slice: nil, gateway: nil, **) exit_codes = [] databases(app: app, slice: slice, gateway: gateway).each do |database| @@ -27,7 +27,7 @@ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) end exit_codes.each do |code| - break command_exit.(code) if code > 0 + throw(:exit, code) if code > 0 end re_run_development_command_in_test diff --git a/lib/hanami/cli/commands/app/db/drop.rb b/lib/hanami/cli/commands/app/db/drop.rb index 227e5405..d9fd9f9a 100644 --- a/lib/hanami/cli/commands/app/db/drop.rb +++ b/lib/hanami/cli/commands/app/db/drop.rb @@ -11,7 +11,7 @@ class Drop < DB::Command option :gateway, required: false, desc: "Use database for gateway" - def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) + def call(app: false, slice: nil, gateway: nil, **) exit_codes = [] databases(app: app, slice: slice, gateway: gateway).each do |database| @@ -27,7 +27,7 @@ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) end exit_codes.each do |code| - break command_exit.(code) if code > 0 + throw(:exit, code) if code > 0 end re_run_development_command_in_test diff --git a/lib/hanami/cli/commands/app/db/migrate.rb b/lib/hanami/cli/commands/app/db/migrate.rb index 385b05c2..d2d1e756 100644 --- a/lib/hanami/cli/commands/app/db/migrate.rb +++ b/lib/hanami/cli/commands/app/db/migrate.rb @@ -17,7 +17,7 @@ class Migrate < DB::Command default: true, desc: "Dump the database structure after migrating" - def call(target: nil, app: false, slice: nil, gateway: nil, dump: true, command_exit: method(:exit), **) + def call(target: nil, app: false, slice: nil, gateway: nil, dump: true, **) databases(app: app, slice: slice, gateway: gateway).each do |database| if migrations_dir_missing?(database) warn_on_missing_migrations_dir(database) @@ -32,8 +32,7 @@ def call(target: nil, app: false, slice: nil, gateway: nil, dump: true, command_ if dump && !re_running_in_test? run_command( Structure::Dump, - app: app, slice: slice, gateway: gateway, - command_exit: command_exit + app: app, slice: slice, gateway: gateway ) end diff --git a/lib/hanami/cli/commands/app/db/prepare.rb b/lib/hanami/cli/commands/app/db/prepare.rb index 67cab143..fd50f7c7 100644 --- a/lib/hanami/cli/commands/app/db/prepare.rb +++ b/lib/hanami/cli/commands/app/db/prepare.rb @@ -35,7 +35,7 @@ def call(app: false, slice: nil, **) # rubocop:disable Metrics/AbcSize nil end - return exit exit_code if exit_code.to_i > 1 + throw(:exit, exit_code) if exit_code.to_i > 1 end # Once all databases are created, the migrator will properly load for each slice, and @@ -54,7 +54,7 @@ def call(app: false, slice: nil, **) # rubocop:disable Metrics/AbcSize nil end - return exit exit_code if exit_code.to_i > 1 + throw(:exit, exit_code) if exit_code.to_i > 1 end # Finally, load the seeds for the slice overall, which is a once-per-slice operation. diff --git a/lib/hanami/cli/commands/app/db/rollback.rb b/lib/hanami/cli/commands/app/db/rollback.rb index cc84b74a..c206ecbf 100644 --- a/lib/hanami/cli/commands/app/db/rollback.rb +++ b/lib/hanami/cli/commands/app/db/rollback.rb @@ -24,7 +24,6 @@ def call( gateway: nil, target: nil, dump: true, - command_exit: method(:exit), ** ) # We allow either a number of steps or a target migration number to be provided @@ -33,7 +32,7 @@ def call( target = steps if steps && !target && !code_is_number?(steps) steps_count = steps && code_is_number?(steps) ? Integer(steps) : 1 - database = resolve_target_database(app: app, slice: slice, gateway: gateway, command_exit: command_exit) + database = resolve_target_database(app: app, slice: slice, gateway: gateway) return unless database migration_code, migration_name = find_migration_target(target, steps_count, database) @@ -61,8 +60,7 @@ def call( Structure::Dump, app: database.slice == self.app, slice: database.slice == self.app ? nil : database.slice.slice_name.to_s, - gateway: database.gateway_name == :default ? nil : database.gateway_name.to_s, - command_exit: command_exit + gateway: database.gateway_name == :default ? nil : database.gateway_name.to_s ) end @@ -72,24 +70,23 @@ def call( private - def resolve_target_database(app:, slice:, gateway:, command_exit:) + def resolve_target_database(app:, slice:, gateway:) if gateway && !app && !slice err.puts "When specifying --gateway, an --app or --slice must also be given" - command_exit.(1) - return + throw :exit, 1 end if slice - resolve_slice_database(slice, gateway, command_exit) + resolve_slice_database(slice, gateway) elsif app - resolve_app_database(gateway, command_exit) + resolve_app_database(gateway) else - resolve_default_database(command_exit) + resolve_default_database end end - def resolve_slice_database(slice_name, gateway, command_exit) - slice = resolve_slice(slice_name, command_exit) + def resolve_slice_database(slice_name, gateway) + slice = resolve_slice(slice_name) return unless slice databases = build_databases(slice) @@ -98,43 +95,41 @@ def resolve_slice_database(slice_name, gateway, command_exit) database = databases[gateway.to_sym] unless database err.puts %(No gateway "#{gateway}" found in slice "#{slice_name}") - command_exit.(1) - return + throw :exit, 1 end database elsif databases.size == 1 databases.values.first else err.puts "Multiple gateways found in slice #{slice_name}. Please specify --gateway option." - command_exit.(1) + throw :exit, 1 end end - def resolve_app_database(gateway, command_exit) + def resolve_app_database(gateway) databases = build_databases(app) if gateway database = databases[gateway.to_sym] unless database err.puts %(No gateway "#{gateway}" found in app) - command_exit.(1) - return + throw :exit, 1 end database elsif databases.size == 1 databases.values.first else err.puts "Multiple gateways found in app. Please specify --gateway option." - command_exit.(1) + throw :exit, 1 end end - def resolve_default_database(command_exit) + def resolve_default_database all_dbs = all_databases if all_dbs.empty? err.puts "No databases found" - command_exit.(1) + throw :exit, 1 elsif all_dbs.size == 1 all_dbs.first else @@ -143,24 +138,21 @@ def resolve_default_database(command_exit) app_databases.values.first elsif app_databases.size > 1 err.puts "Multiple gateways found in app. Please specify --gateway option." - command_exit.(1) - nil + throw :exit, 1 else err.puts "Multiple database contexts found. Please specify --app or --slice option." - command_exit.(1) - nil + throw :exit, 1 end end end - def resolve_slice(slice_name, command_exit) + def resolve_slice(slice_name) slice_name_sym = inflector.underscore(Shellwords.shellescape(slice_name)).to_sym slice = app.slices[slice_name_sym] unless slice err.puts %(Slice "#{slice_name}" not found) - command_exit.(1) - return + throw :exit, 1 end ensure_database_slice(slice) diff --git a/lib/hanami/cli/commands/app/db/structure/dump.rb b/lib/hanami/cli/commands/app/db/structure/dump.rb index 5a58a9a0..cb8aa80f 100644 --- a/lib/hanami/cli/commands/app/db/structure/dump.rb +++ b/lib/hanami/cli/commands/app/db/structure/dump.rb @@ -14,7 +14,7 @@ class Dump < DB::Command option :gateway, required: false, desc: "Use database for gateway" # @api private - def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) # rubocop:disable Metrics/AbcSize + def call(app: false, slice: nil, gateway: nil, **) # rubocop:disable Metrics/AbcSize exit_codes = [] databases(app: app, slice: slice, gateway: gateway).each do |database| @@ -48,7 +48,7 @@ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) end exit_codes.each do |code| - break command_exit.(code) if code > 0 + throw(:exit, code) if code > 0 end end end diff --git a/lib/hanami/cli/commands/app/db/structure/load.rb b/lib/hanami/cli/commands/app/db/structure/load.rb index 25999a9c..6f935f0a 100644 --- a/lib/hanami/cli/commands/app/db/structure/load.rb +++ b/lib/hanami/cli/commands/app/db/structure/load.rb @@ -17,7 +17,7 @@ class Load < DB::Command option :gateway, required: false, desc: "Use database for gateway" # @api private - def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) # rubocop:disable Metrics/AbcSize + def call(app: false, slice: nil, gateway: nil, **) # rubocop:disable Metrics/AbcSize exit_codes = [] databases(app: app, slice: slice, gateway: gateway).each do |database| @@ -42,7 +42,7 @@ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **) end exit_codes.each do |code| - break command_exit.(code) if code > 0 + throw(:exit, code) if code > 0 end re_run_development_command_in_test diff --git a/lib/hanami/cli/commands/app/run.rb b/lib/hanami/cli/commands/app/run.rb index e50026a6..67b5f9fa 100644 --- a/lib/hanami/cli/commands/app/run.rb +++ b/lib/hanami/cli/commands/app/run.rb @@ -32,11 +32,6 @@ class Run < Hanami::CLI::Command argument :code_or_path, required: true, desc: "Path to a Ruby file or inline Ruby code to be executed" - def initialize(command_exit: method(:exit), **opts) - super(**opts) - @command_exit = command_exit - end - # rubocop:disable Metrics/AbcSize def call(code_or_path:, **) require "hanami/prepare" @@ -60,7 +55,7 @@ def call(code_or_path:, **) end end rescue RunError - @command_exit.call(1) + throw :exit, 1 end # rubocop:enable Metrics/AbcSize diff --git a/spec/unit/hanami/cli/commands/app/db/create_spec.rb b/spec/unit/hanami/cli/commands/app/db/create_spec.rb index eb4d12d3..5dff2d02 100644 --- a/spec/unit/hanami/cli/commands/app/db/create_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/create_spec.rb @@ -11,17 +11,10 @@ let(:system_call) { Hanami::CLI::SystemCall.new } let(:test_env_executor) { instance_spy(Hanami::CLI::InteractiveSystemCall) } - let(:exit_double) { double(:exit_method) } let(:out) { StringIO.new } def output = out.string - before do - # Prevent the command from exiting the spec run in the case of unexpected system call failures - allow(command).to receive(:exit) - allow(exit_double).to receive(:call) - end - before do @env = ENV.to_h allow(Hanami::Env).to receive(:loaded?).and_return(false) @@ -95,7 +88,7 @@ def before_prepare end it "does not create the database if it already exists" do - command.run_command(Hanami::CLI::Commands::App::DB::Create, command_exit: exit_double) + command.run_command(Hanami::CLI::Commands::App::DB::Create) out.truncate(0) command.call @@ -120,7 +113,7 @@ def before_prepare end it "does not create the database if it already exists" do - command.run_command(Hanami::CLI::Commands::App::DB::Create, command_exit: exit_double) + command.run_command(Hanami::CLI::Commands::App::DB::Create) out.truncate(0) command.call @@ -190,7 +183,10 @@ def before_prepare .with(a_string_matching(/sqlite3.+app.sqlite3/)) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect { Main::Slice["db.gateway"] }.not_to raise_error @@ -202,7 +198,7 @@ def before_prepare expect(output).to include "database db/main.sqlite3 created" - expect(command).to have_received(:exit).with(2).once + expect(exit_code).to eq(2) end context "app with gateways" do @@ -283,7 +279,10 @@ def before_prepare .with(a_string_matching(/createdb.+_app/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect { Hanami.app["db.gateway"] }.to raise_error Sequel::DatabaseConnectionError expect { Main::Slice["db.gateway"] }.not_to raise_error @@ -293,7 +292,7 @@ def before_prepare expect(output).to include "database #{POSTGRES_BASE_DB_NAME}_main created" - expect(command).to have_received(:exit).with(2).once + expect(exit_code).to eq(2) end end diff --git a/spec/unit/hanami/cli/commands/app/db/drop_spec.rb b/spec/unit/hanami/cli/commands/app/db/drop_spec.rb index 8bfa4003..b0b82c77 100644 --- a/spec/unit/hanami/cli/commands/app/db/drop_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/drop_spec.rb @@ -135,7 +135,10 @@ def before_prepare .with(a_string_including("db/app.sqlite3")) .and_raise Errno::EACCES - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect(File.exist?(@dir.join("db", "app.sqlite3"))).to be true expect(File.exist?(@dir.join("db", "main.sqlite3"))).to be false @@ -145,7 +148,7 @@ def before_prepare expect(output).to include "database db/main.sqlite3 dropped" - expect(command).to have_received(:exit).with(1).once + expect(exit_code).to eq(1) end context "app and slice with gateways" do @@ -351,7 +354,10 @@ def before_prepare .with(a_string_matching(/dropdb.+_app/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect { Hanami.app["db.gateway"].connection.test_connection }.not_to raise_error @@ -360,7 +366,7 @@ def before_prepare expect(output).to include "database #{POSTGRES_BASE_DB_NAME}_main dropped" - expect(command).to have_received(:exit).with(2).once + expect(exit_code).to eq(2) end it "raises exception when DB existence check fails" do @@ -420,14 +426,17 @@ def before_prepare .with(a_string_matching(/-e "DROP DATABASE/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect { Hanami.app["db.gateway"].connection.test_connection }.not_to raise_error expect(output).to include "failed to drop database #{POSTGRES_BASE_DB_NAME}_app" expect(output).to include "app-db-err" - expect(command).to have_received(:exit).with(2).once + expect(exit_code).to eq(2) end it "prints errors when check for DB existence fails" do diff --git a/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb b/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb index fb628481..087e50ed 100644 --- a/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb @@ -419,7 +419,10 @@ class Comments < Hanami::DB::Relation .with(a_string_matching(/createdb.+_main/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "main-db-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect { Hanami.app["relations.posts"] }.to raise_error Sequel::DatabaseError expect { Main::Slice["relations.comments"] }.to raise_error Sequel::DatabaseError @@ -433,7 +436,7 @@ class Comments < Hanami::DB::Relation expect(output).not_to include "migrated" expect(output).not_to include "seed data loaded" - expect(command).to have_received(:exit).with 2 + expect(exit_code).to eq(2) end end diff --git a/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb b/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb index 7647acf4..f5f094fd 100644 --- a/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb @@ -16,28 +16,34 @@ shared_examples "multiple gateways error handling" do |db_type| it "fails with clear error message when multiple gateways exist without specification" do - exit_mock = instance_double("Method", call: nil) - allow(exit_mock).to receive(:call).with(1).and_raise(SystemExit) + exit_code = catch(:exit) { + command.call + 0 + } - expect { command.call(command_exit: exit_mock) }.to raise_error(SystemExit) + expect(exit_code).to eq(1) expect(output).to include "Multiple gateways found in app. Please specify --gateway option." end end shared_examples "invalid argument handling" do it "fails when gateway is specified without app or slice" do - exit_mock = instance_double("Method", call: nil) - allow(exit_mock).to receive(:call).with(1).and_raise(SystemExit) + exit_code = catch(:exit) { + command.call(gateway: "default") + 0 + } - expect { command.call(gateway: "default", command_exit: exit_mock) }.to raise_error(SystemExit) + expect(exit_code).to eq(1) expect(output).to include "When specifying --gateway, an --app or --slice must also be given" end it "fails when gateway does not exist" do - exit_mock = instance_double("Method", call: nil) - allow(exit_mock).to receive(:call).with(1).and_raise(SystemExit) + exit_code = catch(:exit) { + command.call(app: true, gateway: "nonexistent") + 0 + } - expect { command.call(app: true, gateway: "nonexistent", command_exit: exit_mock) }.to raise_error(SystemExit) + expect(exit_code).to eq(1) expect(output).to include %(No gateway "nonexistent" found in app) end end @@ -63,10 +69,6 @@ def output = out.string + err.string allow(Hanami::CLI::Commands::App::DB::Structure::Dump).to receive(:new) { dump_command } end - before do - allow(command).to receive(:exit) - end - before do @env = ENV.to_h allow(Hanami::Env).to receive(:loaded?).and_return(false) @@ -154,20 +156,24 @@ class App < Hanami::App context "with invalid args combinations" do context "when gateway is specified without app or slice" do it "does not allow ambiguity, asks for details" do - exit_mock = instance_double("Method", call: nil) - allow(exit_mock).to receive(:call).with(1).and_raise(SystemExit) + exit_code = catch(:exit) { + command.call(gateway: "default") + 0 + } - expect { command.call(gateway: "default", command_exit: exit_mock) }.to raise_error(SystemExit) + expect(exit_code).to eq(1) expect(output).to include "When specifying --gateway, an --app or --slice must also be given" end end context "when gateway that does not exist in the context is specified" do it "informs about invalid gateway" do - exit_mock = instance_double("Method", call: nil) - allow(exit_mock).to receive(:call).with(1).and_raise(SystemExit) + exit_code = catch(:exit) { + command.call(app: true, gateway: "nonexistent") + 0 + } - expect { command.call(app: true, gateway: "nonexistent", command_exit: exit_mock) }.to raise_error(SystemExit) + expect(exit_code).to eq(1) expect(output).to include %(No gateway "nonexistent" found in app) end end @@ -298,10 +304,12 @@ class App < Hanami::App context "with no arguments" do it "asks for more detailed prompt" do - exit_mock = instance_double("Method", call: nil) - allow(exit_mock).to receive(:call).with(1).and_raise(SystemExit) + exit_code = catch(:exit) { + command.call + 0 + } - expect { command.call(command_exit: exit_mock) }.to raise_error(SystemExit) + expect(exit_code).to eq(1) expect(output).to include "Multiple gateways found in app. Please specify --gateway option." end end diff --git a/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb b/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb index 0ef061cb..7babde96 100644 --- a/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb @@ -205,7 +205,10 @@ def before_prepare .with(a_string_including("db/app.sqlite3")) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "dump-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect(Main::Slice.root.join("config", "db", "structure.sql").exist?).to be true expect(Hanami.app.root.join("config", "db", "structure.sql").exist?).to be false @@ -213,7 +216,7 @@ def before_prepare expect(output).to include %("db/app.sqlite3 structure dumped to config/db/structure.sql" FAILED) expect(output).to include "db/main.sqlite3 structure dumped to slices/main/config/db/structure.sql" - expect(command).to have_received(:exit).with 2 + expect(exit_code).to eq(2) end end @@ -289,7 +292,10 @@ def before_prepare .with(a_string_including("app"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "dump-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect(Main::Slice.root.join("config", "db", "structure.sql").exist?).to be true expect(Hanami.app.root.join("config", "db", "structure.sql").exist?).to be false @@ -297,7 +303,7 @@ def before_prepare expect(output).to include %("#{POSTGRES_BASE_DB_NAME}_app structure dumped to config/db/structure.sql" FAILED) expect(output).to include "#{POSTGRES_BASE_DB_NAME}_main structure dumped to slices/main/config/db/structure.sql" - expect(command).to have_received(:exit).with 2 + expect(exit_code).to eq(2) end end @@ -330,13 +336,16 @@ def before_prepare .with(a_string_including("mysqldump"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "dump-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect(Hanami.app.root.join("config", "db", "structure.sql").exist?).to be false expect(output).to include %("#{MYSQL_BASE_DB_NAME}_app structure dumped to config/db/structure.sql" FAILED) - expect(command).to have_received(:exit).with 2 + expect(exit_code).to eq(2) end end end diff --git a/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb b/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb index 7e7d48f3..6433f84f 100644 --- a/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb @@ -237,7 +237,10 @@ def before_prepare .with(a_string_including("#{POSTGRES_BASE_DB_NAME}_app"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-load-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect(Hanami.app["db.gateway"].connection.tables.include?(:posts)).to be false expect(Main::Slice["db.gateway"].connection.tables.include?(:comments)).to be true @@ -245,7 +248,7 @@ def before_prepare expect(output).to include %("#{POSTGRES_BASE_DB_NAME}_app structure loaded from config/db/structure.sql" FAILED) expect(output).to include "#{POSTGRES_BASE_DB_NAME}_main structure loaded from slices/main/config/db/structure.sql" - expect(command).to have_received(:exit).with 2 + expect(exit_code).to eq(2) end end @@ -273,13 +276,16 @@ def before_prepare .with(a_string_including("#{MYSQL_BASE_DB_NAME}_app"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-load-err") - command.call + exit_code = catch(:exit) { + command.call + 0 + } expect(Hanami.app["db.gateway"].connection.tables.include?(:posts)).to be false expect(output).to include %("#{MYSQL_BASE_DB_NAME}_app structure loaded from config/db/structure.sql" FAILED) - expect(command).to have_received(:exit).with 2 + expect(exit_code).to eq(2) end end diff --git a/spec/unit/hanami/cli/commands/app/run_spec.rb b/spec/unit/hanami/cli/commands/app/run_spec.rb index 616f857d..16320762 100644 --- a/spec/unit/hanami/cli/commands/app/run_spec.rb +++ b/spec/unit/hanami/cli/commands/app/run_spec.rb @@ -4,16 +4,13 @@ require "hanami/cli/commands/app/run" RSpec.describe Hanami::CLI::Commands::App::Run do - subject { described_class.new(out: out, err: err, command_exit: command_exit) } + subject { described_class.new(out: out, err: err) } let(:out) { StringIO.new } let(:err) { StringIO.new } - let(:command_exit) { double(:command_exit) } before do # Mock the hanami/prepare requirement allow(subject).to receive(:require).with("hanami/prepare") - - allow(command_exit).to receive(:call) end describe "#call" do @@ -42,28 +39,37 @@ context "with syntax errors" do it "prints error message and exits with code 1" do - subject.call(code_or_path: "puts 'unclosed string") + exit_code = catch(:exit) { + subject.call(code_or_path: "puts 'unclosed string") + 0 + } expect(err.string).to include("Syntax error in code") - expect(command_exit).to have_received(:call).with(1) + expect(exit_code).to eq(1) end end context "with name errors" do it "prints error message and exits with code 1" do - subject.call(code_or_path: "undefined_variable") + exit_code = catch(:exit) { + subject.call(code_or_path: "undefined_variable") + 0 + } expect(err.string).to include("Name error in code") - expect(command_exit).to have_received(:call).with(1) + expect(exit_code).to eq(1) end end context "with runtime errors" do it "prints error message and exits with code 1" do - subject.call(code_or_path: "1 / 0") + exit_code = catch(:exit) { + subject.call(code_or_path: "1 / 0") + 0 + } expect(err.string).to include("Error executing code") - expect(command_exit).to have_received(:call).with(1) + expect(exit_code).to eq(1) end end end From 70fc7163b0137b69d280bf7d2eb046f401bac1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20=C5=9Awi=C4=85tkowski?= Date: Tue, 7 Jul 2026 22:49:33 +0200 Subject: [PATCH 2/2] Add helper to assert on exit code --- spec/support/helpers.rb | 20 +++++++++++ .../hanami/cli/commands/app/db/create_spec.rb | 14 ++------ .../hanami/cli/commands/app/db/drop_spec.rb | 21 ++--------- .../cli/commands/app/db/prepare_spec.rb | 7 +--- .../cli/commands/app/db/rollback_spec.rb | 36 ++++--------------- .../commands/app/db/structure/dump_spec.rb | 21 ++--------- .../commands/app/db/structure/load_spec.rb | 14 ++------ spec/unit/hanami/cli/commands/app/run_spec.rb | 18 ++-------- 8 files changed, 40 insertions(+), 111 deletions(-) create mode 100644 spec/support/helpers.rb diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb new file mode 100644 index 00000000..d70ba0c7 --- /dev/null +++ b/spec/support/helpers.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RSpec + module Support + module Helpers + def expect_exit_code(expected = 0) + actual = catch(:exit) do + yield + 0 + end + expect(actual).to eq(expected) + actual + end + end + end +end + +RSpec.configure do |config| + config.include RSpec::Support::Helpers +end diff --git a/spec/unit/hanami/cli/commands/app/db/create_spec.rb b/spec/unit/hanami/cli/commands/app/db/create_spec.rb index 5dff2d02..86c7c08f 100644 --- a/spec/unit/hanami/cli/commands/app/db/create_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/create_spec.rb @@ -183,10 +183,7 @@ def before_prepare .with(a_string_matching(/sqlite3.+app.sqlite3/)) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect { Main::Slice["db.gateway"] }.not_to raise_error @@ -197,8 +194,6 @@ def before_prepare expect(output).to include "app-db-err" expect(output).to include "database db/main.sqlite3 created" - - expect(exit_code).to eq(2) end context "app with gateways" do @@ -279,10 +274,7 @@ def before_prepare .with(a_string_matching(/createdb.+_app/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect { Hanami.app["db.gateway"] }.to raise_error Sequel::DatabaseConnectionError expect { Main::Slice["db.gateway"] }.not_to raise_error @@ -291,8 +283,6 @@ def before_prepare expect(output).to include "app-db-err" expect(output).to include "database #{POSTGRES_BASE_DB_NAME}_main created" - - expect(exit_code).to eq(2) end end diff --git a/spec/unit/hanami/cli/commands/app/db/drop_spec.rb b/spec/unit/hanami/cli/commands/app/db/drop_spec.rb index b0b82c77..722742ed 100644 --- a/spec/unit/hanami/cli/commands/app/db/drop_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/drop_spec.rb @@ -135,10 +135,7 @@ def before_prepare .with(a_string_including("db/app.sqlite3")) .and_raise Errno::EACCES - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(1) { command.call } expect(File.exist?(@dir.join("db", "app.sqlite3"))).to be true expect(File.exist?(@dir.join("db", "main.sqlite3"))).to be false @@ -147,8 +144,6 @@ def before_prepare expect(output).to include "Permission denied" # from Errno::EACCESS expect(output).to include "database db/main.sqlite3 dropped" - - expect(exit_code).to eq(1) end context "app and slice with gateways" do @@ -354,10 +349,7 @@ def before_prepare .with(a_string_matching(/dropdb.+_app/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect { Hanami.app["db.gateway"].connection.test_connection }.not_to raise_error @@ -365,8 +357,6 @@ def before_prepare expect(output).to include "app-db-err" expect(output).to include "database #{POSTGRES_BASE_DB_NAME}_main dropped" - - expect(exit_code).to eq(2) end it "raises exception when DB existence check fails" do @@ -426,17 +416,12 @@ def before_prepare .with(a_string_matching(/-e "DROP DATABASE/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-db-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect { Hanami.app["db.gateway"].connection.test_connection }.not_to raise_error expect(output).to include "failed to drop database #{POSTGRES_BASE_DB_NAME}_app" expect(output).to include "app-db-err" - - expect(exit_code).to eq(2) end it "prints errors when check for DB existence fails" do diff --git a/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb b/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb index 087e50ed..d2cc16f1 100644 --- a/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb @@ -419,10 +419,7 @@ class Comments < Hanami::DB::Relation .with(a_string_matching(/createdb.+_main/), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "main-db-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect { Hanami.app["relations.posts"] }.to raise_error Sequel::DatabaseError expect { Main::Slice["relations.comments"] }.to raise_error Sequel::DatabaseError @@ -435,8 +432,6 @@ class Comments < Hanami::DB::Relation expect(output).not_to include "migrated" expect(output).not_to include "seed data loaded" - - expect(exit_code).to eq(2) end end diff --git a/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb b/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb index f5f094fd..95a0118a 100644 --- a/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb @@ -16,34 +16,22 @@ shared_examples "multiple gateways error handling" do |db_type| it "fails with clear error message when multiple gateways exist without specification" do - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(1) { command.call } - expect(exit_code).to eq(1) expect(output).to include "Multiple gateways found in app. Please specify --gateway option." end end shared_examples "invalid argument handling" do it "fails when gateway is specified without app or slice" do - exit_code = catch(:exit) { - command.call(gateway: "default") - 0 - } + expect_exit_code(1) { command.call(gateway: "default") } - expect(exit_code).to eq(1) expect(output).to include "When specifying --gateway, an --app or --slice must also be given" end it "fails when gateway does not exist" do - exit_code = catch(:exit) { - command.call(app: true, gateway: "nonexistent") - 0 - } + expect_exit_code(1) { command.call(app: true, gateway: "nonexistent") } - expect(exit_code).to eq(1) expect(output).to include %(No gateway "nonexistent" found in app) end end @@ -156,24 +144,16 @@ class App < Hanami::App context "with invalid args combinations" do context "when gateway is specified without app or slice" do it "does not allow ambiguity, asks for details" do - exit_code = catch(:exit) { - command.call(gateway: "default") - 0 - } + expect_exit_code(1) { command.call(gateway: "default") } - expect(exit_code).to eq(1) expect(output).to include "When specifying --gateway, an --app or --slice must also be given" end end context "when gateway that does not exist in the context is specified" do it "informs about invalid gateway" do - exit_code = catch(:exit) { - command.call(app: true, gateway: "nonexistent") - 0 - } + expect_exit_code(1) { command.call(app: true, gateway: "nonexistent") } - expect(exit_code).to eq(1) expect(output).to include %(No gateway "nonexistent" found in app) end end @@ -304,12 +284,8 @@ class App < Hanami::App context "with no arguments" do it "asks for more detailed prompt" do - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(1) { command.call } - expect(exit_code).to eq(1) expect(output).to include "Multiple gateways found in app. Please specify --gateway option." end end diff --git a/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb b/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb index 7babde96..38644937 100644 --- a/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb @@ -205,18 +205,13 @@ def before_prepare .with(a_string_including("db/app.sqlite3")) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "dump-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect(Main::Slice.root.join("config", "db", "structure.sql").exist?).to be true expect(Hanami.app.root.join("config", "db", "structure.sql").exist?).to be false expect(output).to include %("db/app.sqlite3 structure dumped to config/db/structure.sql" FAILED) expect(output).to include "db/main.sqlite3 structure dumped to slices/main/config/db/structure.sql" - - expect(exit_code).to eq(2) end end @@ -292,18 +287,13 @@ def before_prepare .with(a_string_including("app"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "dump-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect(Main::Slice.root.join("config", "db", "structure.sql").exist?).to be true expect(Hanami.app.root.join("config", "db", "structure.sql").exist?).to be false expect(output).to include %("#{POSTGRES_BASE_DB_NAME}_app structure dumped to config/db/structure.sql" FAILED) expect(output).to include "#{POSTGRES_BASE_DB_NAME}_main structure dumped to slices/main/config/db/structure.sql" - - expect(exit_code).to eq(2) end end @@ -336,16 +326,11 @@ def before_prepare .with(a_string_including("mysqldump"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "dump-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect(Hanami.app.root.join("config", "db", "structure.sql").exist?).to be false expect(output).to include %("#{MYSQL_BASE_DB_NAME}_app structure dumped to config/db/structure.sql" FAILED) - - expect(exit_code).to eq(2) end end end diff --git a/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb b/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb index 6433f84f..f3cceca3 100644 --- a/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb @@ -237,18 +237,13 @@ def before_prepare .with(a_string_including("#{POSTGRES_BASE_DB_NAME}_app"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-load-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect(Hanami.app["db.gateway"].connection.tables.include?(:posts)).to be false expect(Main::Slice["db.gateway"].connection.tables.include?(:comments)).to be true expect(output).to include %("#{POSTGRES_BASE_DB_NAME}_app structure loaded from config/db/structure.sql" FAILED) expect(output).to include "#{POSTGRES_BASE_DB_NAME}_main structure loaded from slices/main/config/db/structure.sql" - - expect(exit_code).to eq(2) end end @@ -276,16 +271,11 @@ def before_prepare .with(a_string_including("#{MYSQL_BASE_DB_NAME}_app"), anything) .and_return Hanami::CLI::SystemCall::Result.new(exit_code: 2, out: "", err: "app-load-err") - exit_code = catch(:exit) { - command.call - 0 - } + expect_exit_code(2) { command.call } expect(Hanami.app["db.gateway"].connection.tables.include?(:posts)).to be false expect(output).to include %("#{MYSQL_BASE_DB_NAME}_app structure loaded from config/db/structure.sql" FAILED) - - expect(exit_code).to eq(2) end end diff --git a/spec/unit/hanami/cli/commands/app/run_spec.rb b/spec/unit/hanami/cli/commands/app/run_spec.rb index 16320762..bbeac1c9 100644 --- a/spec/unit/hanami/cli/commands/app/run_spec.rb +++ b/spec/unit/hanami/cli/commands/app/run_spec.rb @@ -39,37 +39,25 @@ context "with syntax errors" do it "prints error message and exits with code 1" do - exit_code = catch(:exit) { - subject.call(code_or_path: "puts 'unclosed string") - 0 - } + expect_exit_code(1) { subject.call(code_or_path: "puts 'unclosed string") } expect(err.string).to include("Syntax error in code") - expect(exit_code).to eq(1) end end context "with name errors" do it "prints error message and exits with code 1" do - exit_code = catch(:exit) { - subject.call(code_or_path: "undefined_variable") - 0 - } + expect_exit_code(1) { subject.call(code_or_path: "undefined_variable") } expect(err.string).to include("Name error in code") - expect(exit_code).to eq(1) end end context "with runtime errors" do it "prints error message and exits with code 1" do - exit_code = catch(:exit) { - subject.call(code_or_path: "1 / 0") - 0 - } + expect_exit_code(1) { subject.call(code_or_path: "1 / 0") } expect(err.string).to include("Error executing code") - expect(exit_code).to eq(1) end end end