2006/08/23

テスト完了後にSVNのコミットをするPhingTask

ポスト @ 3:32:51 , 修正 @ 2006/08/23 3:37:13 |     このエントリーを含むはてなブックマーク

どっかで見掛けたんだけど、phpのデプロイにsvnを使ってコミットにフックしてテストを流すというヤツ(mixiだっけかな)
僕の場合は逆で、ローカルでテスト完了後(エラー無し、すべてのテスト通過後)にsvnにコミットしてる。phingで自動化して。(サーバテストは別に行う)
phingにはPHPUnit2のRunnerもあるので、そいつを使ってテストを全て流す。

PHPUnit2のTestRunからsvnへのコミット(デプロイとは違うけど)は以下のようなTask

<?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->listener as $listener){
            $suites = $listener->getTestSuites();
        }
        
        $results = array();
        foreach($suites as $suite){
            $result = new PHPUnit2_Framework_TestResult();
            $result->addListener($listener);
            $suite->run($result);
            $results[] = $result;
        }
        
        foreach($results as $res){
            if($res->wasSuccessful() && $res->allCompletlyImplemented()){
                continue;
            }
            $this->testsNotComplete($res);
        }
        if($this->complete){
            $this->allCompletlyImplemented();
        }
    }

    public function allCompletlyImplemented(){
        $this->log("All Completlyment!!");
        $timer = new Timer();
        $timer->start();
        $this->log("[start] SVN Commit");
        foreach($this->commits as $commit){
            try{
                $commit->init();
                $commit->main();
            } catch(Exception $e){
                $this->log("[exception] " . $e->getMessage());
            }
        }
        $this->log("[end] SVN Commit");
        $timer->stop();
        $this->log("time: " . $timer->getElapsedTime());
    }

    public function testsNotComplete(PHPUnit2_Framework_TestResult $result){
        $this->complete = false;
        foreach($result->failures() as $failure){
            $this->log("[failure] " . $failure->exceptionMessage());
        }
    }

    public function setDeploy($boolean){
        $this->deploy = $boolean;
    }

    public function createListener(){
        $listener = new TestListenerTask($this->project);
        $this->listener[] = $listener;
        return $listener;
    }

    public function createCommit(){
        $commitSvn = new SvnCommitTask($this->project);
        $this->commits[] = $commitSvn;
        return $commitSvn;
    }

}

?>

テストの結果がwasSuccessfulであり、allCompletlyImplementedの場合にsvnでコミットをします。
それ以外はコミットさせない。手動でやっちゃうのは反則。

あと、ここでは、Listenerでやってますが、標準のじゃないです。PhingのPHPUnit2対応のListenerがあまりカッコよくない(というか日本語じゃない)ので以下のようなListenerを用意してます。

<?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->project = $project;
    }

    public function addFileSet(FileSet $fileset){
        $this->filesets[] = $fileset;
    }

    public function getTestSuites(){
        $suites = array();
        foreach($this->filesets as $fileset){
            $ds = $fileset->getDirectoryScanner($this->project);
            $ds->scan();
            
            $files = $ds->getIncludedFiles();
            foreach($files as $file){
                $path = $this->project->getBasedir()->getPath() . self::DIR_SEP;
                $path .= $ds->getBaseDir() . self::DIR_SEP  . $file;
                $this->project->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->getName() . "エラーが発生したよ。: Message[" . $e->getMessage() , "]" . PHP_EOL;
    }

    public function addFailure(PHPUnit2_Framework_Test $test, PHPUnit2_Framework_AssertionFailedError $e){
        echo $test->getName() . "テストに失敗したよ。: Message[" . $e->getMessage() , "]" . PHP_EOL;
    }

    public function addIncompleteTest(PHPUnit2_Framework_Test $test, Exception $e){
        echo $test->getName() . "テストが完了しなかったよ。: Message[" . $e->getMessage() , "]" . PHP_EOL;
    }

    public function startTestSuite(PHPUnit2_Framework_TestSuite $suite){
        echo PHP_EOL;
        echo "テストシート[" . $suite->getName() . "]を開始します。(" . $suite->countTestCases() . ")" . PHP_EOL;
        $this->suiteTimer = new Timer();
        $this->suiteTimer->start();
    }

    public function endTestSuite(PHPUnit2_Framework_TestSuite $suite){
        echo "テストシート[" . $suite->getName() . "]を終了します。" . PHP_EOL;
        $this->suiteTimer->stop();
        echo "このシートの経過時間: " . $this->suiteTimer->getElapsedTime();
        echo PHP_EOL;
        echo PHP_EOL;
    }

    public function startTest(PHPUnit2_Framework_Test $test){
        echo "テスト[" . $test->getName() . "]を実行します。(" . $test->countTestCases() .")" . PHP_EOL;
        $this->timer = new Timer();
        $this->timer->start();
    }

    public function endTest(PHPUnit2_Framework_Test $test){
        echo "テスト[" . $test->getName() . "]を終了します。" . PHP_EOL;
        $this->timer->stop();
        echo "このテストの経過時間: " . $this->timer->getElapsedTime();
        echo PHP_EOL;
    }

}

?>

ほとんどただのTaskにListenerのインタフェース付けただけだけど、日本語と1つのテスト単位と1つのシート単位に経過時間もみれるので、メソッド単位のテストなんかでは重たい部分が把握できます。

んで、Svnへはこんな感じでSvnBaseTaskを上書きて使ってます

<?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->project = $project;
    }

    public function main(){
        $this->setup('ci');
        echo $this->run(array("message" => "Phing Test Complete at " . date("Y/m/d H:i:s")));
    }

    public function setup($mode){
        $this->command = $mode;
        $this->options = array('fetchmode' => VERSIONCONTROL_SVN_FETCHMODE_ASSOC, 'svn_path' => $this->getSvnPath());
        $this->svnArgs = array('username' => $this->username, 'password' => $this->password);
    }

    public function run($swiches){
        $this->svnstack = PEAR_ErrorStack::singleton('VersionControl_SVN');
        $svn = VersionControl_SVN::factory($this->command, $this->options);
        $swiches = array_merge($this->svnArgs, $swiches);
        $args = array($this->getWorkingCopy());

       if ($output = $svn->run($args, $swiches)){
           return $output;
       } else {
           if (count($errs = $this->svnstack->getErrors())) {
               $err = current($errs);
               throw new BuildException("Failed to run the 'svn " . $this->mode . "' command: " . $err['message']);
           }
       }
    }

    public function setUserName($username){
        $this->username = $username;
    }

    public function setPassWord($password){
        $this->password = $password;
    }

    public function getUserName(){
        return $this->username;
    }

    public function getPassWord(){
        return $this->password;
    }
    
}

?>

build.xmlには以下な感じで書いておく(workingCopyは予めチェックアウトで)

<?xml version="1.0"?>
<project name="s2service.php5" default="test" basedir=".">

    <taskdef name="tad" classname="phing.task.TestAndDeployTask" />
    <taskdef name="listener" classname="phing.task.TestListenerTask" />
    <taskdef name="commit" classname="phing.task.SvnCommitTask" />

    <target name="test">
        <php expression="require_once('test/enviroment.inc.php')"/>
        <tad deploy="false">
            <listener>
                <fileset dir="test">
                    <include name="**/*Test.class.php" />
                </fileset>
            </listener>
            <commit svnPath="/usr/bin/svn"
                    repositoryUrl="svn://path/to/repos" workingCopy="src"
                    username="username" password="password">
            </commit>
        </tad>
    </target>
</project>

phingのデフォルトのターゲットがtestであるので、以下のように実行するとパパっといきます。
# build.xmlの書き方とかはs2container.php5/s2baseを読めば十分通用すると思います。

> phing
Buildfile: /workspace/service/soap/deploy/build.xml

s2service.php5 > 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

ディレクトリとしては以下を想像

 > ls
build.xml  phing  src  test  doc

S2PHPだと、pear packageタスクも用意してあるんで、ほんとphingって打つだけで全て流れてくれて便利。
後はあれ、サーバへのデプロイをどうにか自動化したい。
# そういうこと言うとリモートデバグとかに発展するので今日はここまで。


1 Trackback

Feast-Your-Fat-Away--Review.Blogspot.Com

ハタさんのブログ(復刻版) : テスト完了後にSVNのコミットをするPhingTask

From : Feast-Your-Fat-Away--Review.Blogspot.Com @ 2014-02-21 05:01:21

Track from Your Website

http://blog.xole.net/trackback/tb.php?id=486