メモの日々


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::SSHNet::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

やること

  • 請求書
  • 請書