Gitコミット時にphpcsチェックを実行する を作ってみた。

■git commit時にphpcsを実行

・変更した行にphpcsエラーがある場合、phpcsエラーを出力し、コミットをキャンセルします
・phpcsエラー箇所が既にコミット済みの場合はスルーします
・「PhpcsCheckIgnore.txt」に記載されたphpファイルは、phpcsチェックを行いません

■インストール

1)[プロジェクト]/.git/hooks配下に下記ファイルを格納
    ・commit-msg
    ・PhpcsCheck.php
    ・PhpcsCheckIgnore.txt
2)commit-msgに実行権限を付与
    chmod +x commit-msg
  • commit-msg
#!/bin/sh

# ■git commit時にphpcsを実行
# ・変更した行にphpcsエラーがある場合、phpcsエラーを出力し、コミットをキャンセルします
# ・phpcsエラー箇所が既にコミット済みの場合はスルーします
# ・「PhpcsCheckIgnore.txt」に記載されたphpファイルは、phpcsチェックを行いません
#
# ■インストール
# 1)[プロジェクト]/.git/hooks配下に下記ファイルを格納
#     ・commit-msg
#     ・PhpcsCheck.php
#     ・PhpcsCheckIgnore.txt
# 2)commit-msgに実行権限を付与
#     chmod +x commit-msg

# 親ディレクトリ取得
# ※/XXX/XXX/XXX/.git/hookを/XXX/XXX/XXX/に変更
currentDir=$(cd $(dirname $0); pwd)
parentDir=${currentDir/.*/}

# 変更ファイル一覧を取得
isError=false
for file in $(git diff --cached --name-only)
do
    # phpcsでチェック実行
    echo "${file}"
    result=$(php ${currentDir}/PhpcsCheck.php ${parentDir} ${file} 2>/dev/null)

    # phpcsでエラーがある場合はフラグを立てる
    if [ $? -ne 0 ]; then
        isError=true
    fi

    # phpcs結果を出力
    if [ -n "$result" ]; then
        echo "${result}"
    fi
    
done

# phpcsでエラーがある場合は異常終了
if  $isError ; then
    echo "[PHPCS ERROR] has not passed"
    exit 1
fi

# 正常終了
echo "[PHPCS SUCCESS] passed all"
exit 0
  • PhpcsCheck.php
<?php

/**
 * 変更行のphpcsエラー情報を出力します。
 * 正常終了時は「0」、変更行のphpcsエラーがある場合および異常終了時は「1」を返却します。
 * 
 * PHP Version >= 5.2
 *
 * @category Tool
 * @package  Tool
 * @author   Your Name <yourname@example.com>
 * @license  MIT License
 * @link     https://yoursite.com
 */

// 引数取得
$dir = $argv[1];
$file = $argv[2];

// デバッグログファイル設定
$debugLogFile = "${dir}.git/hooks/PhpcsCheck.log";

// 引数チェック
if (checkParameter($dir, $file) === false) {

    // 異常終了
    exit(1);
}

// 除外対象ファイルチェック
if (checkIgnore($dir, $file) === false) {

    // 正常終了
    exit(0);
}

// phpcsのエラー行番号リスト取得
$phpcsErrorList = getPhpcsErrorList($dir, $file);

// 変更箇所の行番号リスト取得
$diffList = getDiffList($dir, $file);

// 変更箇所のphpcsエラー箇所を標準出力
$phpcsErrorInfo = outputPhpcsError($file, $phpcsErrorList, $diffList);

// 変更行のphpcsエラーがある場合は
if ($phpcsErrorInfo !== "") {

    // 変更行のphpcsエラーを標準出力
    echo $phpcsErrorInfo;

    // 異常終了
    exit(1);
}

// 正常終了
exit(0);

/**
 * 引数チェック
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return boolean true:正常、false:異常
 */
function checkParameter($dir, $file)
{
    $ret = true;

    // ファイルパスの存在チェック
    if (file_exists("${dir}${file}") === false) {
        echo "${dir}${file} は存在しないファイルパスです\n";
        $ret = false;
    }

    // デバッグログ出力
    debugLog("[checkParameter] dir=${dir} file=${file} ${ret}");

    return $ret;
}

/**
 * 除外対象チェック
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return boolean true:除外対象でない、false:除外対象である
 */
function checkIgnore($dir, $file)
{
    $ret = true;

    // 除外対象ファイルをオープン
    $lines = file("${dir}.git/hooks/PhpcsCheckIgnore.txt");
    foreach ($lines as $line) {
 
        // コメント行はスキップ
        if (substr($line, 0, 1) === "#") {
            continue;
        }

        // 除外対象ファイルの場合
        if (trim($line) === $file) {
            echo "${file} は除外対象のため、phpcsチェックを行いません";
            $ret = false;
            break;
        }
    }

    // デバッグログ出力
    debugLog("[checkIgnore] file=${file} ${ret}");

    return $ret;
}

/**
 * Phpcsエラーリスト取得
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return array phpcsエラー情報を格納したリスト
 */
function getPhpcsErrorList($dir, $file)
{
    $result = array();

    // phpcs実行
    $command = "phpcs ${dir}${file} 2>/dev/null";
    $outputs = execCommand($command);

    // 全てのphpcsエラーについて
    foreach ($outputs as $output) {

        // 行番号が記載された行であれば、リストに追加する
        if (isLineNumber($output) === true) {
            $itemList = explode("|", $output);
            $result[trim($itemList[0])] = $output;
        }
    }

    // デバッグログ出力
    debugLog("[getPhpcsErrorList] ".implode("\n", $result));

    return $result;
}

/**
 * 変更箇所の行番号リスト取得
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return array 変更箇所の行番号を格納したリスト
 */
function getDiffList($dir, $file)
{
    $result = array();

    // 変更箇所リストを取得
    $command .= "git --no-pager diff --cached --no-ext-diff ";
    $command .= "-U1000000 ${file} 2>/dev/null";
    $outputs = execCommand($command);

    // 全ての変更箇所について
    $lineNumber = -1;
    foreach ($outputs as $output) {

        // 「<?php」で行番号カウント開始
        if (trim($output) === "<?php") {
            $lineNumber = 0;
        }

        // 行番号カウント
        if ($lineNumber >= 0) {

            // 「-」で始まる場合、インクリメントしない
            if (substr($output, 0, 1) === "-") {
                continue;
            }

            // 行番号を更新
            $lineNumber += 1;

            // 「+」で始まる場合、リストに格納
            if (substr($output, 0, 1) === "+") {
                $result[$lineNumber] = $lineNumber;
            }
        }
    }

    // デバッグログ出力
    debugLog("[getDiffList] ".implode(",", $result));

    return $result;
}

/**
 * 変更箇所のphpcsエラー箇所を標準出力
 * 
 * @param string $file           対象ファイル
 * @param array  $phpcsErrorList phpcsエラー情報を格納したリスト
 * @param array  $diffList       変更箇所の行番号を格納したリスト
 * 
 * @return string $phpcsErrorInfo phpcsエラー情報
 */
function outputPhpcsError($file, $phpcsErrorList, $diffList)
{    
    $result = array();
    $phpcsErrorInfo = "";

    // 変更行のphpcsエラーを抽出し、リストに格納
    foreach ($diffList as $diff) {
        if (array_key_exists($diff, $phpcsErrorList)) {
            $result[] = $phpcsErrorList[$diff];
        }
    }

    // phpcsエラーリストから、phpcsエラー情報を生成
    if (empty($result) === false) {
        foreach ($result as $item) {
            $phpcsErrorInfo .= $item."\n";
        }
    }

    // デバッグログ出力
    debugLog("[outputPhpcsError] ${phpcsErrorInfo}");

    // phpcsエラー情報を返却
    return $phpcsErrorInfo;
}

/**
 * コマンド実行
 * 
 * @param string $command コマンド文字列
 * 
 * @return string コマンド実行結果
 */
function execCommand($command)
{
    $outputs = "";
    $status = "";

    // コマンド実行
    exec($command, $outputs, $status);

    // デバッグログ出力
    debugLog("[execCommand] ${command} ${status}");

    return $outputs;
}

/**
 * デバッグログ出力
 * 
 * @param string $output 出力文字列
 * 
 * @return void
 */
function debugLog($output)
{
    $date = date("Y/m/d H:i:s");
    global $debugLogFile;
    $ret = error_log("${date} ${output}\n", 3, $debugLogFile);
}

/**
 * 行番号記載有無の判定
 * 
 * @param string $target 判定対象文字列
 * 
 * @return boolean true:行番号の記載がある、false:行番号の記載がない
 */
function isLineNumber($target)
{
    // 空行はスキップ
    if ($target === "") {
        return false;
    }
    
    // コメント行はスキップ
    if (substr($target, 0, 1) === "-") {
        return false;
    }

    // 「FOUND ~」行、「Time: ~」行、「FILE: ~」行はスキップ
    $skipList = array("/^FOUND /", "/^Time: /", "/^FILE: /");
    foreach ($skipList as $skip) {
        if (preg_match($skip, $target) === true) {
            return false;
        }
    }

    // 行番号の記載がある
    return true;
}
  • PhpcsCheckIgnore.txt
# 除外対象ファイルを記載します(phpcsチェックを行いません)
# 例)test/app/action/Main/CRUD.php

PHPのDBユニットテスト

PhpUnitDBUnit拡張が追加できない特殊な環境向け(MySQL限定)のDBユニットテストもどきを作ってみた。

  • DbTestUtil.php
<?php

/**
 * DBを用いたユニットテストのヘルパークラス
 * 
 * ※PhpUnitのDBUnit拡張が追加できない特殊な環境向け(MySQL限定)
 * ※DBダンプデータは下記コマンドで取得する
 * mysqldump --xml -t -u [ユーザ名] --password=[パスワード] 
 *   --port [ポート番号] [データベース名] [テーブル名] > [インポートするファイル名]
 */
class DbTestUtil
{
    /** 
     * 接続情報
     * 
     * @var string $_dsn
     */
    const DSN = "mysql:host=192.168.XX.XX;port=3306;dbname=XXXX;charset=utf8";
    /** 
     * ユーザ名 
     * 
     * @var string $_username
     */
    const USERNAME = "XXX";
    /** 
     * パスワード 
     * 
     * @var string $_password
     */
    const PASSWORD = "XXX";
    
    /** 
     * PDOクラス
     * 
     * @var PDO $_pdo
     */
    private $_pdo = null;
    /** 
     * テスト対象テーブル名
     * 
     * @var string $_original_table
     */
    private $_original_table = null;
    /** 
     * テスト対象テーブルのバックアップ名
     * 
     * @var string $_backup_table
     */
    private $_backup_table = null;
    /** 
     * プライマリーキー情報を格納したリスト
     * 
     * @var string $_primaryKeyList 
     */
    private $_primaryKeyList = null;

    /**
     * コンストラクタ
     * 
     * @param string $original_table テスト対象テーブル名
     */
    public function __construct($original_table)
    {
        // プロパティ値保存
        $this->_original_table = $original_table;
        $this->_backup_table = "{$original_table}_backup";
    }

    /**
     * データベースをテストデータで初期化する
     * 
     * @param string $importXmlPath 初期データを格納したXML
     * 
     * @return void
     */
    public function init($importXmlPath)
    {        
        // DB接続
        if ($this->_pdo === null) {
            $option = array(PDO::MYSQL_ATTR_LOCAL_INFILE => true);
            $this->_pdo = new PDO(
                self::DSN, 
                self::USERNAME, 
                self::PASSWORD, 
                $option
            );
        }

        // テーブルバックアップ
        $this->_exec("DROP TABLE {$this->_backup_table}", false); // 念のため、削除
        $this->_exec(
            "CREATE TABLE {$this->_backup_table} LIKE {$this->_original_table}"
        );
        $this->_exec(
            "INSERT INTO {$this->_backup_table} 
                SELECT * FROM {$this->_original_table}"
        );

        // プライマリーキー情報を取得
        $this->_primaryKeyList = $this->_getPrimaryKeyList();

        // 初期データ投入
        $this->_loadXml($this->_original_table, $importXmlPath);
    }

    /**
     * データベースを元の状態に戻す
     * 
     * @return void
     */
    public function revert()
    {
        // テーブルを元に戻す
        $this->_exec("DROP TABLE {$this->_original_table}");
        $this->_exec(
            "ALTER TABLE {$this->_backup_table} RENAME TO {$this->_original_table}"
        );
    }

    /**
     * 実行後データ取得
     * 
     * @return array 実行後データを格納したリスト
     */
    public function getAfter()
    {
        // 実行後データを返却
        return $this->_selectAll($this->_original_table);
    }

    /**
     * 期待値データ取得
     * 
     * @param string $expectedXmlPath 期待値データを格納したXMLファイルパス
     * 
     * @return array 期待値データを格納したリスト
     */
    public function getExpected($expectedXmlPath)
    {
        // 比較データ投入
        $this->_loadXml($this->_original_table, $expectedXmlPath);

        // 比較データ取得
        return $this->_selectAll($this->_original_table);
    }

    /**
     * XMLデータを読み込み、DBテーブルに反映する
     * 
     * @param string $tableName   対象テーブル名
     * @param string $xmlFilePath XMLファイルパス
     * 
     * @return void
     */
    private function _loadXml($tableName, $xmlFilePath)
    {
        // XMLファイル存在チェック
        if (file_exists($xmlFilePath) === false) {
            throw new Exception("XMLファイルが存在しません: {$xmlFilePath}");
        }

        // XMLデータ投入
        $this->_exec("TRUNCATE TABLE {$tableName}");
        $this->_exec(
            "LOAD XML LOCAL INFILE '{$xmlFilePath}' INTO TABLE {$tableName}"
        );
    }

    /**
     * SQL実行 
     * 
     * @param string $sql     実行SQL
     * @param bool   $isCheck true:実行結果をチェックする、false:実行結果をチェックしない
     * 
     * @return void
     */
    private function _exec($sql, $isCheck = true)
    {
        $ret = (int)$this->_pdo->exec($sql);
        if ($isCheck === true && $ret === false) {
            throw new Exception("SQL実行に失敗しました: {$sql}");
        }
    }

    /**
     * SELECT実行
     * 
     * @param string $sql 実行SQL
     * 
     * @return array SQL実行結果を格納したリスト
     */
    private function _query($sql)
    {
        $stmt = $this->_pdo->query($sql);
        $ret = $stmt->fetchAll();

        if ($ret === false) {
            throw new Exception("SQL実行に失敗しました: {$sql}");
        }

        return $ret;
    }

    /**
     * プライマリーキー情報の取得
     * 
     * @return array プライマリーキー情報を格納したリスト
     */
    private function _getPrimaryKeyList()
    {
        preg_match('/dbname=(\w+)/', self::DSN, $match);
        $schema = $match[1];

        $sql = "
            SELECT
                column_name
            FROM
                information_schema.columns
            WHERE
                table_schema = '{$schema}'
                AND table_name   = '{$this->_original_table}'
                AND column_key   = 'PRI'
            ORDER BY
                ordinal_position
        ";
        return $this->_query($sql);
    }

    /**
     * 全レコード情報の取得
     * 
     * @param string $tabelName 対象テーブル名
     * 
     * @return array 全レコード情報を格納したリスト
     */
    private function _selectAll($tabelName)
    {
        $order = "";
        foreach ($this->_primaryKeyList as $primaryKey) {
            if ($order === "") {
                $order = $primaryKey[0];
            } else {
                $order = $order.", ".$primaryKey[0];
            }
        }
         
        $sql = "
            SELECT 
                *
            FROM 
                {$tabelName}
            ORDER BY
                {$order} ASC
        ";
        return $this->_query($sql);
    }
}
  • テストの実装例
<?php
require_once(dirname(__FILE__)."/DbTestUtil.php");

class DbTestSampleForPhpUnitOnly extends PHPUnit_Framework_TestCase
{
    /** DBを用いたユニットテストのヘルパークラス */
    private $_dbTestUtil = null;

    // 各テストメソッドの実行前に呼ばれる初期化処理
    public function setUp()
    {
        // テストデータの初期化
        $this->_dbTestUtil = new DbTestUtil("test");
        $this->_dbTestUtil->init(dirname(__FILE__)."/init/init.xml");
    }

    // 各テストメソッドの実行後に呼ばれる終了処理
    public function tearDown()
    {
        // テーブルを元に戻す
        $this->_dbTestUtil->revert();
    }

    // テストメソッド
    public function testSample()
    {
        // DB接続
        $dsn = "mysql:host=192.168.XX.XX;port=3306;dbname=XXXX;charset=utf8";
        $username = "XXXX";
        $password = "XXXX";
        $pdo = new PDO($dsn, $username, $password);
        
        // 参照系のテスト
        $stmt = $pdo->query("SELECT * FROM test ORDER BY id ASC");
        $list = $stmt->fetchAll();
        $answer = [
            ['id' => '1', 0 => '1', 'name' => 'john', 1 => 'john'],
            ['id' => '2', 0 => '2', 'name' => 'tom', 1 => 'tom'],
            ['id' => '3', 0 => '3', 'name' => 'apache', 1 => 'apache']
        ];
        $this->assertEquals($list, $answer);

        // 更新系のテスト
        $id = 4;
        $name = 'php';
        $stmt = $pdo->prepare("INSERT INTO test (id, name) VALUES (:id, :name)");
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);
        $stmt->bindParam(':name', $name, PDO::PARAM_STR);
        $stmt->execute();
        // 実行結果比較
        $after = $this->_dbTestUtil->getAfter();
        $expected = $this->_dbTestUtil->getExpected(dirname(__FILE__)."/expected/expected.xml");
        $this->assertEquals($after, $expected);
    }
}

Node.jsでユニットテスト

Node.jsでユニットテストしてみます。
まずはNode.jsのインストールから。


■Node.jsのインストール(NVM使用)

$wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.18.0/install.sh | bash

環境変数を詠み込むため、Terminalを閉じてもう一度開く

$nvm ls-remote
$nvm install vX.X.X ※使用するバージョンを指定
$node -v


ユニットテストライブラリのインストール

$sudo apt-get install npm
$sudo npm install mocha
$sudo npm install power-assert
$sudo npm install nock
$sudo ln -s "$(which nodejs)" /usr/local/bin/node


■helloWorld.js

exports.greet = function() { 
	return "Hello World!!"; 
}


■helloWorldTest.js

var assert = require('assert');
var module = require('./helloWorld');
 
describe('greet', function () {
	it('戻り値が正しいこと', function () {
		assert.equal(module.greet(), 'Hello World!!');
	});
});


■http.js

var https = require('https');
var url = "https://www.google.co.jp";

exports.getData = function (done) {
	
	https.get(url, function (res) {
		
		var response = '';
		
		res.on('data', function (chunk) {
			response += chunk;
		});
		
		res.on('end', function () {
			console.log(response);
			done(null, response);
		});
	
	}).on('error', function (e) {
		console.log(e.message);
		done(e);
	});
}


■httpTest.js

var nock = require('nock');
var assert = require('power-assert');
var module = require('./http.js');

var url = 'https://www.google.co.jp';
var path = '/';

describe('HTTPレスポンスのMockテスト', function () {
	
	it('getDataのテスト', function (done) {
		
		// HTTPレスポンスのMock作成
		nock(url).get(path).reply(200, 'GoogleのHTML'); ※Mockで指定した値が返却される(Webサイトにアクセスしない)
		
		// テスト実行
		module.getData(function (err, response) {
			assert(response == 'GoogleのHTML');
			done();
		})
	});
});


ユニットテスト実行

$mocha helloWorldTest.js
$mocha httpTest.js

Dropboxの共有リンク

だいぶ前からDropboxのPublicフォルダが廃止になったので、画像リンク切れのまま放置していましたが・・・
共有リンクに張り替えました。


1)画像を右クリック→「Dropboxリンクをコピー」で共有リンクを作成
2)共有リンクURLを直リンクに修正

https://www.dropbox.com/s/dyb1yc7va32lt9f/head_logo_org.jpg?dl=0

https://dl.dropboxusercontent.com/s/dyb1yc7va32lt9f/head_logo_org.jpg


※末尾の「?dl=0」を削除
※「www.dropbox.com」を「dl.dropboxusercontent.com」に変更


後はひたすら記事上の画像リンクURLを共有リンクURLに修正。
数が多くて大変だった・・・

自宅開発環境のアップデート

自宅開発環境アップデートです。
今年も忘れずに、こまめにアップデートします。自分への覚書を残しておきます。


Hyper-V 引っ越し
Windows10からローカル環境でHyper-Vが起動できるようになったので、
Hyper-V Server2012で稼働していた仮想マシンをローカル環境に引っ越し。


Ubuntuアップデート→Ubuntu 16.04 LTS
VirtualBoxHyper-Vは共存できないため、VirtualBoxからHyper-Vに引っ越し。
デスクトップ左側のランチャーの1番上の「unity-dash」アイコンをクリック→「update」と入力→「ソフトウェアの更新」。


■Container Linux by CoreOSアップデート→stable 1576.4.0
下記コマンドでアップデート。

$sudo update_engine_client -update


■hosts→mDNSへ
IPアドレスの管理をhostsファイルで行っていたが、ルータ再起動とかでIPアドレスが変わると書換が必要で面倒だった。
WindowsBonjouruBuntuはavahi-daemon、CoreOSはavahi-daemonのDockerをインストールしてmDNSに変更。
DNSサーバを立てなくてもいいし、IPアドレスが変わっても「ホスト名.local」でアクセスできるので便利。

自宅開発環境のアップデート

自宅開発環境アップデートです。
今年も忘れずに、こまめにアップデートします。自分への覚書を残しておきます。


Ubuntuアップデート
VirtualBox上のEclipse等を詰め込んだ、Ubuntu開発環境をアップデート。
デスクトップ左側のランチャーの1番上の「unity-dash」アイコンをクリック→「update」と入力→「ソフトウェアの更新」で手動アップデートします。
Ubuntu15.10→16.04にアップグレード。特に問題なく終了。


Hyper-V Serverアップデート
Hyper-V ServerのWindows Updateを実行。特に問題なく終了。
Hyper-V上のCoreOSは、下記コマンドで899.15.0→1185.5.0にアップデート。特に問題なく終了。

$sudo systemctl unmask update-engine.service
$sudo systemctl start update-engine.service
$update_engine_client -check_for_update
$sudo update_engine_client -update