3
3

More than 5 years have passed since last update.

runkit で time() 関数をアレするやつ

Posted at

PHP の暗黒面に堕ちました。

.

.

.

time() 関数をベタ書きしているコードをテストするために sleep() して time() 関数の戻り値を無理やり制御している箇所があり、無駄にテストの実行時間が増えていたのでこれをどうにかするために CI 環境に runkit をインストールしました。誰だこんなコード書いたのは、俺か。

runkit のインストール

PHP は CentOS 6 の PHP-5.3.3 なのですが、epel から yum install php-pecl-runkit で入れるとセグりまくりました。

pecl install runkit-0.9 で入れようとすると今度はビルドが通りませんでした。

GitHub からソースを持ってきたら、ビルドも通るしセグりもしませんでした。

$ git clone https://github.com/zenovich/runkit.git
$ cd runkit
$ git checkout 1.0.2
$ phpize
$ ./configure
$ make
$ sudo make install
$ cat <<EOS | sudo tee /etc/php.d/runkit.ini
extension = runkit.so
runkit.internal_override = On
EOS

runkit_function_rename() が機能しない?

runkit_function_rename()time() をリネームした先の関数を呼ぶと謎な Fatal error になりました。

<?php
runkit_function_rename('time', 'runkit_orig_time');
var_dump(runkit_orig_time());
$ php z.php
PHP Fatal error:  Non-static method (null)::[]1\() cannot be called statically in /home/ore/x.php on line 3

runkit_function_rename() 以外は普通に動いているので runkit_function_rename() を使わずにどうにかすることにします。

都合の良いことにプロダクションコードで gettimeofday() は一切使っていなかったので (int)gettimeofday(true) でオリジナルの time() の代わりになるでしょう。

テスト対象のコード

次のコードをテストします。

<?php
class Sample
{
    public function func($time)
    {
        return $time - time();
    }
}

Sample::func($time) を呼ぶと現在日時を引き算して返します。

テストコードその1

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        runkit_function_remove('time');
        function time() {
            return 100;
        }

        $obj = new Sample();
        $this->assertEquals(23, $obj->func(123));
    }
}

これは良くありません。このテストを実行した後も time() 関数が固定値を返し続けてしまうので他のテストに影響があるかもしれません。

また PHPUnit もわけのわからない値を実行時間として報告してきます。

$ vendor/bin/phpunit SampleTest.php 
PHPUnit 3.6.12 by Sebastian Bergmann.

.

Time: -1410525728 seconds, Memory: 5.00Mb

OK (1 test, 1 assertion)

テストコードその2

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        runkit_function_remove('time');
        function time() {
            return 100;
        }

        try {
            $obj = new Sample();
            $this->assertEquals(23, $obj->func(123));

            runkit_function_remove('time');
            function time() {
                return (int)gettimeofday(true);
            }

        } catch (Exception $ex) {
            runkit_function_remove('time');
            function time() {
                return (int)gettimeofday(true);
            }
            throw $ex;
        }
    }
}

テストの実行後に time() 関数を元の動作に戻してやるべきでしょう。

$ vendor/bin/phpunit SampleTest.php 
PHPUnit 3.6.12 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.00Mb

OK (1 test, 1 assertion)

もうちょっとやりやすくする

さすがにこれはしんどいので、もうちょっとやりやすくするために次のようなクラスを作りました。

<?php
class DummyTimer
{
    public static $timeStack = array();

    public static function initRunKit()
    {
        static $first = true;

        if ($first) {
            $first = false;

            if (function_exists('runkit_function_redefine') == false) {
                throw new LogicException("Should be load runkit extension");
            }

            runkit_function_redefine('time', '', 'return ' . __CLASS__ . '::time();');
        }
    }

    public function __construct()
    {
        self::initRunKit();
    }

    public function __destruct()
    {
        $this->restore();
    }

    public static function time()
    {
        if (count(self::$timeStack)) {
            return end(self::$timeStack);
        } else {
            return (int)gettimeofday(true);
        }
    }

    public function set($time)
    {
        unset(self::$timeStack[spl_object_hash($this)]);
        self::$timeStack[spl_object_hash($this)] = $time;
    }

    public function restore()
    {
        unset(self::$timeStack[spl_object_hash($this)]);
    }
}

次のようにテストを書くことが出来ます。

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        $timer = new DummyTimer();
        $timer->set(100);

        $obj = new Sample();
        $this->assertEquals(23, $obj->func(123));
    }
}

DummyTimer::set($time)time() 関数の戻り値を指定します。

DummyTimer がスコープから消えるとデストラクタで time() 関数の戻り値が元に戻ります。

が、幾つか注意する点があります。

テストケースのプロパティに DummyTimer を持たせてはならない

あまりやる意味は無いですが、次のように PHPUnit_Framework_TestCase のプロパティに DummyTimer のインスタンスを持たしてはいけません。

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        $this->timer = new DummyTimer();
        $this->timer->set(100);

        $obj = new Sample();
        $this->assertEquals(23, $obj->func(123));
    }
}

PHPUnit_Framework_TestCase に追加したプロパティは、テストがすべて完了するまで破棄されません。

なので、プロパティに DummyTimer のインスタンスをもたせると PHPUnit の終了までデストラクタが呼ばれません。

DummyTimer をメソッドの引数に渡してはならない

あまりやる意味は無いですが、次のように DummyTimer のインスタンスをメソッドの引数に渡さないほうが良いです。

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        $timer = new DummyTimer();
        $this->func($timer);
    }

    private function func(DummyTimer $timer)
    {
        $timer->set(100);
        $obj = new Sample();
        $this->assertEquals(12, $obj->func(123));
    }
}

メソッドが例外を投げた場合、例外のスタックトレースに DummyTimer のインスタンスが保持されてしまいます。

そしてテストメソッドを突き抜けた例外は PHPUnit によって保持されるので、意図したとおりデストラクタが呼ばれなくなります。

DummyTimer のあるスコープで PHP エラーを発生させてはならない

次のように DummyTimer のインスタンスが存在するスコープで PHP エラーを発生させない方が良いです。

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        $timer = new DummyTimer();
        $timer->set(100);
        $obj = new Sample();
        $this->assertEquals(23, $obj->func(123));

        // PHP Notice
        $xxx = $vvv;
    }
}

PHPUnit は set_error_handler() で PHP エラーを例外に変換しますが、set_error_handler() の第5引数にエラーが発生した箇所のスコープのすべての変数が含まれています。なので、PHPUnit が飛ばす例外のスタックトレースに DummyTimer のインスタンスが保持されてデストラクタが呼ばれなくなります。

さいごに

いろいろ注意点もありますが、例外がテストメソッドを突き抜けるのはテストがコケたときがほとんどなので、あまり気にしてはいません。

あるいは、どうせテストでしか使わないので tearDown() で元に戻してやるだけの方が良かったかもしれません。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3