1# ====================================================================
2#    Licensed to the Apache Software Foundation (ASF) under one
3#    or more contributor license agreements.  See the NOTICE file
4#    distributed with this work for additional information
5#    regarding copyright ownership.  The ASF licenses this file
6#    to you under the Apache License, Version 2.0 (the
7#    "License"); you may not use this file except in compliance
8#    with the License.  You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12#    Unless required by applicable law or agreed to in writing,
13#    software distributed under the License is distributed on an
14#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15#    KIND, either express or implied.  See the License for the
16#    specific language governing permissions and limitations
17#    under the License.
18# ====================================================================
19
20require "fileutils"
21require "pathname"
22
23# Tale of a hack...
24#
25# Here we are, %SVN-WC-ROOT%/subversion/bindings/swig/ruby/test/util.rb,
26# trying to require %SVN-WC-ROOT%/subversion/bindings/swig/ruby/svn/util.rb,
27# all the while supporting both Ruby 1.8 and 1.9.  Simply using this,
28#
29#   require "svn/util"
30#
31# works for Ruby 1.8 if the CWD is subversion/bindings/swig/ruby
32# when we are running the tests, e.g.:
33#
34#   %SVN-WC-ROOT%/subversion/bindings/swig/ruby>ruby test\run-test.rb
35#
36# This is because the CWD is included in the load path when Ruby 1.8
37# searches for required files.  But this doesn't work for Ruby 1.9,
38# which doesn't include the CWD this way, so instead we could use this:
39#
40#   require "./svn/util"
41#
42# But that only works if ./svn/util is relative to the CWD (again if the
43# CWD is %SVN-WC-ROOT%/subversion/bindings/swig/ruby).  However, if we run
44# the tests from a different CWD and specify
45# %SVN-WC-ROOT%/subversion/bindings/swig/ruby as an additional $LOAD_PATH
46# using the ruby -I option, then that fails on both 1.8 and 1.9 with a
47# LoadError.
48#
49# The usual solution in a case like this is to use require_relative,
50#
51#  require_relative "../svn/util"
52#
53# But that's only available in Ruby 1.9.  We could require the backports gem
54# but there is a simple workaround, just calculate the full path of util:
55require File.join(File.dirname(__FILE__), '../svn/util')
56
57require "tmpdir"
58
59require "my-assertions"
60
61class Time
62  unless instance_methods.include?("to_int")
63    alias to_int to_i
64  end
65end
66
67require 'greek_tree'
68
69module SvnTestUtil
70  def setup_default_variables
71    @author = ENV["USER"] || "sample-user"
72    @password = "sample-password"
73    @realm = "sample realm"
74
75    @svnserve_host = "127.0.0.1"
76    @svnserve_ports = (64152..64282).collect{|x| x.to_s}
77
78    @tmp_path = Dir.mktmpdir
79    @wc_path = File.join(@tmp_path, "wc")
80    @import_path = File.join(@tmp_path, "import")
81    @repos_path = File.join(@tmp_path, "repos")
82    @svnserve_pid_file = File.join(@tmp_path, "svnserve.pid")
83    @full_repos_path = File.expand_path(@repos_path)
84    @repos_uri = "file://#{@full_repos_path.sub(/^\/?/, '/')}"
85
86    @config_path = File.join(@tmp_path, "config")
87    @greek = Greek.new(@tmp_path, @import_path, @wc_path, @repos_uri)
88  end
89
90  def setup_basic(need_svnserve=false)
91    @need_svnserve = need_svnserve
92    setup_default_variables
93    setup_tmp
94    setup_tmp(@import_path)
95    setup_repository
96    add_hooks
97    setup_svnserve if @need_svnserve
98    setup_config
99    setup_wc
100    add_authentication
101    GC.stress = true if GC.respond_to?(:stress=) and $DEBUG
102  end
103
104  def teardown_basic
105    GC.stress = false if GC.respond_to?(:stress=)
106    teardown_svnserve if @need_svnserve
107    teardown_repository
108    teardown_wc
109    teardown_config
110    teardown_tmp
111    gc
112  end
113
114  def gc
115    if $DEBUG
116      before_pools = Svn::Core::Pool.number_of_pools
117      puts
118      puts "before pools: #{before_pools}"
119    end
120    gc_enable do
121      GC.start
122    end
123    if $DEBUG
124      after_pools = Svn::Core::Pool.number_of_pools
125      puts "after pools: #{after_pools}"
126      STDOUT.flush
127    end
128  end
129
130  def change_gc_status(prev_disabled)
131    begin
132      yield
133    ensure
134      if prev_disabled
135        GC.disable
136      else
137        GC.enable
138      end
139    end
140  end
141
142  def gc_disable(&block)
143    change_gc_status(GC.disable, &block)
144  end
145
146  def gc_enable(&block)
147    change_gc_status(GC.enable, &block)
148  end
149
150  def setup_tmp(path=@tmp_path)
151    remove_recursively_with_retry(path)
152    FileUtils.mkdir_p(path)
153  end
154
155  def teardown_tmp(path=@tmp_path)
156    remove_recursively_with_retry(path)
157  end
158
159  def setup_repository(path=@repos_path, config={}, fs_config={})
160    require "svn/repos"
161    remove_recursively_with_retry(path)
162    FileUtils.mkdir_p(File.dirname(path))
163    @repos = Svn::Repos.create(path, config, fs_config)
164    @fs = @repos.fs
165  end
166
167  def teardown_repository(path=@repos_path)
168    @fs.close unless @fs.nil?
169    @repos.close unless @repos.nil?
170    remove_recursively_with_retry(path)
171    @repos = nil
172    @fs = nil
173  end
174
175  def setup_wc
176    teardown_wc
177    make_context("") { |ctx| ctx.checkout(@repos_uri, @wc_path) }
178  end
179
180  def teardown_wc
181    remove_recursively_with_retry(@wc_path)
182  end
183
184  def setup_config
185    teardown_config
186    Svn::Core::Config.ensure(@config_path)
187  end
188
189  def teardown_config
190    remove_recursively_with_retry(@config_path)
191  end
192
193  def add_authentication
194    passwd_file = "passwd"
195    File.open(@repos.svnserve_conf, "w") do |conf|
196      conf.print <<-CONF
197[general]
198anon-access = none
199auth-access = write
200password-db = #{passwd_file}
201realm = #{@realm}
202      CONF
203    end
204    File.open(File.join(@repos.conf_dir, passwd_file), "w") do |f|
205      f.print <<-PASSWD
206[users]
207#{@author} = #{@password}
208      PASSWD
209    end
210  end
211
212  def add_hooks
213    add_pre_revprop_change_hook
214  end
215
216  def youngest_rev
217    @fs.youngest_rev
218  end
219
220  def root(rev=nil)
221    @fs.root(rev)
222  end
223
224  def prop(name, rev=nil)
225    @fs.prop(name, rev)
226  end
227
228  def make_context(log)
229    ctx = Svn::Client::Context.new
230    ctx.set_log_msg_func do |items|
231      [true, log]
232    end
233    ctx.add_username_prompt_provider(0) do |cred, realm, username, may_save|
234      cred.username = @author
235      cred.may_save = false
236    end
237    ctx.config = Svn::Core::Config.config(@config_path)
238    setup_auth_baton(ctx.auth_baton)
239    return ctx unless block_given?
240    begin
241      yield ctx
242    ensure
243      ctx.destroy
244    end
245  end
246
247  def setup_auth_baton(auth_baton)
248    auth_baton[Svn::Core::AUTH_PARAM_CONFIG_DIR] = @config_path
249    auth_baton[Svn::Core::AUTH_PARAM_DEFAULT_USERNAME] = @author
250  end
251
252  def normalize_line_break(str)
253    if Svn::Util.windows?
254      str.gsub(/\n/, "\r\n")
255    else
256      str
257    end
258  end
259
260  def setup_greek_tree
261    make_context("setup greek tree") { |ctx| @greek.setup(ctx) }
262  end
263
264  def remove_recursively_with_retry(path)
265    retries = 0
266    while (retries+=1) < 100 && File.exist?(path)
267      begin
268        FileUtils.rm_r(path, :secure=>true)
269      rescue
270        sleep 0.1
271      end
272    end
273    assert(!File.exist?(path), "#{Dir.glob(path+'/**/*').join("\n")} should not exist after #{retries} attempts to delete")
274  end
275
276  module Svnserve
277    def setup_svnserve
278      @svnserve_port = nil
279      @repos_svnserve_uri = nil
280
281      # Look through the list of potential ports until we're able to
282      # successfully start svnserve on a free one.
283      @svnserve_ports.each do |port|
284        @svnserve_pid = fork {
285          STDERR.close
286          exec("svnserve",
287               "--listen-host", @svnserve_host,
288               "--listen-port", port,
289               "-d", "--foreground")
290        }
291        pid, status = Process.waitpid2(@svnserve_pid, Process::WNOHANG)
292        if status and status.exited?
293          if $DEBUG
294            STDERR.puts "port #{port} couldn't be used for svnserve"
295          end
296        else
297          # svnserve started successfully.  Note port number and cease
298          # startup attempts.
299          @svnserve_port = port
300          @repos_svnserve_uri =
301            "svn://#{@svnserve_host}:#{@svnserve_port}#{@full_repos_path}"
302          # Avoid a race by waiting a short time for svnserve to start up.
303          # Without this, tests can fail with "Connection refused" errors.
304          sleep 1
305          break
306        end
307      end
308      if @svnserve_port.nil?
309        msg = "Can't run svnserve because available port "
310        msg << "isn't exist in [#{@svnserve_ports.join(', ')}]"
311        raise msg
312      end
313    end
314
315    def teardown_svnserve
316      if @svnserve_pid
317        Process.kill(:TERM, @svnserve_pid)
318        begin
319          Process.waitpid(@svnserve_pid)
320        rescue Errno::ECHILD
321        end
322      end
323    end
324
325    def add_pre_revprop_change_hook
326      File.open(@repos.pre_revprop_change_hook, "w") do |hook|
327        hook.print <<-HOOK
328#!/bin/sh
329REPOS="$1"
330REV="$2"
331USER="$3"
332PROPNAME="$4"
333
334if [ "$PROPNAME" = "#{Svn::Core::PROP_REVISION_LOG}" -a \
335     "$USER" = "#{@author}" ]; then
336  exit 0
337fi
338
339exit 1
340        HOOK
341      end
342      FileUtils.chmod(0755, @repos.pre_revprop_change_hook)
343    end
344  end
345
346  module SetupEnvironment
347    def setup_test_environment(top_dir, base_dir, ext_dir)
348      svnserve_dir = File.join(top_dir, 'subversion', 'svnserve')
349      ENV["PATH"] = "#{svnserve_dir}:#{ENV['PATH']}"
350      FileUtils.ln_sf(File.join(base_dir, ".libs"), ext_dir)
351    end
352  end
353
354  if Svn::Util.windows?
355    require 'windows_util'
356    include Windows::Svnserve
357    extend Windows::SetupEnvironment
358  else
359    include Svnserve
360    extend SetupEnvironment
361  end
362end
363