Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run db commands on test as well as development databases #247

Merged
merged 8 commits into from
Oct 26, 2024
Merged
76 changes: 75 additions & 1 deletion lib/hanami/cli/commands/app/db/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,23 @@ class Command < App::Command
option :app, required: false, type: :flag, default: false, desc: "Use app database"
option :slice, required: false, desc: "Use database for slice"

# @api private
attr_reader :system_call

# @api private
attr_reader :test_env_executor

def initialize(
out:, err:,
system_call: SystemCall.new,
test_env_executor: InteractiveSystemCall.new(out: out, err: err),
nested_command: false,
**opts
)
super(out: out, err: err, **opts)
@system_call = system_call
@test_env_executor = test_env_executor
@nested_command = nested_command
end

def run_command(klass, ...)
Expand All @@ -34,9 +42,15 @@ def run_command(klass, ...)
inflector: inflector,
fs: fs,
system_call: system_call,
test_env_executor: test_env_executor,
nested_command: true,
).call(...)
end

def nested_command?
@nested_command
end

private

def databases(app: false, slice: nil, gateway: nil)
Expand Down Expand Up @@ -77,7 +91,7 @@ def database_for_slice(slice, gateway: nil)
end
end

def all_databases # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
def all_databases # rubocop:disable Metrics/AbcSize
slices = [app] + app.slices.with_nested

slice_gateways_by_database_url = slices.each_with_object({}) { |slice, hsh|
Expand Down Expand Up @@ -140,6 +154,66 @@ def warn_on_misconfigured_database(database, slices) # rubocop:disable Metrics/A
STR
end
end

# Invokes the currently executing `hanami` CLI command again, but with any `--env` args
# removed and the `HANAMI_ENV=test` env var set.
#
# This is called by certain `db` commands only, and runs only if the Hanami env is
# `:development`. This behavior important to streamline the local development
# experience, making sure that the test databases are kept in sync with operations run
# on the development databases.
#
# Spawning an entirely new process to change the env is a compromise approach until we
# can have an API for reinitializing the DB subsystem in-process with a different env.
def re_run_development_command_in_test
# Only invoke a new process if we've been called as `hanami`. This avoids awkward
# failures when testing commands via RSpec, for which the $0 is "/full/path/to/rspec".
return unless $0.end_with?("hanami")
Comment on lines +169 to +171
Copy link
Member Author

@timriley timriley Oct 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit hacky, but as commented here and in spec/support/hanami_cli_environment.rb, it was necessary because of our current testing approach. Given that I think we'll be able to switch to a cleaner overall approach in the future, I was comfortable with a few hacks like this being employed here.


# If this special env key is set, then a re-run has already been invoked. This would
# mean the current command is actually a nested command run by another db command. In
# this case, don't trigger a re-runs, because one is already in process.
return if nested_command?

# Re-runs in test are for development-env commands only.
return unless Hanami.env == :development

cmd = $0
cmd = "bundle exec #{cmd}" if ENV.key?("BUNDLE_BIN_PATH")

test_env_executor.call(
cmd, *argv_without_env_args,
env: {
"HANAMI_ENV" => "test",
"HANAMI_CLI_DB_COMMAND_RE_RUN_IN_TEST" => "true"
}
)
end

def re_running_in_test?
ENV.key?("HANAMI_CLI_DB_COMMAND_RE_RUN_IN_TEST")
end

# Returns the `ARGV` with every option argument included, but the `-e` or `--env` args
# removed.
def argv_without_env_args
new_argv = ARGV.dup

env_arg_index = new_argv.index {
_1 == "-e" || _1 == "--env" || _1.start_with?("-e=") || _1.start_with?("--env=")
}

if env_arg_index
# Remove the env argument
env_arg = new_argv.delete_at(env_arg_index)

# If the env argument is not in combined form ("--env foo" rather than "--env=foo"),
# then remove the following argument too
new_argv.delete_at(env_arg_index) if ["-e", "--env"].include?(env_arg)
end

new_argv
end
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/hanami/cli/commands/app/db/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **)
exit_codes.each do |code|
break command_exit.(code) if code > 0
end

re_run_development_command_in_test
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/hanami/cli/commands/app/db/drop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def call(app: false, slice: nil, gateway: nil, **)
exit_codes.each do |code|
break exit code if code > 0
end

re_run_development_command_in_test
end
end
end
Expand Down
11 changes: 10 additions & 1 deletion lib/hanami/cli/commands/app/db/migrate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@ def call(target: nil, app: false, slice: nil, gateway: nil, dump: true, command_
end
end

run_command(Structure::Dump, app: app, slice: slice, gateway: gateway, command_exit: command_exit) if dump
# Only dump for the initial command, not a re-run of the command in test env
if dump && !re_running_in_test?
run_command(
Structure::Dump,
app: app, slice: slice, gateway: gateway,
command_exit: command_exit
)
end

re_run_development_command_in_test
end

private
Expand Down
4 changes: 3 additions & 1 deletion lib/hanami/cli/commands/app/db/prepare.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ def call(app: false, slice: nil, **)
end

# Finally, load the seeds for the slice overall, which is a once-per-slice operation.
run_command(DB::Seed, app: app, slice: slice)
run_command(DB::Seed, app: app, slice: slice) unless re_running_in_test?

re_run_development_command_in_test
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/hanami/cli/commands/app/db/structure/load.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **)
exit_codes.each do |code|
break command_exit.(code) if code > 0
end

re_run_development_command_in_test
end
end
end
Expand Down
31 changes: 31 additions & 0 deletions spec/support/hanami_cli_environment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module RSpec
module Support
module HanamiCLIEnvironment
# Adjusts $0 and ARGV to match the values expected when the `hanami` CLI is invoked in
# ordinary usage.
#
# This is a workaround for our current (comrpomise) approach of re-executing DB CLI commands
# in test mode.
#
# @see Hanami::CLI::Commands::App::DB::Command#re_run_development_command_in_test
def as_hanami_cli_with_args(args)
original_0 = $0.dup
original_argv = ARGV.dup

$0 = "hanami"
ARGV.replace(args)

yield

$0 = original_0
ARGV.replace(original_argv)
end
end
end
end

RSpec.configure do |config|
config.include RSpec::Support::HanamiCLIEnvironment
end
39 changes: 38 additions & 1 deletion spec/unit/hanami/cli/commands/app/db/create_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# frozen_string_literal: true

RSpec.describe Hanami::CLI::Commands::App::DB::Create, :app_integration do
subject(:command) { described_class.new(system_call: system_call, out: out) }
subject(:command) {
described_class.new(
system_call: system_call,
test_env_executor: test_env_executor,
out: out
)
}

let(:system_call) { Hanami::CLI::SystemCall.new }
let(:test_env_executor) { instance_spy(Hanami::CLI::InteractiveSystemCall) }

let(:out) { StringIO.new }
def output = out.string
Expand Down Expand Up @@ -287,5 +294,35 @@ def before_prepare
expect(command).to have_received(:exit).with(2).once
end
end

describe "automatic test env execution" do
before do
ENV["DATABASE_URL"] = "sqlite://db/app.sqlite3"
end

around do |example|
as_hanami_cli_with_args(%w[db create]) { example.run }
end

it "re-executes the command in test env when run with development env" do
command.call(env: "development")

expect(test_env_executor).to have_received(:call).with(
"bundle exec hanami",
"db", "create",
{
env: hash_including("HANAMI_ENV" => "test")
}
)
end

it "does not re-execute the command when run with other environments" do
command.call(env: "test")
expect(test_env_executor).not_to have_received(:call)

command.call(env: "production")
expect(test_env_executor).not_to have_received(:call)
end
end
end
end
39 changes: 38 additions & 1 deletion spec/unit/hanami/cli/commands/app/db/drop_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# frozen_string_literal: true

RSpec.describe Hanami::CLI::Commands::App::DB::Drop, :app_integration do
subject(:command) { described_class.new(system_call: system_call, out: out) }
subject(:command) {
described_class.new(
system_call: system_call,
test_env_executor: test_env_executor,
out: out
)
}

let(:system_call) { Hanami::CLI::SystemCall.new }
let(:test_env_executor) { instance_spy(Hanami::CLI::InteractiveSystemCall) }

let(:out) { StringIO.new }
def output = out.string
Expand Down Expand Up @@ -410,4 +417,34 @@ def before_prepare
expect(command).to have_received(:exit).with(2).once
end
end

describe "automatic test env execution" do
before do
ENV["DATABASE_URL"] = "sqlite://db/app.sqlite3"
end

around do |example|
as_hanami_cli_with_args(%w[db drop]) { example.run }
end

it "re-executes the command in test env when run with development env" do
command.call(env: "development")

expect(test_env_executor).to have_received(:call).with(
"bundle exec hanami",
"db", "drop",
{
env: hash_including("HANAMI_ENV" => "test")
}
)
end

it "does not re-execute the command when run with other environments" do
command.call(env: "test")
expect(test_env_executor).not_to have_received(:call)

command.call(env: "production")
expect(test_env_executor).not_to have_received(:call)
end
end
end
35 changes: 34 additions & 1 deletion spec/unit/hanami/cli/commands/app/db/migrate_spec.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# frozen_string_literal: true

RSpec.describe Hanami::CLI::Commands::App::DB::Migrate, :app_integration do
subject(:command) { described_class.new(out: out) }
subject(:command) { described_class.new(out: out, test_env_executor: test_env_executor) }

let(:out) { StringIO.new }
def output = out.string

let(:test_env_executor) { instance_spy(Hanami::CLI::InteractiveSystemCall) }

let(:dump_command) { instance_spy(Hanami::CLI::Commands::App::DB::Structure::Dump) }

before do
Expand Down Expand Up @@ -418,4 +420,35 @@ def before_prepare
expect(output).not_to include "migrated"
end
end

describe "automatic test env execution" do
before do
ENV["DATABASE_URL"] = "sqlite://db/app.sqlite3"
db_create
end

around do |example|
as_hanami_cli_with_args(%w[db migrate]) { example.run }
end

it "re-executes the command in test env when run with development env" do
command.call(env: "development")

expect(test_env_executor).to have_received(:call).with(
"bundle exec hanami",
"db", "migrate",
{
env: hash_including("HANAMI_ENV" => "test")
}
)
end

it "does not re-execute the command when run with other environments" do
command.call(env: "test")
expect(test_env_executor).not_to have_received(:call)

command.call(env: "production")
expect(test_env_executor).not_to have_received(:call)
end
end
end
Loading