わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

svnとgitの基本コマンドをTest::Unitで書いてみた

ここ最近,SubversionからGitへの移行を考えています.TortoiseGitは,すでに複数のWindows PCに入れました.研究室の学生に使わせるかどうかは,分かりません.むしろ,後期の演習科目で,「svnかgitを使ってファイルを管理すること」を要請しようと考えていて*1,最低限のコマンド(svn,gitに与える引数)とその効果について,自習しないといけないなと思っていたのでした.
動作確認の方法として,コマンドを順に並べて,当雑記で解説するのが手っ取り早いのですが,受講生がそのエントリしか見ない(バージョン管理の意義を学ばずに,使い方だけを知る)というのはよろしくないので,何か工夫をしたいなあと考えていたところに,RubyのTest::Unit*2を思い出しました.
Test::Unitは,通常,テスト対象のRubyスクリプトがあって(もしくはtest-driven developmentだと,コードを付け加えていきながら),それに対するテストを書きます.なのですが,Rubysvnやgitのコマンドに相当するアクセスをしたいという意図ではなく*3,関心があるのはsvnやgitのコマンドのほうなので,テスト対象のRubyスクリプトを作らず,テストのコードだけを書きました.
論よりcodeです:

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

# svn-test.rb

require "test/unit"
require "fileutils"

class SvnTest < Test::Unit::TestCase
  #### setup & teardown ####
  def setup
    ENV["LC_ALL"] = "C"
    @option_fullpath = false
    @option_print_command_only = false
    if @option_fullpath
      @command_svn = "/usr/bin/svn"
      @command_svnadmin = "/usr/bin/svnadmin"
    else
      @command_svn = "svn"
      @command_svnadmin = "svnadmin"
    end
    @dir = {
      :work => "svn-work",
      # :work2 => "svn-work2",
      :repo => "svn-repository"
    }
    @url = "file://" + File.expand_path(@dir[:repo])
    clear_directory
  end

  def teardown
    return if $DEBUG
    clear_directory
  end

  #### auxiliary methods ####
  def rm_r(dir)
    method = nil
    case method
    when 1
      print_prompt
      if FileUtils.methods.include?(:remove_entry_secure)
        print_prompt
        puts "rm -Rf #{dir}" unless @option_print_command_only
        FileUtils.remove_entry_secure(dir)
      else
        FileUtils.rm_r(dir, :verbose => true)
      end
    when 2
      my_system "rm -Rf #{dir}"
    else
      print_prompt
      FileUtils.rm_r(dir, :verbose => true)
    end
  end

  def clear_directory
    @dir.each_value do |d|
      if test(?d, d)
        rm_r(d)
      end
    end
  end

  def print_prompt
    print "> "
  end

  def my_system(command)
    if /^svnadmin / =~ command
      command["svnadmin"] = @command_svnadmin
    elsif /^svn / =~ command
      command["svn"] = @command_svn
    end

    print_prompt
    puts command
    if @option_print_command_only
      command += ">/dev/null 2>&1"
    end
    system command
  end

  def my_exec(command)
    if /^svnadmin / =~ command
      command["svnadmin"] = @command_svnadmin
    elsif /^svn / =~ command
      command["svn"] = @command_svn
    end

    print_prompt
    puts command
    result = `#{command}`
    puts result if !result.empty? && !@option_print_command_only
    result
  end

  def create_workfile(filename, num = -1)
    open(filename, "w") do |f_out|
      1.upto(10) do |i|
        if i >= num
          f_out.puts "No. #{i}"
        else
          f_out.puts "#{i}行目"
        end
      end
    end
  end

  #### test methods ####
  def test_svn1
    txt = "test.txt"

    # create
    my_system "svnadmin create #{@dir[:repo]}"
    assert(test(?d, @dir[:repo]))
    assert(test(?f, File.join(@dir[:repo], "README.txt")))

    # checkout
    my_system "svn checkout #{@url} #{@dir[:work]}"
    assert(test(?d, @dir[:work]))
    assert(test(?d, File.join(@dir[:work], ".svn")))

    print_prompt
    FileUtils.cd(@dir[:work], :verbose => true) do
      # add file
      create_workfile(txt)
      my_system "cat #{txt}"
      result = my_exec("svn status")
      assert(/^\?\s*#{txt}/ =~ result)
      assert(/^A\s*#{txt}/ !~ result)

      my_system "svn add #{txt}"
      result = my_exec("svn status")
      assert(/^A\s*#{txt}/ =~ result)
      assert(/^\?\s*#{txt}/ !~ result)

      # first commit
      my_system "svn commit -m \"first commit\""
      result = my_exec("svn status")
      assert(result.index(txt) == nil)

      # modify file repeatedly
      1.upto(10) do |i|
        create_workfile(txt, i)
        my_system "cat #{txt}"
        if i == 1
          result = my_exec("svn status")
          assert(result.empty?)
          next
        end
        if i == 5
          result = my_exec("svn status")
          assert(/^M\s*#{txt}/ =~ result)
        end
        my_system "svn commit -m \"#{txt}: a bit changed\""
      end

      # diff (working copy)
      result = my_exec("svn diff -r 3:5 #{txt}")
      assert(/^-.*revision 3/ =~ result)
      assert(/^\+.*revision 5/ =~ result)
      assert(result.index("-No. 3") != nil)
      assert(result.index("+4行目") != nil)
      assert(result.index("+5行目") == nil)

      # diff (repository)
      result2 = my_exec("svn diff -r 3:5 #{@url}/#{txt}")
      assert(result2 == result)

      print_prompt
    end
  end
end
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

# git-test.rb

require "test/unit"
require "fileutils"

class GitTest < Test::Unit::TestCase
  #### setup & teardown ####
  def setup
    ENV["LC_ALL"] = "C"
    @option_fullpath = false
    @option_print_command_only = false
    if @option_fullpath
      @command_git = "/usr/bin/git"
    else
      @command_git = "git"
    end
    @dir = {
      :work => "git-work",
      :work2 => "git-work2",
      :repo => "git-repository.git"
    }
    @url = "file://" + File.expand_path(@dir[:repo])
    clear_directory
  end

  def teardown
    return if $DEBUG
    clear_directory
  end

  #### auxiliary methods ####
  def rm_r(dir)
    method = 2
    case method
    when 1
      print_prompt
      if FileUtils.methods.include?(:remove_entry_secure)
        print_prompt
        puts "rm -Rf #{dir}" unless @option_print_command_only
        FileUtils.remove_entry_secure(dir)
      else
        FileUtils.rm_r(dir, :verbose => true)
      end
    when 2
      my_system "rm -Rf #{dir}"
    else
      print_prompt
      FileUtils.rm_r(dir, :verbose => true)
    end
  end

  def clear_directory
    @dir.each_value do |d|
      if test(?d, d)
        rm_r(d)
      end
    end
  end

  def print_prompt
    print "> "
  end

  def my_system(command)
    if /^git / =~ command
      command["git"] = @command_git
    end

    print_prompt
    puts command
    if @option_print_command_only
      command += ">/dev/null 2>&1"
    end
    system command
  end

  def my_exec(command)
    if /^git / =~ command
      command["git"] = @command_git
    end

    print_prompt
    puts command
    result = `#{command}`
    puts result if !result.empty? && !@option_print_command_only
    result
  end

  def create_workfile(filename, num = -1)
    open(filename, "w") do |f_out|
      1.upto(10) do |i|
        if i >= num
          f_out.puts "No. #{i}"
        else
          f_out.puts "#{i}行目"
        end
      end
    end
  end

  #### test methods ####
  def test_git1
    txt = "test.txt"

    # create
    my_system "git init #{@dir[:work]}"
    assert(test(?d, @dir[:work]))
    assert(test(?d, File.join(@dir[:work], ".git")))

    print_prompt
    FileUtils.cd(@dir[:work], :verbose => true) do
      # add file
      create_workfile(txt)
      my_system "cat #{txt}"
      result = my_exec("git status")
      assert(/^\#\s*#{txt}/ =~ result)

      my_system "git add #{txt}"
      result = my_exec("git status")
      assert(/^\#\s*new file:\s*#{txt}/ =~ result)

      # first commit
      my_system "git commit -m \"first commit\""
      result = my_exec("git status")
      assert(result.index(txt) == nil)

      # modify file repeatedly
      commit3 = ""
      commit5 = ""
      1.upto(10) do |i|
        create_workfile(txt, i)
        my_system "cat #{txt}"
        if i == 1
          result = my_exec("git status")
          assert(result.index(txt) == nil)
          next
        end
        my_system "git commit -a -m \"#{txt}: a bit changed\""
        if i == 3
          result = my_exec("git log -1 | head -n 1")
          commit3 = result.strip.sub(/^commit /, "")
        elsif i == 5
          result = my_exec("git log -1 | head -n 1")
          commit5 = result.strip.sub(/^commit /, "")
        end
      end

      # diff
      result = my_exec("git diff #{commit3}..#{commit5}")
      assert(result.index("-No. 3") != nil)
      assert(result.index("+4行目") != nil)
      assert(result.index("+5行目") == nil)
      result2 = my_exec("git diff #{commit3[0, 7]}..#{commit5[0, 7]}")
      assert(result2 == result)

      print_prompt
    end
  end

  def test_git2
    txt = "test.txt"

    # create
    my_system "git init --bare #{@dir[:repo]}"
    assert(test(?d, @dir[:repo]))
    assert(test(?f, File.join(@dir[:repo], "config")))
    assert(!test(?d, File.join(@dir[:repo], ".git")))

    # clone
    my_system "git clone #{@url} #{@dir[:work]}"
    assert(test(?d, @dir[:work]))
    assert(test(?d, File.join(@dir[:work], ".git")))

    commit3 = ""
    commit5 = ""
    diff35 = ""
    print_prompt
    FileUtils.cd(@dir[:work], :verbose => true) do
      # add file
      create_workfile(txt)
      my_system "cat #{txt}"
      result = my_exec("git status")
      assert(/^\#\s*#{txt}/ =~ result)

      my_system "git add #{txt}"
      result = my_exec("git status")
      assert(/^\#\s*new file:\s*#{txt}/ =~ result)

      # first commit
      my_system "git commit -m \"first commit\""
      result = my_exec("git status")
      assert(result.index(txt) == nil)

      # modify file repeatedly
      1.upto(10) do |i|
        create_workfile(txt, i)
        my_system "cat #{txt}"
        if i == 1
          result = my_exec("git status")
          assert(result.index(txt) == nil)
          next
        end
        my_system "git commit -a -m \"#{txt}: a bit changed\""
        if i == 3
          result = my_exec("git log -1 | head -n 1")
          commit3 = result.strip.sub(/^commit /, "")
        elsif i == 5
          result = my_exec("git log -1 | head -n 1")
          commit5 = result.strip.sub(/^commit /, "")
        end
      end

      # push
      my_system("git push origin master")

      # diff
      diff35 = my_exec("git diff #{commit3}..#{commit5}")
      assert(diff35.index("-No. 3") != nil)
      assert(diff35.index("+4行目") != nil)
      assert(diff35.index("+5行目") == nil)
      result = my_exec("git diff #{commit3[0, 7]}..#{commit5[0, 7]}")
      assert(result == diff35)

      print_prompt
    end

    # clone one more
    my_system "git clone #{@url} #{@dir[:work2]}"
    assert(test(?d, @dir[:work2]))
    assert(test(?d, File.join(@dir[:work2], ".git")))
    print_prompt
    FileUtils.cd(@dir[:work2], :verbose => true) do
      result = my_exec("git diff #{commit3[0, 7]}..#{commit5[0, 7]}")
      assert(result == diff35)
      print_prompt
    end
  end
end

ruby svn-test.rbそれからruby git-test.rbで動きます.前者は1本の,後者は2本のテストをします.「1本のテスト」というのは,リポジトリを作って,作業コピー(作業ツリー)を作って,その中にtest.txtを作ってコミットして,その中身を何回書き換えてその都度コミットして,gitの2本目のテストではpushをして,コミットした中で特定の2つの時点についてその差分を求める,という流れです.どのテストでも,リポジトリと作業コピー/ツリーはカレントディレクトリの直下に作られ,テスト終了時には消されます*4.git-test.rbの2本のテストのうち,test_git1はローカルなリポジトリ(git init),test_git2は公開リポジトリ(git init --bare)*5に対するテストです.
ディレクトリ削除の方法として,FileUtils.#rm_rではセキュリティ上の問題があるということで*6,FileUtils.#remove_entry_secureを試してみると,このメソッドには:verbose => trueをつけられないのに気づいて少々げんなりです.まあコードを残しています.
以下の値を変更すれば,動作が少し変わるようになっています.

  • setupの中の@option_fullpathをfalseからtrueに変更すれば,コマンドはフルパスで実行します.フルパスは,直後の@command_svn, @command_svnadmin, @command_gitで任意に変更可能です.
  • setupの中の@option_print_command_onlyをfalseからtrueに変更すれば,実行するコマンドだけを出力する(コマンドの実行結果は表示しない)ようにします.
  • setupの中で,@urlの値を変更すれば,リポジトリURLも変わります.SSHでのアクセスも可能に…なればいいのですが,通信のたびにパスワードを要求されないよう,事前に認証の設定をしておく必要があるほか,各テストで,リポジトリ初期化のコードを実行しないよう,コードに手を加えないといけませんね.
  • rm_rの中のローカル変数methodは,どの方法でディレクトリを削除するかを決めます.svn-test.rbとgit-test.rbでデフォルトの値が違うのは,git-test.rbをCygwinで実行したときに苦労させられたからです.

動作はCygwinLinuxVine, Ubuntu)のコマンドラインで確認しました.Cygwinではしばしば,git-work, git-work2のディレクトリ削除に失敗します.またCygwinでは,毎回のコマンド実行で時間がかかり,面白みがないのですが,Linuxで実行し比べると,gitのテストのほうが高速に処理できていることが見てとれまして,ほんの小さなテストながらも,毎回リポジトリに送らないことの時間的なメリットを体感できます.

*1:コマンドが使えるのは,確認しています.バージョン管理システムを使わない場合に,どのように減点するかについては,まだ検討中です.

*2:library test/unit

*3:gritで,各メソッドに何かオプションを与えれば,gitでのコマンドが出力される,というのなら,積極的に使用したいのですが.

*4:rubyに-dオプションをつけて実行すると,$DEBUGが真になり,リポジトリと作業コピー/ツリーが消されないようになります.ただし,git-test.rbの1本目のテストで作ったディレクトリは,2本目のテストの開始時に削除されます.1本目のテストを実施し,ディレクトリを残したいというときは,ruby -d git-test.rb --name=test_git1を実行します.

*5:Subversionからの移行も想定しています.すなわち,リポジトリと作業ツリーを別ディレクトリで管理するということです.

*6:module FileUtils