 deaac7ce47
			
		
	
	
		deaac7ce47
		
			
		
	
	
	
	
		
			
			This replaces `FileUtils.cp` and `system_command! "cp"` with the new `Utils::Cp` utility where it is expected that the performance improvement outweighs the cost of the system command invocation.
		
			
				
	
	
		
			391 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			391 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| RSpec.describe Cask::Artifact::App, :cask do
 | |
|   let(:cask) { Cask::CaskLoader.load(cask_path("local-caffeine")) }
 | |
|   let(:command) { NeverSudoSystemCommand }
 | |
|   let(:adopt) { false }
 | |
|   let(:force) { false }
 | |
|   let(:app) { cask.artifacts.find { |a| a.is_a?(described_class) } }
 | |
| 
 | |
|   let(:source_path) { cask.staged_path.join("Caffeine.app") }
 | |
|   let(:target_path) { cask.config.appdir.join("Caffeine.app") }
 | |
| 
 | |
|   let(:install_phase) { app.install_phase(command:, adopt:, force:) }
 | |
|   let(:uninstall_phase) { app.uninstall_phase(command:, force:) }
 | |
| 
 | |
|   before do
 | |
|     InstallHelper.install_without_artifacts(cask)
 | |
|   end
 | |
| 
 | |
|   describe "install_phase" do
 | |
|     it "installs the given app using the proper target directory" do
 | |
|       install_phase
 | |
| 
 | |
|       expect(target_path).to be_a_directory
 | |
|       expect(source_path).to be_a_symlink
 | |
|     end
 | |
| 
 | |
|     describe "when app is in a subdirectory" do
 | |
|       let(:cask) do
 | |
|         Cask::Cask.new("subdir") do
 | |
|           url "file://#{TEST_FIXTURE_DIR}/cask/caffeine.zip"
 | |
|           homepage "https://brew.sh/local-caffeine"
 | |
|           version "1.2.3"
 | |
|           sha256 "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94"
 | |
|           app "subdir/Caffeine.app"
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       it "installs the given app using the proper target directory" do
 | |
|         appsubdir = cask.staged_path.join("subdir").tap(&:mkpath)
 | |
|         FileUtils.mv(source_path, appsubdir)
 | |
| 
 | |
|         install_phase
 | |
| 
 | |
|         expect(target_path).to be_a_directory
 | |
|         expect(appsubdir.join("Caffeine.app")).to be_a_symlink
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     it "only uses apps when they are specified" do
 | |
|       staged_app_copy = source_path.sub("Caffeine.app", "Caffeine Deluxe.app")
 | |
|       FileUtils.cp_r source_path, staged_app_copy
 | |
| 
 | |
|       install_phase
 | |
| 
 | |
|       expect(target_path).to be_a_directory
 | |
|       expect(source_path).to be_a_symlink
 | |
| 
 | |
|       expect(cask.config.appdir.join("Caffeine Deluxe.app")).not_to exist
 | |
|       expect(cask.staged_path.join("Caffeine Deluxe.app")).to exist
 | |
|     end
 | |
| 
 | |
|     describe "when the target already exists" do
 | |
|       before do
 | |
|         target_path.mkpath
 | |
|       end
 | |
| 
 | |
|       it "avoids clobbering an existing app" do
 | |
|         expect { install_phase }.to raise_error(
 | |
|           Cask::CaskError,
 | |
|           "It seems there is already an App at '#{target_path}'.",
 | |
|         )
 | |
| 
 | |
|         expect(source_path).to be_a_directory
 | |
|         expect(target_path).to be_a_directory
 | |
|         expect(File.identical?(source_path, target_path)).to be false
 | |
| 
 | |
|         contents_path = target_path.join("Contents/Info.plist")
 | |
|         expect(contents_path).not_to exist
 | |
|       end
 | |
| 
 | |
|       describe "given the adopt option" do
 | |
|         let(:adopt) { true }
 | |
| 
 | |
|         describe "when the target compares different from the source" do
 | |
|           it "avoids clobbering the existing app" do
 | |
|             stdout = <<~EOS
 | |
|               ==> Adopting existing App at '#{target_path}'
 | |
|             EOS
 | |
| 
 | |
|             expect { install_phase }
 | |
|               .to output(stdout).to_stdout
 | |
|               .and raise_error(
 | |
|                 Cask::CaskError,
 | |
|                 "It seems the existing App is different from the one being installed.",
 | |
|               )
 | |
| 
 | |
|             expect(source_path).to be_a_directory
 | |
|             expect(target_path).to be_a_directory
 | |
|             expect(File.identical?(source_path, target_path)).to be false
 | |
| 
 | |
|             contents_path = target_path.join("Contents/Info.plist")
 | |
|             expect(contents_path).not_to exist
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         describe "when the target compares the same as the source" do
 | |
|           before do
 | |
|             target_path.delete
 | |
|             FileUtils.cp_r source_path, target_path
 | |
|           end
 | |
| 
 | |
|           it "adopts the existing app" do
 | |
|             stdout = <<~EOS
 | |
|               ==> Adopting existing App at '#{target_path}'
 | |
|             EOS
 | |
| 
 | |
|             stderr = ""
 | |
| 
 | |
|             expect { install_phase }
 | |
|               .to output(stdout).to_stdout
 | |
|               .and output(stderr).to_stderr
 | |
| 
 | |
|             expect(source_path).to be_a_symlink
 | |
|             expect(target_path).to be_a_directory
 | |
| 
 | |
|             contents_path = target_path.join("Contents/Info.plist")
 | |
|             expect(contents_path).to exist
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       describe "given the force option" do
 | |
|         let(:force) { true }
 | |
| 
 | |
|         before do
 | |
|           allow(User).to receive(:current).and_return(User.new("fake_user"))
 | |
|         end
 | |
| 
 | |
|         describe "target is both writable and user-owned" do
 | |
|           it "overwrites the existing app" do
 | |
|             stdout = <<~EOS
 | |
|               ==> Removing App '#{target_path}'
 | |
|               ==> Moving App 'Caffeine.app' to '#{target_path}'
 | |
|             EOS
 | |
| 
 | |
|             stderr = <<~EOS
 | |
|               Warning: It seems there is already an App at '#{target_path}'; overwriting.
 | |
|             EOS
 | |
| 
 | |
|             expect { install_phase }
 | |
|               .to output(stdout).to_stdout
 | |
|               .and output(stderr).to_stderr
 | |
| 
 | |
|             expect(source_path).to be_a_symlink
 | |
|             expect(target_path).to be_a_directory
 | |
| 
 | |
|             contents_path = target_path.join("Contents/Info.plist")
 | |
|             expect(contents_path).to exist
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         describe "target is user-owned but contains read-only files" do
 | |
|           before do
 | |
|             FileUtils.touch "#{target_path}/foo"
 | |
|             FileUtils.chmod 0555, target_path
 | |
|           end
 | |
| 
 | |
|           after do
 | |
|             FileUtils.chmod 0755, target_path
 | |
|           end
 | |
| 
 | |
|           it "overwrites the existing app" do
 | |
|             expect(command).to receive(:run).and_call_original.at_least(:once)
 | |
| 
 | |
|             stdout = <<~EOS
 | |
|               ==> Removing App '#{target_path}'
 | |
|               ==> Moving App 'Caffeine.app' to '#{target_path}'
 | |
|             EOS
 | |
| 
 | |
|             stderr = <<~EOS
 | |
|               Warning: It seems there is already an App at '#{target_path}'; overwriting.
 | |
|             EOS
 | |
| 
 | |
|             expect { install_phase }
 | |
|               .to output(stdout).to_stdout
 | |
|               .and output(stderr).to_stderr
 | |
| 
 | |
|             expect(source_path).to be_a_symlink
 | |
|             expect(target_path).to be_a_directory
 | |
| 
 | |
|             contents_path = target_path.join("Contents/Info.plist")
 | |
|             expect(contents_path).to exist
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe "when the target is a broken symlink" do
 | |
|       let(:deleted_path) { cask.staged_path.join("Deleted.app") }
 | |
| 
 | |
|       before do
 | |
|         deleted_path.mkdir
 | |
|         File.symlink(deleted_path, target_path)
 | |
|         deleted_path.rmdir
 | |
|       end
 | |
| 
 | |
|       it "leaves the target alone" do
 | |
|         expect { install_phase }.to raise_error(
 | |
|           Cask::CaskError, "It seems there is already an App at '#{target_path}'."
 | |
|         )
 | |
|         expect(target_path).to be_a_symlink
 | |
|       end
 | |
| 
 | |
|       describe "given the force option" do
 | |
|         let(:force) { true }
 | |
| 
 | |
|         it "overwrites the existing app" do
 | |
|           stdout = <<~EOS
 | |
|             ==> Removing App '#{target_path}'
 | |
|             ==> Moving App 'Caffeine.app' to '#{target_path}'
 | |
|           EOS
 | |
| 
 | |
|           stderr = <<~EOS
 | |
|             Warning: It seems there is already an App at '#{target_path}'; overwriting.
 | |
|           EOS
 | |
| 
 | |
|           expect { install_phase }
 | |
|             .to output(stdout).to_stdout
 | |
|             .and output(stderr).to_stderr
 | |
| 
 | |
|           expect(source_path).to be_a_symlink
 | |
|           expect(target_path).to be_a_directory
 | |
| 
 | |
|           contents_path = target_path.join("Contents/Info.plist")
 | |
|           expect(contents_path).to exist
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     it "gives a warning if the source doesn't exist" do
 | |
|       source_path.rmtree
 | |
| 
 | |
|       message = "It seems the App source '#{source_path}' is not there."
 | |
| 
 | |
|       expect { install_phase }.to raise_error(Cask::CaskError, message)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "uninstall_phase" do
 | |
|     after do
 | |
|       FileUtils.chmod 0755, target_path if target_path.exist?
 | |
|       FileUtils.chmod 0755, source_path if source_path.exist?
 | |
|     end
 | |
| 
 | |
|     it "deletes managed apps" do
 | |
|       install_phase
 | |
| 
 | |
|       expect(target_path).to exist
 | |
| 
 | |
|       uninstall_phase
 | |
| 
 | |
|       expect(target_path).not_to exist
 | |
|     end
 | |
| 
 | |
|     it "backs up read-only managed apps" do
 | |
|       install_phase
 | |
| 
 | |
|       FileUtils.chmod 0544, target_path
 | |
| 
 | |
|       uninstall_phase
 | |
| 
 | |
|       expect(source_path).to be_a_directory
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "summary" do
 | |
|     let(:description) { app.class.english_description }
 | |
|     let(:contents) { app.summarize_installed }
 | |
| 
 | |
|     it "returns the correct english_description" do
 | |
|       expect(description).to eq("Apps")
 | |
|     end
 | |
| 
 | |
|     describe "app is correctly installed" do
 | |
|       it "returns the path to the app" do
 | |
|         install_phase
 | |
| 
 | |
|         expect(contents).to eq("#{target_path} (#{target_path.abv})")
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe "app is missing" do
 | |
|       it "returns a warning and the supposed path to the app" do
 | |
|         expect(contents).to match(/.*Missing App.*: #{target_path}/)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "upgrade" do
 | |
|     before do
 | |
|       install_phase
 | |
|     end
 | |
| 
 | |
|     # Fix for https://github.com/Homebrew/homebrew-cask/issues/102721
 | |
|     it "reuses the same directory" do
 | |
|       contents_path = target_path.join("Contents/Info.plist")
 | |
| 
 | |
|       expect(target_path).to exist
 | |
|       inode = target_path.stat.ino
 | |
|       expect(contents_path).to exist
 | |
| 
 | |
|       app.uninstall_phase(command:, force:, successor: cask)
 | |
| 
 | |
|       expect(target_path).to exist
 | |
|       expect(target_path.children).to be_empty
 | |
|       expect(contents_path).not_to exist
 | |
| 
 | |
|       app.install_phase(command:, adopt:, force:, predecessor: cask)
 | |
|       expect(target_path).to exist
 | |
|       expect(target_path.stat.ino).to eq(inode)
 | |
| 
 | |
|       expect(contents_path).to exist
 | |
|     end
 | |
| 
 | |
|     describe "when the system blocks modifying apps" do
 | |
|       it "uninstalls and reinstalls the app" do
 | |
|         target_contents_path = target_path.join("Contents")
 | |
| 
 | |
|         expect(File).to receive(:write).with(target_path / ".homebrew-write-test",
 | |
|                                              instance_of(String)).and_raise(Errno::EACCES)
 | |
| 
 | |
|         app.uninstall_phase(command:, force:, successor: cask)
 | |
|         expect(target_path).not_to exist
 | |
| 
 | |
|         app.install_phase(command:, adopt:, force:, predecessor: cask)
 | |
|         expect(target_contents_path).to exist
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe "when the directory is owned by root" do
 | |
|       before do
 | |
|         allow(app.target).to receive_messages(writable?: false, owned?: false)
 | |
|       end
 | |
| 
 | |
|       it "reuses the same directory" do
 | |
|         source_contents_path = source_path.join("Contents")
 | |
|         target_contents_path = target_path.join("Contents")
 | |
| 
 | |
|         allow(command).to receive(:run!).with(any_args).and_call_original
 | |
| 
 | |
|         expect(command).to receive(:run!)
 | |
|           .with(a_string_ending_with("cp"),
 | |
|                 hash_including(args: include(source_contents_path, target_path),
 | |
|                                sudo: true))
 | |
|           .and_call_original
 | |
|         expect(FileUtils).not_to receive(:move).with(source_contents_path, an_instance_of(Pathname))
 | |
| 
 | |
|         app.uninstall_phase(command:, force:, successor: cask)
 | |
|         expect(target_contents_path).not_to exist
 | |
|         expect(target_path).to exist
 | |
|         expect(source_contents_path).to exist
 | |
| 
 | |
|         app.install_phase(command:, adopt:, force:, predecessor: cask)
 | |
|         expect(target_contents_path).to exist
 | |
|       end
 | |
| 
 | |
|       describe "when the system blocks modifying apps" do
 | |
|         it "uninstalls and reinstalls the app" do
 | |
|           target_contents_path = target_path.join("Contents")
 | |
| 
 | |
|           allow(command).to receive(:run!).with(any_args).and_call_original
 | |
| 
 | |
|           expect(command).to receive(:run!)
 | |
|             .with("touch", args:         [target_path / ".homebrew-write-test"],
 | |
|                            print_stderr: false,
 | |
|                            sudo:         true)
 | |
|             .and_raise(ErrorDuringExecution.new([], status: 1,
 | |
| output: [[:stderr, "touch: #{target_path}/.homebrew-write-test: Operation not permitted\n"]], secrets: []))
 | |
| 
 | |
|           app.uninstall_phase(command:, force:, successor: cask)
 | |
|           expect(target_path).not_to exist
 | |
| 
 | |
|           app.install_phase(command:, adopt:, force:, predecessor: cask)
 | |
|           expect(target_contents_path).to exist
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |