<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="http://blog.xole.net/rss/style.css" type="text/css"?>
<rdf:RDF xmlns="http://purl.org/rss/1.0/"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:content="http://purl.org/rss/1.0/modules/content/"
         xmlns:dc="http://purl.org/dc/elements/1.1/"
         xml:lang="ja">
<channel rdf:about="http://blog.xole.net/rss/1.0.php?id=486">
<title>ハタさんのブログ(復刻版)</title>
<link>http://blog.xole.net/index.php</link>
<dc:date>2006-08-23T03:32:51+09:00</dc:date>
<description>
ハタさんのブログ(復刻版) - RSS (RDF Site Summary).
</description>
<items>
<rdf:Seq>
<rdf:li rdf:resource="http://blog.xole.net/article.php?id=486" />
</rdf:Seq>
</items>
</channel>
<item>
<title>テスト完了後にSVNのコミットをするPhingTask</title>
<link>http://blog.xole.net/article.php?id=486</link>
<dc:date>2006-08-23T03:32:51+09:00</dc:date>
<description>どっかで見掛けたんだけど、phpのデプロイにsvnを使ってコミットにフックしてテストを流すというヤツ(mixiだっけかな)
僕の場合は逆で、ローカルでテスト完了後(エラー無し、すべてのテスト通過後)にsvnにコミットしてる。phingで自...</description>
<content:encoded>
<![CDATA[
<p>どっかで見掛けたんだけど、phpのデプロイにsvnを使ってコミットにフックしてテストを流すというヤツ(mixiだっけかな)<br />
僕の場合は逆で、ローカルでテスト完了後(エラー無し、すべてのテスト通過後)にsvnにコミットしてる。phingで自動化して。(サーバテストは別に行う)<br />
phingにはPHPUnit2のRunnerもあるので、そいつを使ってテストを全て流す。</p>

<p>PHPUnit2のTestRunからsvnへのコミット(デプロイとは違うけど)は以下のようなTask</p>
<pre>
<code>&lt;?php
require_once 'PHPUnit2/Framework/TestListener.php';
require_once 'phing/Task.php';

/**
 * @author nowel
 */
class TestAndDeployTask extends Task {

    private $listener = array();
    private $commits = array();
    private $deploy = false;
    private $complete = true;
    
    public function init(){
    }

    public function main(){
        $suites = array();
        foreach($this-&gt;listener as $listener){
            $suites = $listener-&gt;getTestSuites();
        }
        
        $results = array();
        foreach($suites as $suite){
            $result = new PHPUnit2_Framework_TestResult();
            $result-&gt;addListener($listener);
            $suite-&gt;run($result);
            $results[] = $result;
        }
        
        foreach($results as $res){
            if($res-&gt;wasSuccessful() && $res-&gt;allCompletlyImplemented()){
                continue;
            }
            $this-&gt;testsNotComplete($res);
        }
        if($this-&gt;complete){
            $this-&gt;allCompletlyImplemented();
        }
    }

    public function allCompletlyImplemented(){
        $this-&gt;log("All Completlyment!!");
        $timer = new Timer();
        $timer-&gt;start();
        $this-&gt;log("[start] SVN Commit");
        foreach($this-&gt;commits as $commit){
            try{
                $commit-&gt;init();
                $commit-&gt;main();
            } catch(Exception $e){
                $this-&gt;log("[exception] " . $e-&gt;getMessage());
            }
        }
        $this-&gt;log("[end] SVN Commit");
        $timer-&gt;stop();
        $this-&gt;log("time: " . $timer-&gt;getElapsedTime());
    }

    public function testsNotComplete(PHPUnit2_Framework_TestResult $result){
        $this-&gt;complete = false;
        foreach($result-&gt;failures() as $failure){
            $this-&gt;log("[failure] " . $failure-&gt;exceptionMessage());
        }
    }

    public function setDeploy($boolean){
        $this-&gt;deploy = $boolean;
    }

    public function createListener(){
        $listener = new TestListenerTask($this-&gt;project);
        $this-&gt;listener[] = $listener;
        return $listener;
    }

    public function createCommit(){
        $commitSvn = new SvnCommitTask($this-&gt;project);
        $this-&gt;commits[] = $commitSvn;
        return $commitSvn;
    }

}

?&gt;</code>
</pre>

<p>テストの結果がwasSuccessfulであり、allCompletlyImplementedの場合にsvnでコミットをします。<br />
それ以外はコミットさせない。手動でやっちゃうのは反則。</p>

<p>あと、ここでは、Listenerでやってますが、標準のじゃないです。PhingのPHPUnit2対応のListenerがあまりカッコよくない(というか日本語じゃない)ので以下のようなListenerを用意してます。</p>

<pre>
<code>&lt;?php
require_once 'phing/types/FileSet.php';
require_once 'PHPUnit2/Framework/TestListener.php';

/**
 * @author nowel
 */
class TestListenerTask implements PHPUnit2_Framework_TestListener {

    const DIR_SEP = DIRECTORY_SEPARATOR;
    private $project = null;
    private $filesets = array();
    private $suiteTimer = null;
    private $timer = null;
    
    public function __construct(Project $project){
        $this-&gt;project = $project;
    }

    public function addFileSet(FileSet $fileset){
        $this-&gt;filesets[] = $fileset;
    }

    public function getTestSuites(){
        $suites = array();
        foreach($this-&gt;filesets as $fileset){
            $ds = $fileset-&gt;getDirectoryScanner($this-&gt;project);
            $ds-&gt;scan();
            
            $files = $ds-&gt;getIncludedFiles();
            foreach($files as $file){
                $path = $this-&gt;project-&gt;getBasedir()-&gt;getPath() . self::DIR_SEP;
                $path .= $ds-&gt;getBaseDir() . self::DIR_SEP  . $file;
                $this-&gt;project-&gt;log('[require_once]' . basename($path));
                require_once $path;

                $filePath = basename($path);
                $class = StringHelper::root($filePath, '.class.php');
                if($filePath == $class){
                    $class = StringHelper::root($filePath, '.php');
                }
                $suites[] = new PHPUnit2_Framework_TestSuite(new ReflectionClass($class));
            }
        }
        return $suites;
    }
    
    public function addError(PHPUnit2_Framework_Test $test, Exception $e){
        echo $test-&gt;getName() . "エラーが発生したよ。: Message[" . $e-&gt;getMessage() , "]" . PHP_EOL;
    }

    public function addFailure(PHPUnit2_Framework_Test $test, PHPUnit2_Framework_AssertionFailedError $e){
        echo $test-&gt;getName() . "テストに失敗したよ。: Message[" . $e-&gt;getMessage() , "]" . PHP_EOL;
    }

    public function addIncompleteTest(PHPUnit2_Framework_Test $test, Exception $e){
        echo $test-&gt;getName() . "テストが完了しなかったよ。: Message[" . $e-&gt;getMessage() , "]" . PHP_EOL;
    }

    public function startTestSuite(PHPUnit2_Framework_TestSuite $suite){
        echo PHP_EOL;
        echo "テストシート[" . $suite-&gt;getName() . "]を開始します。(" . $suite-&gt;countTestCases() . ")" . PHP_EOL;
        $this-&gt;suiteTimer = new Timer();
        $this-&gt;suiteTimer-&gt;start();
    }

    public function endTestSuite(PHPUnit2_Framework_TestSuite $suite){
        echo "テストシート[" . $suite-&gt;getName() . "]を終了します。" . PHP_EOL;
        $this-&gt;suiteTimer-&gt;stop();
        echo "このシートの経過時間: " . $this-&gt;suiteTimer-&gt;getElapsedTime();
        echo PHP_EOL;
        echo PHP_EOL;
    }

    public function startTest(PHPUnit2_Framework_Test $test){
        echo "テスト[" . $test-&gt;getName() . "]を実行します。(" . $test-&gt;countTestCases() .")" . PHP_EOL;
        $this-&gt;timer = new Timer();
        $this-&gt;timer-&gt;start();
    }

    public function endTest(PHPUnit2_Framework_Test $test){
        echo "テスト[" . $test-&gt;getName() . "]を終了します。" . PHP_EOL;
        $this-&gt;timer-&gt;stop();
        echo "このテストの経過時間: " . $this-&gt;timer-&gt;getElapsedTime();
        echo PHP_EOL;
    }

}

?&gt;</code>
</pre>

<p>ほとんどただのTaskにListenerのインタフェース付けただけだけど、日本語と1つのテスト単位と1つのシート単位に経過時間もみれるので、メソッド単位のテストなんかでは重たい部分が把握できます。</p>

<p>んで、Svnへはこんな感じでSvnBaseTaskを上書きて使ってます</p>

<pre>
<code>&lt;?php
require_once 'phing/tasks/ext/svn/SvnBaseTask.php';

/**
 * @author nowel
 */
class SvnCommitTask extends SvnBaseTask {

    const DIR_SEP = DIRECTORY_SEPARATOR;
    private $filesets = array();
    private $command = null;
    private $options = array();
    private $svnArgs = array();

    private $username;
    private $password;
    
    public function __construct(Project $project){
        $this-&gt;project = $project;
    }

    public function main(){
        $this-&gt;setup('ci');
        echo $this-&gt;run(array("message" =&gt; "Phing Test Complete at " . date("Y/m/d H:i:s")));
    }

    public function setup($mode){
        $this-&gt;command = $mode;
        $this-&gt;options = array('fetchmode' =&gt; VERSIONCONTROL_SVN_FETCHMODE_ASSOC, 'svn_path' =&gt; $this-&gt;getSvnPath());
        $this-&gt;svnArgs = array('username' =&gt; $this-&gt;username, 'password' =&gt; $this-&gt;password);
    }

    public function run($swiches){
        $this-&gt;svnstack = PEAR_ErrorStack::singleton('VersionControl_SVN');
        $svn = VersionControl_SVN::factory($this-&gt;command, $this-&gt;options);
        $swiches = array_merge($this-&gt;svnArgs, $swiches);
        $args = array($this-&gt;getWorkingCopy());

       if ($output = $svn-&gt;run($args, $swiches)){
           return $output;
       } else {
           if (count($errs = $this-&gt;svnstack-&gt;getErrors())) {
               $err = current($errs);
               throw new BuildException("Failed to run the 'svn " . $this-&gt;mode . "' command: " . $err['message']);
           }
       }
    }

    public function setUserName($username){
        $this-&gt;username = $username;
    }

    public function setPassWord($password){
        $this-&gt;password = $password;
    }

    public function getUserName(){
        return $this-&gt;username;
    }

    public function getPassWord(){
        return $this-&gt;password;
    }
    
}

?&gt;</code>
</pre>

<p>build.xmlには以下な感じで書いておく(workingCopyは予めチェックアウトで)</p>

<pre>
<code>&lt;?xml version="1.0"?&gt;
&lt;project name="s2service.php5" default="test" basedir="."&gt;

    &lt;taskdef name="tad" classname="phing.task.TestAndDeployTask" /&gt;
    &lt;taskdef name="listener" classname="phing.task.TestListenerTask" /&gt;
    &lt;taskdef name="commit" classname="phing.task.SvnCommitTask" /&gt;

    &lt;target name="test"&gt;
        &lt;php expression="require_once('test/enviroment.inc.php')"/&gt;
        &lt;tad deploy="false"&gt;
            &lt;listener&gt;
                &lt;fileset dir="test"&gt;
                    &lt;include name="**/*Test.class.php" /&gt;
                &lt;/fileset&gt;
            &lt;/listener&gt;
            &lt;commit svnPath="/usr/bin/svn"
                    repositoryUrl="svn://path/to/repos" workingCopy="src"
                    username="username" password="password"&gt;
            &lt;/commit&gt;
        &lt;/tad&gt;
    &lt;/target&gt;
&lt;/project&gt;</code>
</pre>

<p>phingのデフォルトのターゲットがtestであるので、以下のように実行するとパパっといきます。<br />
# build.xmlの書き方とかはs2container.php5/s2baseを読めば十分通用すると思います。</p>

<pre>
<code>&gt; phing
Buildfile: /workspace/service/soap/deploy/build.xml

s2service.php5 &gt; test:
      [php] Evaluating PHP expression: require_once('test/enviroment.inc.php')
[require_once]HogeTest.class.php

テストシート[HogeTest]を開始します。(2)
テスト[testHoge1]を実行します。(1)
テスト[testHoge1]を終了します。
このテストの経過時間: 0.00046
テスト[testHoge2]を実行します。(1)
テスト[testHoge2]を終了します。
このテストの経過時間: 0.00013
テストシート[HogeTest]を終了します。
このシートの経過時間: 0.00144

      [tad] All Completlyment!!
      [tad] [start] SVN Commit
[SvnCommit] Commit files :
送信しています              src/org/seasar/aaa.php
ファイルのデータを送信中です.
リビジョン 23 をコミットしました。      [tad] [end] SVN Commit
      [tad] time: 2.84013

BUILD FINISHED

Total time: 3.1066 seconds</code>
</pre>

<p>ディレクトリとしては以下を想像</p>
<pre> &gt; ls
build.xml  phing  src  test  doc
</pre>

<p>S2PHPだと、pear packageタスクも用意してあるんで、ほんとphingって打つだけで全て流れてくれて便利。<br />
後はあれ、サーバへのデプロイをどうにか自動化したい。<br />
# そういうこと言うとリモートデバグとかに発展するので今日はここまで。</p>
]]>
</content:encoded>
</item>

</rdf:RDF>