From 4254b75cb8059b0f728bf07c040d7c309b59daf3 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Wed, 2 Apr 2025 07:30:14 +0100 Subject: [PATCH] bundle: add tests for `exec --services` --- Library/Homebrew/bundle/commands/exec.rb | 17 ++- .../test/bundle/brew_services_spec.rb | 52 +++++-- .../test/bundle/commands/exec_spec.rb | 139 ++++++++++++++++++ 3 files changed, 189 insertions(+), 19 deletions(-) diff --git a/Library/Homebrew/bundle/commands/exec.rb b/Library/Homebrew/bundle/commands/exec.rb index b2a4f52400..27c1a4354d 100644 --- a/Library/Homebrew/bundle/commands/exec.rb +++ b/Library/Homebrew/bundle/commands/exec.rb @@ -191,13 +191,22 @@ module Homebrew [entry, formula] end.to_h + conflicts = entries_formulae.to_h do |entry, formula| + [ + entry, + ( + formula.versioned_formulae_names + + formula.conflicts.map(&:name) + + Array(entry.options[:conflicts_with]) + ).uniq, + ] + end + # The formula + everything that could possible conflict with the service names_to_query = entries_formulae.flat_map do |entry, formula| [ formula.name, - *formula.versioned_formulae_names, - *formula.conflicts.map(&:name), - *entry.options[:conflicts_with], + *conflicts.fetch(entry), ] end @@ -227,7 +236,7 @@ module Homebrew conflicting_services = services_info.select do |candidate| next unless candidate["running"] - formula.versioned_formulae_names.include?(candidate["name"]) + conflicts.fetch(entry).include?(candidate["name"]) end raise "Failed to get service info for #{entry.name}" if info.nil? diff --git a/Library/Homebrew/test/bundle/brew_services_spec.rb b/Library/Homebrew/test/bundle/brew_services_spec.rb index 27a088edc1..99d706d5b9 100644 --- a/Library/Homebrew/test/bundle/brew_services_spec.rb +++ b/Library/Homebrew/test/bundle/brew_services_spec.rb @@ -67,27 +67,49 @@ RSpec.describe Homebrew::Bundle::BrewServices do let(:foo) do instance_double( Formula, - name: "fooformula", - version: "1.0", - rack: HOMEBREW_CELLAR/"fooformula", - plist_name: "homebrew.mxcl.fooformula", + name: "fooformula", + version: "1.0", + rack: HOMEBREW_CELLAR/"fooformula", + plist_name: "homebrew.mxcl.fooformula", + service_name: "fooformula", ) end - it "returns the versioned service file" do - expect(Formula).to receive(:[]).with(foo.name).and_return(foo) - expect(Homebrew::Bundle).to receive(:formula_versions_from_env).and_return(foo.name => foo.version) + shared_examples "returns the versioned service file" do + it "returns the versioned service file" do + expect(Formula).to receive(:[]).with(foo.name).and_return(foo) + expect(Homebrew::Bundle).to receive(:formula_versions_from_env).and_return(foo.name => foo.version) - prefix = foo.rack/"1.0" - allow(FileTest).to receive(:directory?).and_call_original - expect(FileTest).to receive(:directory?).with(prefix.to_s).and_return(true) + prefix = foo.rack/"1.0" + allow(FileTest).to receive(:directory?).and_call_original + expect(FileTest).to receive(:directory?).with(prefix.to_s).and_return(true) - service_file = prefix/"#{foo.plist_name}.plist" - expect(Homebrew::Services::System).to receive(:launchctl?).and_return(true) - allow(FileTest).to receive(:file?).and_call_original - expect(FileTest).to receive(:file?).with(service_file.to_s).and_return(true) + service_file = prefix/service_basename + allow(FileTest).to receive(:file?).and_call_original + expect(FileTest).to receive(:file?).with(service_file.to_s).and_return(true) - expect(described_class.versioned_service_file(foo.name)).to eq(service_file) + expect(described_class.versioned_service_file(foo.name)).to eq(service_file) + end + end + + context "with launchctl" do + before do + allow(Homebrew::Services::System).to receive(:launchctl?).and_return(true) + end + + let(:service_basename) { "#{foo.plist_name}.plist" } + + include_examples "returns the versioned service file" + end + + context "with systemd" do + before do + allow(Homebrew::Services::System).to receive(:launchctl?).and_return(false) + end + + let(:service_basename) { "#{foo.service_name}.service" } + + include_examples "returns the versioned service file" end end end diff --git a/Library/Homebrew/test/bundle/commands/exec_spec.rb b/Library/Homebrew/test/bundle/commands/exec_spec.rb index d760537e20..836ca9fa05 100644 --- a/Library/Homebrew/test/bundle/commands/exec_spec.rb +++ b/Library/Homebrew/test/bundle/commands/exec_spec.rb @@ -2,6 +2,8 @@ require "bundle" require "bundle/commands/exec" +require "bundle/brewfile" +require "bundle/brew_services" RSpec.describe Homebrew::Bundle::Commands::Exec do context "when a Brewfile is not found" do @@ -124,5 +126,142 @@ RSpec.describe Homebrew::Bundle::Commands::Exec do described_class.run("/usr/bin/true") end end + + describe "--services" do + let(:brewfile_contents) { "brew 'nginx'\nbrew 'redis'" } + + let(:nginx_formula) do + instance_double( + Formula, + name: "nginx", + any_version_installed?: true, + any_installed_prefix: HOMEBREW_PREFIX/"opt/nginx", + plist_name: "homebrew.mxcl.nginx", + service_name: "nginx", + versioned_formulae_names: [], + conflicts: [instance_double(FormulaConflict, name: "httpd")], + keg_only?: false, + ) + end + + let(:redis_formula) do + instance_double( + Formula, + name: "redis", + any_version_installed?: true, + any_installed_prefix: HOMEBREW_PREFIX/"opt/redis", + plist_name: "homebrew.mxcl.redis", + service_name: "redis", + versioned_formulae_names: ["redis@6.2"], + conflicts: [], + keg_only?: false, + ) + end + + let(:services_info_pre) do + [ + { "name" => "nginx", "running" => true, "loaded" => true }, + { "name" => "httpd", "running" => true, "loaded" => true }, + { "name" => "redis", "running" => false, "loaded" => false }, + { "name" => "redis@6.2", "running" => true, "loaded" => true, "registered" => true }, + ] + end + + let(:services_info_post) do + [ + { "name" => "nginx", "running" => true, "loaded" => true }, + { "name" => "httpd", "running" => false, "loaded" => false }, + { "name" => "redis", "running" => true, "loaded" => true }, + { "name" => "redis@6.2", "running" => false, "loaded" => false, "registered" => true }, + ] + end + + before do + stub_formula_loader(nginx_formula, "nginx") + stub_formula_loader(redis_formula, "redis") + + pkgconf = formula("pkgconf") { url "pkgconf-1.0" } + stub_formula_loader(pkgconf) + allow(pkgconf).to receive(:any_version_installed?).and_return(false) + + allow_any_instance_of(Pathname).to receive(:file?).and_return(true) + + allow(described_class).to receive(:exit!).and_return(nil) + end + + shared_examples "handles service lifecycle correctly" do + it "handles service lifecycle correctly" do + # The order of operations is important. This unweildly looking test is so it tests that. + + # Return original service state + expect(Utils).to receive(:safe_popen_read) + .with(HOMEBREW_BREW_FILE, "services", "info", "--json", "nginx", "httpd", "redis", "redis@6.2") + .and_return(services_info_pre.to_json) + + # Stop original nginx + expect(Homebrew::Bundle::BrewServices).to receive(:stop) + .with("nginx", keep: true).and_return(true).ordered + + # Stop nginx conflicts + expect(Homebrew::Bundle::BrewServices).to receive(:stop) + .with("httpd", keep: true).and_return(true).ordered + + # Start new nginx + expect(Homebrew::Bundle::BrewServices).to receive(:run) + .with("nginx", file: nginx_service_file).and_return(true).ordered + + # No need to stop original redis (not started) + + # Stop redis conflicts + expect(Homebrew::Bundle::BrewServices).to receive(:stop) + .with("redis@6.2", keep: true).and_return(true).ordered + + # Start new redis + expect(Homebrew::Bundle::BrewServices).to receive(:run) + .with("redis", file: redis_service_file).and_return(true).ordered + + # Run exec commands + expect(Kernel).to receive(:system).with("/usr/bin/true").and_return(true).ordered + + # Return new service state + expect(Utils).to receive(:safe_popen_read) + .with(HOMEBREW_BREW_FILE, "services", "info", "--json", "nginx", "httpd", "redis", "redis@6.2") + .and_return(services_info_post.to_json) + + # Stop new services + expect(Homebrew::Bundle::BrewServices).to receive(:stop) + .with("nginx", keep: true).and_return(true).ordered + expect(Homebrew::Bundle::BrewServices).to receive(:stop) + .with("redis", keep: true).and_return(true).ordered + + # Restart registered services we stopped due to conflicts (skip httpd as not registered) + expect(Homebrew::Bundle::BrewServices).to receive(:run).with("redis@6.2").and_return(true).ordered + + described_class.run("/usr/bin/true", services: true) + end + end + + context "with launchctl" do + before do + allow(Homebrew::Services::System).to receive(:launchctl?).and_return(true) + end + + let(:nginx_service_file) { nginx_formula.any_installed_prefix/"#{nginx_formula.plist_name}.plist" } + let(:redis_service_file) { redis_formula.any_installed_prefix/"#{redis_formula.plist_name}.plist" } + + include_examples "handles service lifecycle correctly" + end + + context "with systemd" do + before do + allow(Homebrew::Services::System).to receive(:launchctl?).and_return(false) + end + + let(:nginx_service_file) { nginx_formula.any_installed_prefix/"#{nginx_formula.service_name}.service" } + let(:redis_service_file) { redis_formula.any_installed_prefix/"#{redis_formula.service_name}.service" } + + include_examples "handles service lifecycle correctly" + end + end end end