Fool Pool

ハマった記

Subversion + Jenkins コミットしたタイミングでビルドする

しくみ:

  • Jenkins: 特定のページを GET したらビルドプロセスが走るようにする。
  • Subversion :コミット後に Jenkins の特定のページを GET するように仕掛ける。

Jenkins でビルド・トリガの設定

設定 => ビルド・トリガ => 「SCMをポーリング」にチェック。
スケジュールを「H H 1 1 *」に設定。
(毎年 1 月 1 日に 1 回ビルド)

Subversion: post-commit hook の設定

Subversionリポジトリに移動して以下を設定

[hook/post-commit]

SVNLOOK=/var/lib/jenkins/csvn/bin/svnlook
REPOS="$1"
REV="$2"
UUID=`$SVNLOOK uuid $REPOS`
/usr/bin/wget \
  --header "Content-Type:text/plain;charset=UTF-8" \
  --post-data "`$SVNLOOK changed --revision 12 $REPOS`" \
  --output-document "-" \
  --timeout=2 \
  http://your-server:8090/jenkins/subversion/${UUID}/notifyCommit?rev=$REV

※ SubversionEdge をインストールした場合は、システムにインストールされている Subversion とは別に、独自の Subversion をもっているので、そちらのパスを絶対パスで指定する。
(参考: "Expected FS format between '1' and '4'; found format '6'" - Fool Pool

Perl + Jenkins テストの書き方

先に構築した CI 環境(
Jenkins + Perl + Suvbersion で CentOS に CI 環境の構築 - Fool Pool
)では「TAP::Formatter::JUnit」を使っている。

モジュールの説明

  • TAP::Formatter::JUnit => Perl の TAP::Harness の出力結果を JUnit の出力形式に変換する
  • TAP::Harness => Perl におけるテスト出力形式の規約
  • Test::More => TAP::Harness の形式に対応したテストモジュール

テストの書き方

フォルダ構造は以下のようにする
ProjectRoot/
- lib/ : 自前のモジュールを格納
- t/ : テストモジュールを格納

[lib/MyNewModule.pm]

package MyNewModule;

sub func
{
    my $arg = shift;

    return $arg ? 'func:' . $arg : 'func';
}

1;


[t/use_ok.t]

#!/usr/bin/perl
 use strict;
 use warnings;
 use Test::More tests => 1;

 BEGIN { use_ok('MyNewModule'); }

[t/func_ok.t]

#!/usr/bin/perl
use strict;
use warnings;
use Test::More tests => 2;
use MyNewModule;

is(MyNewModule::func('arg1'), 'func:arg1');
is(MyNewModule::func(), 'func');

Jenkins + Perl + Suvbersion で CentOS に CI 環境の構築

jenkins, plenv インストール

以下のサイトで「plenvのインストール」まで実施

Jenkins + plenv で、ブランチ毎のテスト実行環境を構築する - Qiita

Perl のバージョン: 5.18.2

必要なモジュールのインストール

$ su - jenkins
$ cpanm Storable
$ cpanm Digest::MD5
$ cpanm Devel::Cover
$ cpanm Devel::Cover::Report::Clover::Builder
$ cpanm Devel::Cover::Report::Clover
$ cpanm Perl::Metrics::Lite
$ cpanm TAP::Formatter::JUnit

※ なお、 TAP::Formatter::JUnit の依存モジュール「XML::Parser」のインストールでコケるので、「expat-devel」をインストール。

$ sudo yum install  expat-devel

Collabo Net Subversion Edge のインストール

ホームページからダウンロード:
Subversion for the Enterprise, from CollabNet | CollabNet

$ su - jenkins
$ tar xvfz CollabNetSubversionEdge-4.0.11_linux-x86_64.tar.gz
$ sudo -E csvn/bin/csvn install

※ root で install するとあとでハマる(svnserve 起動できない)。

Collabo Net Subversion Edge の初期設定

$ csvn/bin/csvn start

管理画面にログイン:
http://yourserver:3343/csvn

アカウント: admin
パスワード: admin

Subversion 状態: 停止 となっているので、「起動」をクリック。
==> svnserve が起動する

Jenkinsの設定

以下のサイトを参考に、設定:
http://blog.on-net.jp/tf/2012/08/perl_jenkins.html

新規ジョブの作成

ジョブ名: JenkinsJOB

ビルド => ビルド手順の追加 => シェルの実行

以下のスクリプトを追加:

#!/bin/bash
 
JENKINS_WORK=~/jobs/JenkinsJOB/workspace/trunk
PROVE=~/.plenv/shims/prove
CHECK_STYLE=~/.plenv/shims/measureperl-checkstyle
COVER=~/.plenv/shims/cover

${PROVE} \
-I${JENKINS_WORK}/lib \
--formatter=TAP::Formatter::JUnit -l ${JENKINS_WORK}/t > test-results.xml

${CHECK_STYLE} \
--max_sub_lines 60 \
--max_sub_mccabe_complexity 10 \
--directory ${JENKINS_WORK}/lib > checkstyle-result.xml

${COVER} -delete
HARNESS_PERL_SWITCHES=-MDevel::Cover=+ignore,inc \
${PROVE} \
-I${JENKINS_WORK}/lib \
-lv ${JENKINS_WORK}/t 2>&1

${COVER} -report clover
${COVER} -report html -outputfile index.html

※ 最後の行で、「${COVER} -report html -outputfile index.html」を追加しないと、「Clover カバレッジレポート」が 「404 Not Found」になる。

ビルド結果の評価方法の設定

ビルド後の処理 => Clover カバレッジレポートを集計

  • Cloverレポートディレクトリ: cover_db
  • Cloverレポートファイル名: clover.xml

ビルド後の処理 => JUnitテスト結果を集計

  • テスト結果 xml: test-results.xml

完成

f:id:nakamine11:20141130184723p:plain

"Expected FS format between '1' and '4'; found format '6'"

発生契機

Subversion コミット後、post-commit hook で Jenkins の Build プロセスを起動させようとした。

エラーメッセージ

Warning: post-commit hook failed (exit code 8) with output:
svnlook: Expected FS format between '1' and '4'; found format '6'
svnlook: Expected FS format between '1' and '4'; found format '6'

原因

Subversion Edge にインストールされている svn のバージョン(1.8.10)と、システムにインストールされている svn のバージョン(1.6.11)が異なるため。

対処

post-commit hook の記述を修正。
CollabNet Subversion Edge が持っている svnlook のパスをフルパスで指定する。

[hook/post-commit]

REPOS="$1"
REV="$2"
UUID=`(Collab Net Subversion のインストールパス)/bin/svnlook uuid $REPOS`
/usr/bin/wget \
  --header "Content-Type:text/plain;charset=UTF-8" \
  --post-data "`svnlook changed --revision $REV $REPOS`" \
  --output-document "-" \
  --timeout=2 \
  http://<YOUR_SERVER_NAME>/subversion/${UUID}/notifyCommit?rev=$REV

C++ 条件を満たすオブジェクトを検索する

※ 注: このエントリにあるコードを動かすには、C++11 が必要。

仕事で C++ のコードを保守することになり、かれこれ5年ぶりに C++ のコードを読んだ。
そしたら、次のようなコードに行き当たって衝撃を受けた(悪い意味で)。

コード1: まずいコード

#include <iostream>
#include <vector>
#include <boost/format.hpp>
using namespace std;

class Foo {
public:
	Foo(int x, int y) : _x(x), _y(y) {};
	int x() const { return _x; }
	int y() const { return _y; }
	string to_str() { return str(boost::format("(%s, %s)") % _x % _y); }
	bool operator == (Foo other) { return this->x() == other.x(); }
        // [!] 検索のためだけにイコール演算子を再定義?!
private:
	int _x;
	int _y;
};

int main(int argc, char **argv) {
	vector<Foo> fv;

	fv.push_back(Foo {3,5});
	fv.push_back(Foo {7,8});
	fv.push_back(Foo {6,8});

	Foo key{3, 4};
	auto found = find(std::begin(fv), std::end(fv), key);
        // もちろん、検索はできるけど・・・。

	if (found != std::end(fv)) {
		cout << "Found: " << (*found).to_str() << '\n';
	}
	else {
		cout << "Not found\n";
	}

        // [!] Foo {3, 4} と Foo {3, 5} が同じになってしまう 
        if (Foo {3, 4} == Foo {3, 5}) {
                cout << "Foo {3, 4} equals Foo {3, 5}.\n";
        }
}

コード1の出力:

Found: (3, 5)
Foo {3, 4} equals Foo {3, 5}.

まさか、「リストから条件に合ったものを検索する」だけのために演算子再定義するとは。後で Foo オブジェクトをきちんと比較したくなった時はどうするつもりだったのか? こういった急場しのぎの対処を放置すると、後で保守する人が混乱するので問題だ(せめて、コメントを残してあげよう)。

代替としては、find_if を使うやり方と、for 文を回して自分でマッチングの処理を書くやり方がある。

例1(find_if):

	auto found = find_if(std::begin(fv), std::end(fv),
			[key] (Foo f) {
				return f.x() == key.x();
			});

例2(for文):

	boost::optional<Foo> found;
	for (Foo foo : fv) {
		if (key.x() == foo.x()) {
			found = foo;
		}
	}

例1の注意としては、Foo::x() を constメンバ関数として定義していない場合、 以下のようなコンパイルエラーが出る。

../sample.cpp:74:21: error: member function 'x' not viable: 'this' argument has type 'const Foo', but function is not marked const

つまり、ラムダ式の [...] 内の key はコピーキャプチャなので、状態を変更しても無意味だ。にもかかわらず、状態を変更しうる(constマーカーのない)メソッド Foo::x() を呼ぼうとしたので、エラーとなった。つまり、コピーキャプチャとして宣言したオブジェクトに対しては、状態を変更しない(constメソッドしか呼べない仕様になっているらしい。よくできた仕様だ(メッセージが分かりやすければ最高だけど)。

例2の boost::optional はけっこうイイ。perl のように、if文の条件式中にオブジェクトを入れて存在確認ができる。ポインタを意識しなくて良いのがいい。

	if (found) {
		cout << "Found: " << (*found).to_str() << '\n';
	}
	else {
		cout << "Not found\n";
	}

(boost::optional を使うときは boost/optional.hpp をインクルードする。)

ところで、「ラムダ式の先頭にキャプチャ変数を書く」という C++11 の流儀はあまり慣れない。素直にラムダの引数として渡したいのだが、find_if は1引数の関数しか受け付けてくれない。2引数の関数を1引数の関数に変換する方法はないものだろうか?

こういうときは、カリー化を使う。functional ライブラリで提供されている。

まず、binary_function を使って2引数の関数オブジェクトを作り、find_if の引数として渡すときにbind2nd 関数を使って 2 引数関数を 1 引数関数に変換する。この例のように、もとの関数の引数のいくつかを与えて、引数が少ない新たな関数を生成することを「カリー化」という。

「a と b で属性 x が等しい」ことを表現した関数オブジェクト:

class pred_with_same_x: public binary_function<Foo, Foo, bool> {
public:
	bool operator()(Foo f1, Foo f2) const
	{
		return f1.x() == f2.x();
	}
};

main のコード:

	pred_with_same_x same_x; // 2変数関数オブジェクトの生成
	auto found = find_if(std::begin(fv), std::end(fv), bind2nd(same_x, key)); // 1変数にカリー化

それにしても、昔から C++ は泥臭い処理を書かないといけないイメージがあってずっと敬遠していたが、ラムダに高階関数までできたとは。5年の間に進歩したものだ!
(でもやはり、使えるなら JavaC# を使いたい自分がいる。)

ちなみに、上であげたコードは C++11 に対応したコンパイラでないと動かない。コンパイラの引数で「-std=c++0x」などと指定する。

参考文献

C++ラムダ式は[1]で詳しく説明されている。

[1] C++0x ラムダ式 - Faith and Brave - C++で遊ぼう

Vimを開発で使うためのカスタマイズTips

行番号を表示する

これがないとデバッグできないし。

set number

シンタックスハイライト

テキストに色をつける。

syntax on
set nohlsearch
set cursorline

オートインデント

タブストップはスペース4つ分が基本。

set tabstop=4
set autoindent
set expandtab
set shiftwidth=4

行頭と直前の行の文末をつなげる

  • コマンドモード: 行頭と直前の行を行き来できるようになる
  • 編集モード: 行頭でバックスペースを打つと直前の行と結合される

普通のエディタだと当たり前だが、vimだと当たり前ではない。

set backspace=start,eol,indent
set whichwrap=b,s,[,],,~

自動補完

オムニ補完(※1)をONにする。一度入力した単語は、2回目から候補に上がる。
※1: <C-x><C-o>で単語を類推する機能。Visual Studio の IntelliSense と同じ。

filetype plugin on
setlocal omnifunc=syntaxcomplete#Complete

<C-x><C-o>をタイプしなくても入力中に自動的にポップアップさせるには、以下のプラグインをインストールする。

HTML/XML の閉じタグ自動補完

「<text>こんにちは</」まで入力すると「text>」の部分を自動で補完してくれる。この設定はvimの標準の機能(オムニ補完)を自動で呼び出されるようにしただけ[1]。ちなみに、筆者の環境(Mac OS 10.9.2 Marverics)だと「filetype plugin on」を設定していないと有効にならなかった。

filetype plugin on
augroup MyXML
  autocmd!
  autocmd Filetype xml inoremap <buffer> </ </<C-x><C-o>
  autocmd Filetype html inoremap <buffer> </ </<C-x><C-o>
  autocmd Filetype eruby inoremap <buffer> </ </<C-x><C-o>
augroup END

正規表現の練習 -- XMLの閉じタグ補完

概要

開きタグのみ記述されたXMLコードの閉じタグを補完する。

例)
[入力データ]

<day id="05">5日
<month>4月

[出力データ]

<day id="05">5日</day>
<month>4月</month>

要件

  • タグの階層は1つのみ(入れ子は考えない)
  • タグの属性(id属性など)はあってもよい。

実装

[sample.in]

<day id="01">5日
<month>4月

[tag_complete.pl]

@in_text = <>;
@out_text = ();

for (@in_text) {
    my $line = $_; 
    chomp $line;

    my ($whole,$tag) = ($line =~ m/(<([^> ]+)[^>]*>[^<>]+)/);
    push @out_text, "$whole</$tag>";
}

print "$_\n" for @out_text;

[出力結果]

$ perl tag_complete.pl sample.in
<day id="01">5日</day>
<month>4月</month>

制約

  • 1行につき開きタグは1つのみ
  • タグで囲っている文字列は途中で改行されていない

例)以下のような入力は正しく処理できない。

<day>5日<month>4月
<day>
6日
<month>4
月

以下のような入力は正しく処理できる。

<day>5日
<month>4月
<day>6日
<month>4月
  • XMLのタグ名はスペース( )、およびタグの終端記号(>)をのぞく任意の文字の繰り返しからなる[4]

参考文献

[1] ActivePerlのダウンロード及びインストール -- Perlのインストール方法
[2] Perl入門 -- 初心者向けの How To
[3] Perl基礎入門 | KentWeb -- 正規表現の解説
[4] Extensible Markup Language (XML) 1.0 (Fifth Edition) -- W3C, XML 1.0 勧告

補足

W3C, SML 1.0勧告[4]によると、タグ名にドット(.)やコロン(:)なども許容するらしい。

Names and Tokens

[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
[5] Name ::= NameStartChar (NameChar)*
[6] Names ::= Name (#x20 Name)*
[7] Nmtoken ::= (NameChar)+
[8] Nmtokens ::= Nmtoken (#x20 Nmtoken)*

よりXMLに忠実に書くと、タグ名を切り出す正規表現は以下のようになる。
[仕様]

  • タグ名の開始文字 ::= ":" | [A-Z] | "_" | [a-z]
  • タグ名の2文字目以降 ::= ":" | [A-Z] | "_" | [a-z] | "-" | "." | [0-9]
/([:A-Z_a-z][:A-Z_a-z-.0-9]*)/