# frozen_string_literal: true require "formula" require "service" describe Homebrew::Service do let(:name) { "formula_name" } def stub_formula(&block) formula(name) do url "https://brew.sh/test-1.0.tbz" instance_eval(&block) if block end end describe "#std_service_path_env" do it "returns valid std_service_path_env" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :immediate environment_variables PATH: std_service_path_env error_log_path var/"log/beanstalkd.error.log" log_path var/"log/beanstalkd.log" working_dir var keep_alive true end end path = f.service.std_service_path_env expect(path).to eq("#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin") end end describe "#process_type" do it "throws for unexpected type" do f = stub_formula do service do run opt_bin/"beanstalkd" process_type :cow end end expect do f.service.manual_command end.to raise_error TypeError, "Service#process_type allows: 'background'/'standard'/'interactive'/'adaptive'" end end describe "#keep_alive" do it "throws for unexpected keys" do f = stub_formula do service do run opt_bin/"beanstalkd" keep_alive test: "key" end end expect do f.service.manual_command end.to raise_error TypeError, "Service#keep_alive allows only [:always, :successful_exit, :crashed, :path]" end end describe "#requires_root?" do it "returns status when set" do f = stub_formula do service do run opt_bin/"beanstalkd" require_root true end end expect(f.service.requires_root?).to be(true) end it "returns status when not set" do f = stub_formula do service do run opt_bin/"beanstalkd" end end expect(f.service.requires_root?).to be(false) end end describe "#run_type" do it "throws for unexpected type" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :cow end end expect do f.service.manual_command end.to raise_error TypeError, "Service#run_type allows: 'immediate'/'interval'/'cron'" end end describe "#sockets" do it "throws for missing type" do f = stub_formula do service do run opt_bin/"beanstalkd" sockets "127.0.0.1:80" end end expect do f.service.manual_command end.to raise_error TypeError, "Service#sockets a formatted socket definition as ://:" end it "throws for missing host" do f = stub_formula do service do run opt_bin/"beanstalkd" sockets "tcp://:80" end end expect do f.service.manual_command end.to raise_error TypeError, "Service#sockets a formatted socket definition as ://:" end it "throws for missing port" do f = stub_formula do service do run opt_bin/"beanstalkd" sockets "tcp://127.0.0.1" end end expect do f.service.manual_command end.to raise_error TypeError, "Service#sockets a formatted socket definition as ://:" end end describe "#manual_command" do it "returns valid manual_command" do f = stub_formula do service do run "#{HOMEBREW_PREFIX}/bin/beanstalkd" run_type :immediate environment_variables PATH: std_service_path_env, ETC_DIR: etc/"beanstalkd" error_log_path var/"log/beanstalkd.error.log" log_path var/"log/beanstalkd.log" working_dir var keep_alive true end end path = f.service.manual_command expect(path).to eq("ETC_DIR=\"#{HOMEBREW_PREFIX}/etc/beanstalkd\" #{HOMEBREW_PREFIX}/bin/beanstalkd") end it "returns valid manual_command without variables" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :immediate environment_variables PATH: std_service_path_env error_log_path var/"log/beanstalkd.error.log" log_path var/"log/beanstalkd.log" working_dir var keep_alive true end end path = f.service.manual_command expect(path).to eq("#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd") end end describe "#to_plist" do it "returns valid plist" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] run_type :immediate environment_variables PATH: std_service_path_env, FOO: "BAR", ETC_DIR: etc/"beanstalkd" error_log_path var/"log/beanstalkd.error.log" log_path var/"log/beanstalkd.log" input_path var/"in/beanstalkd" require_root true root_dir var working_dir var keep_alive true launch_only_once true process_type :interactive restart_delay 30 interval 5 macos_legacy_timers true end end plist = f.service.to_plist plist_expect = <<~EOS \tEnvironmentVariables \t \t\tETC_DIR \t\t#{HOMEBREW_PREFIX}/etc/beanstalkd \t\tFOO \t\tBAR \t\tPATH \t\t#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin \t \tKeepAlive \t \tLabel \thomebrew.mxcl.formula_name \tLaunchOnlyOnce \t \tLegacyTimers \t \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProcessType \tInteractive \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t\ttest \t \tRootDirectory \t#{HOMEBREW_PREFIX}/var \tRunAtLoad \t \tStandardErrorPath \t#{HOMEBREW_PREFIX}/var/log/beanstalkd.error.log \tStandardInPath \t#{HOMEBREW_PREFIX}/var/in/beanstalkd \tStandardOutPath \t#{HOMEBREW_PREFIX}/var/log/beanstalkd.log \tTimeOut \t30 \tWorkingDirectory \t#{HOMEBREW_PREFIX}/var EOS expect(plist).to eq(plist_expect) end it "returns valid plist with socket" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] sockets "tcp://127.0.0.1:80" end end plist = f.service.to_plist plist_expect = <<~EOS \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t\ttest \t \tRunAtLoad \t \tSockets \t \t\tListeners \t\t \t\t\tSockFamily \t\t\tIPv4v6 \t\t\tSockNodeName \t\t\t127.0.0.1 \t\t\tSockProtocol \t\t\tTCP \t\t\tSockServiceName \t\t\t80 \t\t \t EOS expect(plist).to eq(plist_expect) end it "returns valid partial plist" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :immediate end end plist = f.service.to_plist plist_expect = <<~EOS \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t \tRunAtLoad \t EOS expect(plist).to eq(plist_expect) end it "returns valid partial plist with run_at_load being false" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :immediate run_at_load false end end plist = f.service.to_plist plist_expect = <<~EOS \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t \tRunAtLoad \t EOS expect(plist).to eq(plist_expect) end it "returns valid interval plist" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :interval interval 5 end end plist = f.service.to_plist plist_expect = <<~EOS \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t \tRunAtLoad \t \tStartInterval \t5 EOS expect(plist).to eq(plist_expect) end it "returns valid cron plist" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :cron cron "@daily" end end plist = f.service.to_plist plist_expect = <<~EOS \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t \tRunAtLoad \t \tStartCalendarInterval \t \t\tHour \t\t0 \t\tMinute \t\t0 \t EOS expect(plist).to eq(plist_expect) end it "returns valid keepalive-exit plist" do f = stub_formula do service do run opt_bin/"beanstalkd" keep_alive successful_exit: false end end plist = f.service.to_plist plist_expect = <<~EOS \tKeepAlive \t \t\tSuccessfulExit \t\t \t \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t \tRunAtLoad \t EOS expect(plist).to eq(plist_expect) end it "returns valid keepalive-crashed plist" do f = stub_formula do service do run opt_bin/"beanstalkd" keep_alive crashed: true end end plist = f.service.to_plist plist_expect = <<~EOS \tKeepAlive \t \t\tCrashed \t\t \t \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t \tRunAtLoad \t EOS expect(plist).to eq(plist_expect) end it "returns valid keepalive-path plist" do f = stub_formula do service do run opt_bin/"beanstalkd" keep_alive path: opt_pkgshare/"test-path" end end plist = f.service.to_plist plist_expect = <<~EOS \tKeepAlive \t \t\tPathState \t\t#{HOMEBREW_PREFIX}/opt/formula_name/share/formula_name/test-path \t \tLabel \thomebrew.mxcl.formula_name \tLimitLoadToSessionType \t \t\tAqua \t\tBackground \t\tLoginWindow \t\tStandardIO \t\tSystem \t \tProgramArguments \t \t\t#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd \t \tRunAtLoad \t EOS expect(plist).to eq(plist_expect) end end describe "#to_systemd_unit" do it "returns valid unit" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] run_type :immediate environment_variables PATH: std_service_path_env, FOO: "BAR" error_log_path var/"log/beanstalkd.error.log" log_path var/"log/beanstalkd.log" input_path var/"in/beanstalkd" require_root true root_dir var working_dir var keep_alive true process_type :interactive restart_delay 30 macos_legacy_timers true end end unit = f.service.to_systemd_unit std_path = "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin" unit_expect = <<~EOS [Unit] Description=Homebrew generated unit for formula_name [Install] WantedBy=default.target [Service] Type=simple ExecStart=#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd test Restart=always RestartSec=30 WorkingDirectory=#{HOMEBREW_PREFIX}/var RootDirectory=#{HOMEBREW_PREFIX}/var StandardInput=file:#{HOMEBREW_PREFIX}/var/in/beanstalkd StandardOutput=append:#{HOMEBREW_PREFIX}/var/log/beanstalkd.log StandardError=append:#{HOMEBREW_PREFIX}/var/log/beanstalkd.error.log Environment="PATH=#{std_path}" Environment="FOO=BAR" EOS expect(unit).to eq(unit_expect.strip) end it "returns valid partial oneshot unit" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :immediate launch_only_once true end end unit = f.service.to_systemd_unit unit_expect = <<~EOS [Unit] Description=Homebrew generated unit for formula_name [Install] WantedBy=default.target [Service] Type=oneshot ExecStart=#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd EOS expect(unit).to eq(unit_expect.strip) end end describe "#to_systemd_timer" do it "returns valid timer" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] run_type :interval interval 5 end end unit = f.service.to_systemd_timer unit_expect = <<~EOS [Unit] Description=Homebrew generated timer for formula_name [Install] WantedBy=timers.target [Timer] Unit=homebrew.formula_name OnUnitActiveSec=5 EOS expect(unit).to eq(unit_expect.strip) end it "returns valid partial timer" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :immediate end end unit = f.service.to_systemd_timer unit_expect = <<~EOS [Unit] Description=Homebrew generated timer for formula_name [Install] WantedBy=timers.target [Timer] Unit=homebrew.formula_name EOS expect(unit).to eq(unit_expect) end it "throws on incomplete cron" do f = stub_formula do service do run opt_bin/"beanstalkd" run_type :cron cron "1 2 3 4" end end expect do f.service.to_systemd_timer end.to raise_error TypeError, "Service#parse_cron expects a valid cron syntax" end it "returns valid cron timers" do styles = { "@hourly": "*-*-*-* *:00:00", "@daily": "*-*-*-* 00:00:00", "@weekly": "0-*-*-* 00:00:00", "@monthly": "*-*-*-1 00:00:00", "@yearly": "*-*-1-1 00:00:00", "@annually": "*-*-1-1 00:00:00", "5 5 5 5 5": "5-*-5-5 05:05:00", } styles.each do |cron, calendar| f = stub_formula do service do run opt_bin/"beanstalkd" run_type :cron cron cron.to_s end end unit = f.service.to_systemd_timer unit_expect = <<~EOS [Unit] Description=Homebrew generated timer for formula_name [Install] WantedBy=timers.target [Timer] Unit=homebrew.formula_name Persistent=true OnCalendar=#{calendar} EOS expect(unit).to eq(unit_expect.chomp) end end end describe "#timed?" do it "returns false for immediate" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] run_type :immediate end end expect(f.service.timed?).to be(false) end it "returns true for interval" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] run_type :interval end end expect(f.service.timed?).to be(true) end end describe "#keep_alive?" do it "returns true when keep_alive set to hash" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] keep_alive crashed: true end end expect(f.service.keep_alive?).to be(true) end it "returns true when keep_alive set to true" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] keep_alive true end end expect(f.service.keep_alive?).to be(true) end it "returns false when keep_alive not set" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] end end expect(f.service.keep_alive?).to be(false) end it "returns false when keep_alive set to false" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] keep_alive false end end expect(f.service.keep_alive?).to be(false) end end describe "#command" do it "returns @run data" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] run_type :immediate end end command = f.service.command expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test"]) end it "returns @run data on Linux", :needs_linux do f = stub_formula do service do run linux: [opt_bin/"beanstalkd", "test"] run_type :immediate end end command = f.service.command expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test"]) end it "returns nil on Linux", :needs_linux do f = stub_formula do service do run macos: [opt_bin/"beanstalkd", "test"] run_type :immediate end end command = f.service.command expect(command).to be_nil end it "returns @run data on macOS", :needs_macos do f = stub_formula do service do run macos: [opt_bin/"beanstalkd", "test"] run_type :immediate end end command = f.service.command expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test"]) end it "returns nil on macOS", :needs_macos do f = stub_formula do service do run linux: [opt_bin/"beanstalkd", "test"] run_type :immediate end end command = f.service.command expect(command).to be_nil end it "returns appropriate @run data on Linux", :needs_linux do f = stub_formula do service do run macos: [opt_bin/"beanstalkd", "test", "macos"], linux: [opt_bin/"beanstalkd", "test", "linux"] run_type :immediate end end command = f.service.command expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test", "linux"]) end it "returns appropriate @run data on macOS", :needs_macos do f = stub_formula do service do run macos: [opt_bin/"beanstalkd", "test", "macos"], linux: [opt_bin/"beanstalkd", "test", "linux"] run_type :immediate end end command = f.service.command expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test", "macos"]) end end describe "#serialize" do let(:serialized_hash) do { environment_variables: { PATH: "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/bin:/bin:/usr/sbin:/sbin", }, run: [Pathname("$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd"), "test"], run_type: :immediate, working_dir: "/$HOME", cron: "0 0 * * 0", sockets: "tcp://0.0.0.0:80", } end # @note The calls to `Formula.generating_hash!` and `Formula.generated_hash!` # are not idempotent so they can only be used in one test. it "replaces local paths with placeholders" do f = stub_formula do service do run [opt_bin/"beanstalkd", "test"] environment_variables PATH: std_service_path_env working_dir Dir.home cron "@weekly" sockets "tcp://0.0.0.0:80" end end Formula.generating_hash! expect(f.service.serialize).to eq(serialized_hash) Formula.generated_hash! end end describe ".deserialize" do let(:serialized_hash) do { "environment_variables" => { "PATH" => "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/bin:/bin:/usr/sbin:/sbin", }, "run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"], "run_type" => "immediate", "working_dir" => HOMEBREW_HOME_PLACEHOLDER, "keep_alive" => { "successful_exit" => false }, } end let(:deserialized_hash) do { environment_variables: { PATH: "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin", }, run: ["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "test"], run_type: :immediate, working_dir: Dir.home, keep_alive: { successful_exit: false }, } end it "replaces placeholders with local paths" do expect(described_class.deserialize(serialized_hash)).to eq(deserialized_hash) end describe "run command" do it "handles String argument correctly" do expect(described_class.deserialize({ "run" => "$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", })).to eq({ run: "#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", }) end it "handles Array argument correctly" do expect(described_class.deserialize({ "run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "--option"], })).to eq({ run: ["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "--option"], }) end it "handles Hash argument correctly" do expect(described_class.deserialize({ "run" => { "linux" => "$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "macos" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "--option"], }, })).to eq({ run: { linux: "#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", macos: ["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "--option"], }, }) end end end end