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ユニットテスト
PhpUnitのDBUnit拡張が追加できない特殊な環境向け(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
VirtualBoxとHyper-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アドレスが変わると書換が必要で面倒だった。
WindowsはBonjour、uBuntuはavahi-daemon、CoreOSはavahi-daemonのDockerをインストールしてmDNSに変更。
DNSサーバを立てなくてもいいし、IPアドレスが変わっても「ホスト名.local」でアクセスできるので便利。