test_env.rb 11.4 KB
Newer Older
1
require 'rspec/mocks'
Ken's avatar
Ken committed
2
require 'toml-rb'
3

4 5 6
module TestEnv
  extend self

7 8
  ComponentFailedToInstallError = Class.new(StandardError)

9 10
  # When developing the seed repository, comment out the branch you will modify.
  BRANCH_SHA = {
11
    'signed-commits'                     => '2d1096e',
12 13
    'not-merged-branch'                  => 'b83d6e3',
    'branch-merged'                      => '498214d',
14
    'empty-branch'                       => '7efb185',
15
    'ends-with.json'                     => '98b0d8b',
16 17 18 19 20
    'flatten-dir'                        => 'e56497b',
    'feature'                            => '0b4bc9a',
    'feature_conflict'                   => 'bb5206f',
    'fix'                                => '48f0be4',
    'improve/awesome'                    => '5937ac0',
21
    'merged-target'                      => '21751bf',
22
    'markdown'                           => '0ed8c6c',
23
    'lfs'                                => '55bc176',
24
    'master'                             => 'b83d6e3',
25
    'merge-test'                         => '5937ac0',
26 27 28 29 30
    "'test'"                             => 'e56497b',
    'orphaned-branch'                    => '45127a9',
    'binary-encoding'                    => '7b1cf43',
    'gitattributes'                      => '5a62481',
    'expand-collapse-diffs'              => '4842455',
31
    'symlink-expand-diff'                => '81e6355',
32 33 34
    'expand-collapse-files'              => '025db92',
    'expand-collapse-lines'              => '238e82d',
    'video'                              => '8879059',
35
    'add-balsamiq-file'                  => 'b89b56d',
36
    'crlf-diff'                          => '5938907',
Sean McGivern's avatar
Sean McGivern committed
37
    'conflict-start'                     => '824be60',
38 39
    'conflict-resolvable'                => '1450cd6',
    'conflict-binary-file'               => '259a6fb',
Sean McGivern's avatar
Sean McGivern committed
40
    'conflict-contains-conflict-markers' => '78a3086',
41
    'conflict-missing-side'              => 'eb227b3',
42
    'conflict-non-utf8'                  => 'd0a293c',
43
    'conflict-too-large'                 => '39fa04f',
44
    'deleted-image-test'                 => '6c17798',
45
    'wip'                                => 'b9238ee',
46
    'csv'                                => '3dd0896',
47
    'v1.1.0'                             => 'b83d6e3',
Phil Hughes's avatar
Phil Hughes committed
48
    'add-ipython-files'                  => '93ee732',
49
    'add-pdf-file'                       => 'e774ebd',
Felipe Artur's avatar
Felipe Artur committed
50 51
    'add-pdf-text-binary'                => '79faa7b',
    'add_images_and_changes'             => '010d106'
Douwe Maan's avatar
Douwe Maan committed
52
  }.freeze
53

54 55 56 57
  # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
  # need to keep all the branches in sync.
  # We currently only need a subset of the branches
  FORKED_BRANCH_SHA = {
58 59 60 61
    'add-submodule-version-bump' => '3f547c0',
    'master'                     => '5937ac0',
    'remove-submodule'           => '2a33e0c',
    'conflict-resolvable-fork'   => '404fa3f'
Douwe Maan's avatar
Douwe Maan committed
62
  }.freeze
63

64
  TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
65
  REPOS_STORAGE = 'default'.freeze
66

67 68
  # Test environment
  #
69
  # See gitlab.yml.example test section for paths
70
  #
71
  def init(opts = {})
72 73 74 75 76
    unless Rails.env.test?
      puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
      exit 1
    end

77 78 79
    # Disable mailer for spinach tests
    disable_mailer if opts[:mailer] == false

Robert Speicher's avatar
Robert Speicher committed
80
    clean_test_path
81

82 83 84
    # Setup GitLab shell for test instance
    setup_gitlab_shell

85
    setup_gitaly
86

87
    # Create repository for FactoryBot.create(:project)
88
    setup_factory_repo
89

90
    # Create repository for FactoryBot.create(:forked_project_with_submodules)
91
    setup_forked_repo
92 93
  end

94
  def disable_mailer
95 96
    allow_any_instance_of(NotificationService).to receive(:mailer)
      .and_return(double.as_null_object)
97
  end
98

99
  def enable_mailer
100 101
    allow_any_instance_of(NotificationService).to receive(:mailer)
      .and_call_original
102 103
  end

104
  def disable_pre_receive
105
    allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
106 107
  end

Robert Speicher's avatar
Robert Speicher committed
108 109 110 111
  # Clean /tmp/tests
  #
  # Keeps gitlab-shell and gitlab-test
  def clean_test_path
112
    Dir[TMP_TEST_PATH].each do |entry|
113
      unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/
Robert Speicher's avatar
Robert Speicher committed
114 115 116
        FileUtils.rm_rf(entry)
      end
    end
117 118 119 120

    FileUtils.mkdir_p(repos_path)
    FileUtils.mkdir_p(backup_path)
    FileUtils.mkdir_p(pages_path)
121
    FileUtils.mkdir_p(artifacts_path)
Robert Speicher's avatar
Robert Speicher committed
122 123
  end

124 125 126 127 128 129 130 131
  def clean_gitlab_test_path
    Dir[TMP_TEST_PATH].each do |entry|
      if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/
        FileUtils.rm_rf(entry)
      end
    end
  end

132
  def setup_gitlab_shell
133 134 135 136
    component_timed_setup('GitLab Shell',
      install_dir: Gitlab.config.gitlab_shell.path,
      version: Gitlab::Shell.version_required,
      task: 'gitlab:shell:install')
137
  end
138

139
  def setup_gitaly
Jacob Vosmaer's avatar
Jacob Vosmaer committed
140
    socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
141
    gitaly_dir = File.dirname(socket_path)
142

143 144 145 146
    component_timed_setup('Gitaly',
      install_dir: gitaly_dir,
      version: Gitlab::GitalyClient.expected_server_version,
      task: "gitlab:gitaly:install[#{gitaly_dir}]") do
147

148 149 150
      # Always re-create config, in case it's outdated. This is fast anyway.
      Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true)

151
      start_gitaly(gitaly_dir)
152
    end
153 154
  end

155
  def start_gitaly(gitaly_dir)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
156 157 158 159 160 161 162
    if ENV['CI'].present?
      # Gitaly has been spawned outside this process already
      return
    end

    spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
    @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i }
163 164
    Kernel.at_exit { stop_gitaly }

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
    wait_gitaly
  end

  def wait_gitaly
    sleep_time = 10
    sleep_interval = 0.1
    socket = Gitlab::GitalyClient.address('default').sub('unix:', '')

    Integer(sleep_time / sleep_interval).times do
      begin
        Socket.unix(socket)
        return
      rescue
        sleep sleep_interval
      end
    end

    raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
183 184 185 186 187 188
  end

  def stop_gitaly
    return unless @gitaly_pid

    Process.kill('KILL', @gitaly_pid)
189 190
  rescue Errno::ESRCH
    # The process can already be gone if the test run was INTerrupted.
191 192
  end

193
  def setup_factory_repo
194 195 196 197 198 199 200 201 202 203 204
    setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
               BRANCH_SHA)
  end

  # This repo has a submodule commit that is not present in the main test
  # repository.
  def setup_forked_repo
    setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name,
               FORKED_BRANCH_SHA)
  end

205
  def setup_repo(repo_path, repo_path_bare, repo_name, refs)
206
    clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
207

208
    unless File.directory?(repo_path)
209
      system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
210 211
    end

212
    set_repo_refs(repo_path, refs)
213

214 215 216 217
    unless File.directory?(repo_path_bare)
      # We must copy bare repositories because we will push to them.
      system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
    end
218 219
  end

220
  def copy_repo(project, bare_repo:, refs:)
221
    target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.disk_path}.git")
222
    FileUtils.mkdir_p(target_repo_path)
223
    FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path)
224
    FileUtils.chmod_R 0755, target_repo_path
225
    set_repo_refs(target_repo_path, refs)
226 227
  end

228
  def repos_path
229
    Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
230
  end
231

232 233 234 235
  def backup_path
    Gitlab.config.backup.path
  end

236 237 238 239
  def pages_path
    Gitlab.config.pages.path
  end

240
  def artifacts_path
241
    Gitlab.config.artifacts.storage_path
242 243
  end

244 245 246 247
  # When no cached assets exist, manually hit the root path to create them
  #
  # Otherwise they'd be created by the first test, often timing out and
  # causing a transient test failure
248
  def eager_load_driver_server
249 250
    return unless defined?(Capybara)

251
    puts "Starting the Capybara driver server..."
252
    Capybara.current_session.visit '/'
253 254
  end

255 256 257 258 259 260 261 262
  def factory_repo_path_bare
    "#{factory_repo_path}_bare"
  end

  def forked_repo_path_bare
    "#{forked_repo_path}_bare"
  end

263 264 265 266 267 268 269 270
  def with_empty_bare_repository(name = nil)
    path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s

    yield(Rugged::Repository.init_at(path, :bare))
  ensure
    FileUtils.rm_rf(path)
  end

271 272 273
  private

  def factory_repo_path
274 275 276
    @factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name)
  end

277 278 279
  def factory_repo_name
    'gitlab-test'
  end
280

281 282 283 284 285 286 287 288
  def forked_repo_path
    @forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name)
  end

  def forked_repo_name
    'gitlab-test-fork'
  end

289 290 291
  # Prevent developer git configurations from being persisted to test
  # repositories
  def git_env
292
    { 'GIT_TEMPLATE_DIR' => '' }
293
  end
294 295

  def set_repo_refs(repo_path, branch_sha)
296
    instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
297 298
    update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
    reset = proc do
299 300 301 302
      Dir.chdir(repo_path) do
        IO.popen(update_refs, "w") { |io| io.write(instructions) }
        $?.success?
      end
303 304
    end

305 306 307 308 309 310
    # Try to reset without fetching to avoid using the network.
    unless reset.call
      raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))

      # Before we used Git clone's --mirror option, bare repos could end up
      # with missing refs, clearing them and retrying should fix the issue.
311
      clean_gitlab_test_path && init unless reset.call
312 313
    end
  end
314

315 316 317 318 319 320
  def component_timed_setup(component, install_dir:, version:, task:)
    puts "\n==> Setting up #{component}..."
    start = Time.now

    ensure_component_dir_name_is_correct!(component, install_dir)

321 322 323
    # On CI, once installed, components never need update
    return if File.exist?(install_dir) && ENV['CI']

324 325 326
    if component_needs_update?(install_dir, version)
      # Cleanup the component entirely to ensure we start fresh
      FileUtils.rm_rf(install_dir)
327

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
      unless system('rake', task)
        raise ComponentFailedToInstallError
      end
    end

    yield if block_given?

  rescue ComponentFailedToInstallError
    puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"
    FileUtils.rm_rf(install_dir)
    exit 1
  ensure
    puts "    #{component} setup in #{Time.now - start} seconds...\n"
  end

  def ensure_component_dir_name_is_correct!(component, path)
    actual_component_dir_name = File.basename(path)
    expected_component_dir_name = component.parameterize

    unless actual_component_dir_name == expected_component_dir_name
      puts "    #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n"
      exit 1
    end
  end

353
  def component_needs_update?(component_folder, expected_version)
354 355 356
    # Allow local overrides of the component for tests during development
    return false if Rails.env.test? && File.symlink?(component_folder)

357
    version = File.read(File.join(component_folder, 'VERSION')).strip
358 359 360 361

    # Notice that this will always yield true when using branch versions
    # (`=branch_name`), but that actually makes sure the server is always based
    # on the latest branch revision.
362
    version != expected_version
363 364 365
  rescue Errno::ENOENT
    true
  end
366
end