2009年05月07日(木) [長年日記]
- 長ーい連休は終了。特別なイベントは無く、淡々と過ごした。
- 今日は6日だと思っていたら7日だった。6日まで休みだったのか!定期券が切れてしまった。
■ [ruby] Net::SSH.start() は close を保証しない
RubyでNet::SSH version 2.xを使っている(バージョンは2.0.11)。
Net::SSH.start()をブロック付きで呼び出したときは File.open() などと同様に close が保障されることを期待していたが、そうではないようだ。ブロック内で例外が投げられると session が close されない。バグなのかな。
次のスクリプトで動作確認した。
#!/usr/bin/ruby
require "rubygems"
require "net/ssh"
include Net::SSH::Prompt
def parse_args
if ARGV.size < 2 || ARGV.size > 3
$stderr.puts "Usage: #{$0} HOST USER [PASSWORD]"
exit 2
end
host = ARGV[0]
user = ARGV[1]
password = ARGV[2] || prompt("Enter password for #{user}@#{host}:", false)
return host, user, password
end
def ssh_start(host, user, password)
sss = nil
begin
Net::SSH.start(host, user, :password => password) { |session|
sss = session
raise
}
rescue => ex
puts "Rescued: #{ex.message}"
end
puts "Session#closed?: #{sss.closed?}"
end
def file_open(path)
fff = nil
begin
File.open(path) { |f|
fff = f
raise
}
rescue => ex
puts "Rescued: #{ex.message}"
end
puts "File#closed?: #{fff.closed?}"
end
ssh_start(*parse_args)
file_open($0)
実行結果は次の通り。
$ ./closetest.rb localhost kenichi Enter password for kenichi@localhost: Rescued: Session#closed?: false Rescued: File#closed?: true
2009年05月11日(月) [長年日記]
- 連休が明けたばかりだからか夜眠れない。朝になってやっと眠くなる。
- 夏のように暑い。
■ [ruby] Net::SSH でコマンドの終了コードを得る
(追記:Net::SSHの最新のドキュメントはgithub上のものみたい。)
Net::SSHは Net::SSH::Connection::Session#exec()で簡単にリモートでのコマンド実行ができる。が、このメソッドではコマンドの終了コードを得られず困る。
Net::SSH::Connection::Channel#on_request()を使うことで終了コードを取得できたので、サンプルスクリプトと実行結果をメモ。
require "rubygems"
require "net/ssh"
include Net::SSH::Prompt
def parse_args
if ARGV.size < 2 || ARGV.size > 3
$stderr.puts "Usage: #{$0} HOST USER [PASSWORD]"
exit 2
end
host = ARGV[0]
user = ARGV[1]
password = ARGV[2] || prompt("Enter password for #{user}@#{host}:", false)
return host, user, password
end
# ssh session の close を保証する
def ssh_start(host, user, password)
begin
session = Net::SSH.start(host, user, :password => password)
yield session
rescue => ex
puts "Exception: #{ex.message}"
ex.backtrace.each { |trace| puts trace }
ensure
session.close if session
end
end
def ssh_exec(session, command)
exit_status = nil
session.open_channel { |channel|
# Channel#on_request() を使うと終了コードを取得できる
channel.on_request("exit-status") { |ch, data|
exit_status = data.read_long
}
# 標準出力は Channel#on_data() で扱う
channel.on_data { |ch, data|
data.each_line { |line| puts "[remote:out] #{line}" }
}
# 標準エラーは Channel#on_extended_data() で扱う
channel.on_extended_data { |ch, type, data|
data.each_line { |line| puts "[remote:err] #{line}" if type == 1 }
}
channel.exec(command)
}
session.loop
return exit_status
end
ssh_start(*parse_args) { |session|
puts "Exit status: #{ssh_exec(session, "ps | grep ssh")}"
puts "Exit status: #{ssh_exec(session, "sp")}"
puts "Exit status: #{ssh_exec(session, "cat nantoka")}"
}
実行結果は次の通り。
$ ruby sshexec.rb localhost kenichi Enter password for kenichi@localhost: [remote:out] 3668 ? 00:00:02 ssh-agent [remote:out] 10067 ? 00:00:00 sshd [remote:out] 11922 ? 00:00:00 sshd Exit status: 0 [remote:err] zsh:1: command not found: sp Exit status: 127 [remote:err] cat: [remote:err] nantoka [remote:err] : No such file or directory [remote:err] Exit status: 1
■ [shell][unix] /usr/bin/getopt を使ったサンプル
/usr/bin/getoptを使うとシェルスクリプトの引数の解析が簡単(でもないけど)にできるが、使い方を覚えられないのでメモ。
- getoptは与えられたパラメータを並び変えた結果を出力する。また、パラメータのエラーを検出してくれる。
- ロングオプションも扱える。
- getoptが並び変えた結果を自前で解析する必要がある。
- getoptが行うクォート処理を正しく扱う必要がある。ここ理解しきれていなくて、spikelet daysにある例の真似をした。
- 少なくともbashでは「set --」を使って位置パラメータを置き換えることができる。
以下サンプルスクリプトと実行結果。
#!/bin/bash
# -a 引数必須
# -b 引数なし
# --long 引数必須
# $@を""で囲む必要がある。
args=`getopt -o a:b -l long: -- "$@"`
# getoptの終了コードで書式に間違いがあるかが分かる
if [ "$?" -ne 0 ]; then
echo "usage: $0 [-a VALUE] [-b] [--long VALUE] ARGS" >&2
exit 2
fi
# getoptはパラメータの順序を整理した結果を出力する
echo 'getopt' output: $args
# 位置パラメータ($1, $2, ...)の内容を再設定する
# ""で囲った結果をevalで実行する必要がある
eval set -- "$args"
until [ "$1" == "--" ]; do
case $1 in
-a)
a=$2
shift
;;
-b)
b=true
;;
--long)
long=$2
shift
;;
esac
shift
done
shift # '--' を取り除く
echo a=$a
echo b=$b
echo long=$long
for arg; do echo arg=$arg; done
実行結果は次の通り。
$ type getopt getopt is /usr/bin/getopt $ getopt --version getopt (enhanced) 1.1.4 $ ./getopt.sh abc def getopt output: -- 'abc' 'def' a= b= long= arg=abc arg=def $ ./getopt.sh 'abc def' getopt output: -- 'abc def' a= b= long= arg=abc def $ ./getopt.sh abc def --oreore getopt: unrecognized option '--oreore' usage: ./getopt.sh [-a VALUE] [-b] [--long VALUE] ARGS $ ./getopt.sh abc def --long getopt: option '--long' requires an argument usage: ./getopt.sh [-a VALUE] [-b] [--long VALUE] ARGS $ ./getopt.sh abc def --long=loooong -a 's p a c e' -b getopt output: --long 'loooong' -a 's p a c e' -b -- 'abc' 'def' a=s p a c e b=true long=loooong arg=abc arg=def
2009年05月12日(火) [長年日記]
- 未だに暇。
■ [vim] Vimで上位ディレクトリのtagsファイルを参照
ctagsで作ったtagsファイルをVimから使うときに、カレントディレクトリよりも上位に存在するtagsファイルも見てくれるようにできたのでメモ。
ヘルプのfile-searchingに説明があって、tagsオプションにセミコロン区切りで上位ディレクトリを指定するよう書かれているが、読んでもすぐには理解できなかった。
元々tagsオプションには「./tags,./TAGS,tags,TAGS」と設定されていたが、これを「./tags,./TAGS,tags;/home/ore,TAGS」のように変更することで「tags」というファイルは /home/ore まで上に辿って探してくれるようになった。ははあなるほど。
.tags や TAGS というタグファイルは使っていないので、.vimrc には
set tags=tags;$HOME
と書くことにした。
2009年05月26日(火) [長年日記]
- 運動のため、ときどき家まで歩いて帰ってみている。1時間くらいで帰れる。
- 暇は終了している。今はC++を使っています。
■ [c++][dev] Boost Test Library Unit Test Framework のサンプル
Boost Test Libraryに含まれているUnit Test Frameworkを使ったのでサンプルをメモ。
手元のBoostのバージョンは1.37、最新は1.39のようだ。
サンプルソース
test_main.cpp、vector_test1.cpp、vector_test2.cpp の3つのファイルを作った。
- テストの構成は色々あるようだが、dynamic library variant方式にした。BOOST_TEST_DYN_LINK を define すればいいみたい。
- dynamic library variant方式の場合は BOOST_TEST_MODULE を define すると main() が自動生成される。
- include するヘッダは boost/test/unit_test.hpp。
- テストケースは BOOST_AUTO_TEST_CASE() を使って書くのがよさそう。
- 値の検査は BOOST_CHECK_EQUAL() や BOOST_REQUIRE_EQUAL() で行える。BOOST_CHECK_EQUAL() だと検査がfailedになった場合でもテストケースの処理は先へ進む。使える検査用のマクロ一覧はリファレンスにある。
- setup, teardown 相当の処理は、「フィクスチャ」のコンストラクタとデストラクタで行う。下のサンプルではフィクスチャとして Fixture というクラスを作った。
- テストケース内ではフィクスチャクラスのメンバ変数に直接アクセスできる。フィクスチャクラスを継承しているようだ。
- フィクスチャを使うときは、Test suite level fixtureを使用するのがよさそう。BOOST_FIXTURE_TEST_SUITE() を使う。
- テストケースやフィクスチャは無名(じゃなくてもいいけど)名前空間の中に書くのがよさそう。名前の衝突を気にしなくてよくなるので。
- 全体的に文字列をほとんど与えられず、日本語を埋め込む余地があまりなさそうなのが残念。
// test_main.cpp #define BOOST_TEST_MODULE "ここは日本語を書けそう" #define BOOST_TEST_DYN_LINK #include <boost/test/unit_test.hpp>
// vector_test1.cpp
#include <boost/test/unit_test.hpp>
#include <vector>
namespace {
struct Fixture {
std::vector<int> v;
Fixture() {
v.push_back(1);
}
};
BOOST_FIXTURE_TEST_SUITE(my_suite, Fixture)
BOOST_AUTO_TEST_CASE(add_element) {
// Fixtureのメンバ変数 v を直接参照できる
v.push_back(100);
BOOST_CHECK_EQUAL(v.back(), 100);
BOOST_CHECK_EQUAL(v.size(), 2);
}
BOOST_AUTO_TEST_CASE(remove_element) {
v.pop_back();
BOOST_CHECK_EQUAL(v.size(), 0);
}
BOOST_AUTO_TEST_SUITE_END()
}
// vector_test2.cpp
#include <boost/test/unit_test.hpp>
#include <vector>
namespace {
struct Fixture {
std::vector<int> v;
Fixture() {
v.push_back(1);
v.push_back(2);
v.push_back(3);
}
};
BOOST_FIXTURE_TEST_SUITE(my_suite, Fixture)
BOOST_AUTO_TEST_CASE(add_element) {
v.push_back(100);
BOOST_CHECK_EQUAL(v.back(), 100);
BOOST_CHECK_EQUAL(v.size(), 4);
}
BOOST_AUTO_TEST_CASE(remove_element) {
v.pop_back();
BOOST_CHECK_EQUAL(v.back(), 2);
// テストをわざと失敗させるてみる
BOOST_CHECK_EQUAL(v.size(), 100);
BOOST_REQUIRE_EQUAL(v.empty(), true); // これも実行される
BOOST_CHECK_EQUAL(v[0], 100); // これは実行されない
}
BOOST_AUTO_TEST_SUITE_END()
}
ビルド
次のようにしてビルドできる。
$ g++ -Wall -lboost_unit_test_framework-mt -o testrunner *.cpp
実行結果
実行結果は次の通り。
$ ./testrunner Running 4 test cases... vector_test2.cpp(29): error in "remove_element": check v.size() == 100 failed [2 != 100] vector_test2.cpp(30): fatal error in "remove_element": critical check v.empty() == true failed [false != true] *** 2 failures detected in test suite "ここは日本語を書けそう"
オプションを付けることで出力内容を変えられるが、気に入ったフォーマットが見当たらない。
$ ./testrunner --log_level=test_suite --report_level=detailed
Running 4 test cases...
Entering test suite "ここは日本語を書けそう"
Entering test suite "my_suite"
Entering test case "add_element"
Leaving test case "add_element"
Entering test case "remove_element"
vector_test2.cpp(29): error in "remove_element": check v.size() == 100 failed [2 != 100]
vector_test2.cpp(30): fatal error in "remove_element": critical check v.empty() == true failed [false != true]
Leaving test case "remove_element"
Entering test case "add_element"
Leaving test case "add_element"
Entering test case "remove_element"
Leaving test case "remove_element"
Leaving test suite "my_suite"
Leaving test suite "ここは日本語を書けそう"
Test suite "ここは日本語を書けそう" failed with:
6 assertions out of 8 passed
2 assertions out of 8 failed
3 test cases out of 4 passed
1 test case out of 4 failed
1 test case out of 4 aborted
Test suite "my_suite" failed with:
6 assertions out of 8 passed
2 assertions out of 8 failed
3 test cases out of 4 passed
1 test case out of 4 failed
1 test case out of 4 aborted
Test case "add_element" passed with:
2 assertions out of 2 passed
Test case "remove_element" aborted with:
1 assertion out of 3 passed
2 assertions out of 3 failed
Test case "add_element" passed with:
2 assertions out of 2 passed
Test case "remove_element" passed with:
1 assertion out of 1 passed
■ やること
- 請求書
- 請書