1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
|
# frozen_string_literal: true
require 'date'
require 'fileutils'
require 'minitar'
require 'test/unit'
require 'mocha/test_unit'
require 'tmpdir'
require 'git'
$stdout.sync = true
$stderr.sync = true
# Make tests that emit a deprecation warning fail
#
# Deprecation warnings should not be ignored.
#
# This is important so that:
# * when a user sees a deprecation warning, they can be confident it is coming from
# their code and not this gem
# * test output is clean and does not contain noisey deprecation warnings
#
# Tests whose purpose is to test that a deprecation warning is issued in the right
# circumstance should mock Git::Deprecation#warn to avoid raising an error.
#
Git::Deprecation.behavior = :raise
module Test
module Unit
# A base class for all test cases in this project
#
# This class provides utility methods for setting up and tearing down test
# environments, creating temporary repositories, and mocking the Git binary.
#
class TestCase
TEST_ROOT = File.expand_path(__dir__)
TEST_FIXTURES = File.join(TEST_ROOT, 'files')
BARE_REPO_PATH = File.join(TEST_FIXTURES, 'working.git')
def clone_working_repo
@wdir = create_temp_repo('working')
end
teardown
def git_teardown
FileUtils.rm_r(@tmp_path) if instance_variable_defined?(:@tmp_path)
end
def in_bare_repo_clone
in_temp_dir do |_path|
git = Git.clone(BARE_REPO_PATH, 'bare')
Dir.chdir('bare') do
yield git
end
end
end
def in_temp_repo(clone_name, &)
clone_path = create_temp_repo(clone_name)
Dir.chdir(clone_path, &)
end
def create_temp_repo(clone_name)
clone_path = File.join(TEST_FIXTURES, clone_name)
filename = "git_test#{Time.now.to_i}#{rand(300).to_s.rjust(3, '0')}"
path = File.expand_path(File.join(Dir.tmpdir, filename))
FileUtils.mkdir_p(path)
@tmp_path = File.realpath(path)
FileUtils.cp_r(clone_path, @tmp_path)
tmp_path = File.join(@tmp_path, File.basename(clone_path))
FileUtils.cd tmp_path do
FileUtils.mv('dot_git', '.git')
end
tmp_path
end
# Creates a temp directory and yields that path to the passed block
#
# On Windows, using Dir.mktmpdir with a block sometimes raises an error:
# `Errno::ENOTEMPTY: Directory not empty @ dir_s_rmdir`. I think this might
# be a configuration issue with the Windows CI environment.
#
# This was worked around by using the non-block form of Dir.mktmpdir and
# then removing the directory manually in an ensure block.
#
def in_temp_dir
tmpdir = Dir.mktmpdir
tmpdir_realpath = File.realpath(tmpdir)
Dir.chdir(tmpdir_realpath) do
yield tmpdir_realpath
end
ensure
FileUtils.rm_rf(tmpdir_realpath) if tmpdir_realpath
# raise "Temp dir #{tmpdir} not removed. Remaining files : #{Dir["#{tmpdir}/**/*"]}" if File.exist?(tmpdir)
end
def create_file(path, content)
File.open(path, 'w') do |file|
file.puts(content)
end
end
def update_file(path, content)
create_file(path, content)
end
def delete_file(path)
File.delete(path)
end
def move_file(source_path, target_path)
File.rename source_path, target_path
end
def new_file(name, contents)
create_file(name, contents)
end
def append_file(name, contents)
File.open(name, 'a') do |f|
f.puts contents
end
end
# Assert that the expected command line is generated by a given Git::Base method
#
# This assertion generates an empty git repository and then yields to the
# given block passing the Git::Base instance for the empty repository. The
# current directory is set to the root of the repository's working tree.
#
#
# @example Test that calling `git.fetch` generates the command line `git fetch`
# # Only need to specify the arguments to the git command
# expected_command_line = ['fetch']
# assert_command_line_eq(expected_command_line) { |git| git.fetch }
#
# @example Test that calling `git.fetch('origin', { ref: 'master', depth: '2' })` generates the command line `git fetch --depth 2 -- origin master`
# expected_command_line = ['fetch', '--depth', '2', '--', 'origin', 'master']
# assert_command_line_eq(expected_command_line) { |git| git.fetch('origin', { ref: 'master', depth: '2' }) }
#
# @param expected_command_line [Array<String>] The expected arguments to be sent to Git::Lib#command
# @param git_output [String] The mocked output to be returned by the Git::Lib#command method
#
# @yield [git] a block to call the method to be tested
# @yieldparam git [Git::Base] The Git::Base object resulting from initializing the test project
# @yieldreturn [void] the return value of the block is ignored
#
# @return [void]
#
def assert_command_line_eq(expected_command_line, method: :command, mocked_output: '', include_env: false)
actual_command_line = nil
command_output = ''
in_temp_dir do |_path|
git = Git.init('test_project')
git.lib.define_singleton_method(method) do |*cmd, **opts|
actual_command_line = if include_env
[env_overrides, *cmd, opts]
else
[*cmd, opts]
end
mocked_output
end
Dir.chdir 'test_project' do
yield(git) if block_given?
end
end
expected_command_line = expected_command_line.call if expected_command_line.is_a?(Proc)
assert_equal(expected_command_line, actual_command_line)
command_output
end
def assert_child_process_success
yield
assert_equal 0, $CHILD_STATUS.exitstatus, "Child process failed with exitstatus #{$CHILD_STATUS.exitstatus}"
end
def windows_platform?
# Check if on Windows via RUBY_PLATFORM (CRuby) and RUBY_DESCRIPTION (JRuby)
win_platform_regex = /mingw|mswin/
RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex
end
def jruby_platform?
RUBY_PLATFORM == 'java'
end
# Run a command and return the status including stdout and stderr output
#
# @example
# command = %w[git status]
# status = run(command)
# status.success? # => true
# status.exitstatus # => 0
# status.out # => "On branch master\nnothing to commit, working tree clean\n"
# status.err # => ""
#
# @param command [Array<String>] The command to run
# @param timeout [Numeric, nil] Seconds to allow command to run before killing it or nil for no timeout
# @param raise_errors [Boolean] Raise an exception if the command fails
# @param error_message [String] The message to use when raising an exception
#
# @return [CommandResult] The result of running
#
def run_command(*command, raise_errors: true, error_message: "#{command[0]} failed")
result = ProcessExecuter.run_with_capture(*command, raise_errors: false)
raise "#{error_message}: #{result.stderr}" if raise_errors && !result.success?
result
end
end
end
end
# Replace the default git binary with the given script
#
# This method creates a temporary directory and writes the given script to a file
# named `git` in a subdirectory named `bin`. This subdirectory name can be changed by
# passing a different value for the `subdir` parameter.
#
# On non-windows platforms, make sure the script starts with a hash bang. On windows,
# make sure the script has a `.bat` extension.
#
# On non-windows platforms, the script is made executable.
#
# `Git::Base.config.binary_path` set to the path to the script.
#
# The block is called passing the path to the mocked git binary.
#
# `Git::Base.config.binary_path` is reset to its original value after the block
# returns.
#
# @example mocked_git_script = <<~GIT_SCRIPT #!/bin/sh puts 'git version 1.2.3'
# GIT_SCRIPT
#
# mock_git_binary(mocked_git_script) do
# # Run Git commands here -- they will call the mocked git script
# end
#
# @param script [String] The bash script to run instead of the real git binary
#
# @param subdir [String] The subdirectory to place the mocked git binary in
#
# @yield Call the block while the git binary is mocked
#
# @yieldparam git_binary_path [String] The path to the mocked git binary
#
# @yieldreturn [void] the return value of the block is ignored
#
# @return [void]
#
def mock_git_binary(script, subdir: 'bin')
Dir.mktmpdir do |binary_dir|
binary_name = windows_platform? ? 'git.bat' : 'git'
git_binary_path = File.join(binary_dir, subdir, binary_name)
FileUtils.mkdir_p(File.dirname(git_binary_path))
File.write(git_binary_path, script)
File.chmod(0o755, git_binary_path) unless windows_platform?
saved_binary_path = Git::Base.config.binary_path
Git::Base.config.binary_path = git_binary_path
yield git_binary_path
Git::Base.config.binary_path = saved_binary_path
end
end
|