Hatena::ブログ(Diary)

make for h @ppy_things;

2012-02-27(Mon)

autotest-twitterでブヒる

最近とあるgemを書きながら付随するgemを書いてて優先すべきそれが中々進まない昨今ですこんばんは。

で、恥ずかしながらテストファーストってあんまりやったことなくて、今それを実践しながらの開発をしています。使っているツールはRSpecなんですが、コマンドひとつでテストできるとは言っても、今度はそのコマンドを実行するのがめんどくさい。ひたすら怠惰な生き物ですね。

そういう生き物たちにうってつけなのが当然あって、それのひとつにautotestってのがあります。しばらくはautotest + autotest-growlで開発してたんですが、家で使ってるマシンはMac、職場で使ってるマシンはUbuntuなんですね。Macには当然Growlインストールしてるんですが、UbuntuとなるとGrowl以前の問題です。なので「Twitterにテストの結果をツイートして、あとは各OS向けのTwitterクライアントから通知すればいいんじゃね」と思い至りました。というわけでautotest-twitterです。まあ後からよく調べたらautotest-growlLinuxにも対応してましたけどね。ちくしょう。

使い方

README読めと言いたいところですが、GitHubに置いてあるのがいい加減なのでアレです。何がアレってテストを書いていないところですよね。まあとりあえずテストの対象となるアプリケーションなりライブラリが置いてあるディレクトリに.rspecを作り:

--format nested
--color

を、.autotestに:

require 'autotest-twitter'

Autotest::Twitter.configure do |config|
  # ツイートするアカウントのアクセストークンを設定
  config.consumer_key = 'your consumer key'
  config.consumer_secret = 'your consumer secret'
  config.oauth_token = 'your access token'
  config.oauth_token_secret = 'your access token secret'

  # ラベル。アプリケーションの名前とか
  config.label = 'any application'

  # テストの結果に応じてアイコンを変えられるので、そのアイコンが
  # 置いてあるディレクトリ
  #   - missing.png: テスト自体がない場合のアイコン
  #   - failed.png: テストに失敗した場合のアイコン
  #   - pending.png: ペンディングが存在する場合のアイコン
  #   - passed.png: テストに成功した場合のアイコン
  config.image_dir = 'path/to/icons'

  # テストの結果に応じたツイートの内容。$で始まるのは変数
  #   - $label: config.labelで設定した内容
  #   - $all: テストの全件数
  #   - $failed: 失敗したテストの件数
  #   - $pending: ペンディングしてるテストの件数
  config.missing_messages = ['$labelのテストが存在しないよ']
  config.failed_messages = ['$labelのテストに失敗したよ。$all件中$failed件がダメみたい']
  config.pending_messages = ['$labelのテストに$pending件のペンディングがあるよ']
  config.passed_messages = ['$labelのテストに成功したよ! $all件あったみたいだね']
end

を、Gemfileに:

source 'https://rubygems.org'

gem 'autotest'
gem 'autotest-fsevent'
gem 'autotest-twitter', :git => 'git://github.com/takkkun/autotest-twitter.git'

こう。で:

$ bundle --path vendor/bundle

でもしてautotest諸々をインストール。後は:

$ bundle exec autotest

でテストを開始。後はファイルに変更があるたびにテストが走り、結果に応じてツイートされるはずです。config.image_dirを設定してればアイコンも変わります。ちなみにRSpecでしか試していませんし、とりあえず動いてるっぽいってことしか確認してないのであしからず。

ちなみに僕は @Shinobu_DD で試していました。まるでアイコンセットのような画像(TVアニメ偽物語の一部でしょうが)があったので。でもまあ「$labelで$pending件ペンディングがあるようじゃな。お前様の生き様が垣間見えるの。かか」とか打ってると頭抱えたくなりますし、いざブヒろうにも全然テンション上がらないのであんま向いてなかったようです。ていうか元々そういうのじゃないし!

まあでもブヒろうと思えばブヒれるので、テストがコケたらツンツンされたり、テストが通ったらデレデレされたりして、「今日も開発がんばりましゅううう」とか言ってればいいんじゃないですかね。

あとさっき思い付いたんですけど、ツイートするアカウントを自分のアカウントにし、passed.pngをいつも使ってるアイコン、failed.pngをとてつもなく恥ずかしいアイコンにすると面白いんじゃないかと思います。はやくテスト通さないとエラい思いをするハメになるというマゾい開発が出来ていいんじゃないかナーーー。

2011-01-09(Sun)

単数形/複数形の変換ルールを独自に定義する

必要に迫られたので探してました. 案の定用意されたメソッドで好き勝手できるようになってました.

Rails御用達のActiveSupportの場合.

require 'active_support'
require 'active_support/inflector' # Railsは自動で取り込んでくれるだろうけど, ActiveSupportを単体で使う場合は取り込んでくれないみたいです

ActiveSupport::Inflector.inflections do |inflect|
  inflect.irregular 'octopus', 'octopi'
end

ActiveSupport::Inflector.inflections.irregular 'octopus', 'octopi' # コレでも一緒

イレギュラーなケース以外にもいろいろと定義できるので, ActiveSupport::Inflector::Inflectionsのドキュメントなり読みましょう.

Sequelもモデル名とテーブル名の変換に同様の仕組みを用いてる.

require 'sequel'

Sequel.inflections do |inflect|
  inflect.irregular 'octopus', 'octopi'
end

2010-03-13(Sat)

hetemlにRubyGemsをインストールする

"いざhetemlにRubyGemsインストールしようとしたら, すんなりとインストールされず, なんてこったい"を2回繰り返してしまったので, 3度目がないようにメモ.

RubyGemsインストールディレクトリを作成

当然/usr/localとかには入れられないので, ホーム配下にインストール先のディレクトリを作っておく.

$ cd
$ mkdir .gem

RubyGemsのソースをダウンロード

とりあえず適当なディレクトリを作ってそこにダウンロードし, 伸張. RubyGemsのバージョンに合わせて, URLは適当に変える.

$ mkdir src
$ cd src
$ wget http://rubyforge.org/frs/download.php/69365/rubygems-1.3.6.tgz
$ tar zxf rubygems-1.3.6.tgz
$ cd rubygems-1.3.6

ソースを修正

後はインストールすればいいんだけど, このままではgemコマンドがコケるので, ソースを直しておく. ちなみにコケる理由は, rubyコマンドのパスが違うから. RubyGems自体はRbConfig::CONFIG[:bindir]からrubyコマンドのありかを見ているんだけど, なぜかそこにはないのでそれを直す.

でも食い違ってるっておかしくないですか(・∀・`)?

$ vi lib/rubygems.rb

127行目あたり.

:bindir            => RbConfig::CONFIG["bindir"],

コレを:

:bindir            => '/usr/bin',

って直に書いちゃう.

インストール

$ ruby setup.rb --prefix=~/.gem

環境変数の定義

.bashrcを新規作成して書けばいいんだけど, .bash_profileもないので, .bashrcが読込まれない. 一応.bashrcに書くのなら以下のようにでもしとけばいいのかな?

$ cd
$ vi .bash_profile
source .bashrc
$ vi .bashrc
export GEM_HOME=~/.gem
export PATH=~$GEM_HOME/bin:$PATH
export RUBYLIB=$GEM_HOME/lib

次からログインしたときは, .bashrcを読込んで環境変数を定義してくれるけど, 今は.bashrcが読込まれてないので, sourceで読込んでおく. 次回ログインからは不要.

$ source .bashrc

インストールされてるか確認

$ gem -v
1.3.6

ちなみに

RubyGemsをhetemlに入れる理由なんて, Sinatra + CGIでサーバーサイドの処理を書きたい, ぐらいだと思うのだけれど, CGIを実行するユーザーがログインユーザーとは違うので, 環境変数GEM_HOMEとRUBYLIBが定義されておらず, rubygemsを取り込むと盛大に血反吐を吐く.

ので, *.cgiの先頭に:

ENV['GEM_HOME'] = 'ログインユーザーの環境変数GEM_HOMEと同じ値("~"ってやっても無理なので絶対パスで)'
$: << File.join(ENV['GEM_HOME'], 'lib')

って書いておくといいかなーと.

コレはhetemlに限らず, 他のレンタルサーバー(さくらとか)でも一緒かな?

2009-12-12(Sat)

シェルライクなTwitterクライアントをざっくり作ってみた

昨日Twitterでシェルっぽい感じのTwitterクライアントあったらおもしろくない? とか言っていて, なんか楽しそうだったのでざっくり作ってみました.

使い方はこんな感じ.

$ ruby twsh.rb -u your_twitter_id -p password
twsh% echo "はろー twsh.rb"
twsh% ls
screen_name1: tweet1
screen_name2: tweet2
screen_name3: tweet3
screen_name4: tweet4
screen_name5: tweet5
twsh% exit
$

下記コードをtwsh.rbとかで保存して, Rubyとして実行してあげれば起動します. -pオプションはパスワードで必須ですが, -uによるTwitter IDは省略すると環境変数USERが使われます.

後はechoで発言, lsでTL取得です. exit(quit, logout)でシェルを抜けます*1.

ちなみにTwsh.rbとして実装されていないコマンドを用いると, 普段使っているシェルに実行を委ねます. そしてバッククォートによるコマンドの実行を備えているので:

twsh% echo "ボクの$ unameは" `uname` です

とかやると, "ボクの$ unameは Darwin です"って出せたり.

適当で抜けとかありますが, 興味がある方は適当にいじっちゃってくだしあ.

あ, あとボクがTwitterでぶつくさ言っていたら@omasanoriさんが興味を持ったようでして, PythonでTwsh.pyを作っているみたいです! たぶんTwsh.rbよりクールなやつができあがると思いますよ!

require 'optparse'
require 'stringio'
require 'readline'
require 'rubygems'
require 'twitter'

module Twsh
  class Logout < StandardError; end

  class Shell
    class << self
      def login(argv, env)
        new argv, env
      end
    end

    def initialize(argv, env)
      username = env['USER']
      password = nil

      OptionParser.new do |opt|
        opt.on('-u VAL') {|v| username = v}
        opt.on('-p VAL') {|v| password = v}
        opt.parse! argv
      end

      fail if username.nil? || password.nil?

      @client = Twitter::Base.new Twitter::HTTPAuth.new username, password
      input = Input.new self
      logged_in = true

      while logged_in
        begin
          output = execute input.read
          puts output if output
        rescue Logout
          logged_in = false
        rescue Interrupt
          puts
          logged_in = false
        end
      end
    end

    attr_reader :client

    def execute(argv)
      return if argv.empty?
      command = argv.shift
      Command.new(self).execute command, argv
    end
  end

  class Command
    @@commands = {}

    class << self
      def setup
        fail unless block_given?
        yield new
      end
    end

    def initialize(twsh = nil)
      @twsh = twsh
    end

    def on(*commands, &process)
      fail unless block_given?
      commands.each {|command| @@commands[command] = process}
    end

    def execute(command, argv)
      process = @@commands[command]

      if process
        args = []
        args << @twsh if process.arity >= 1
        args << argv if process.arity >= 2
        output = nil

        StringIO.open '', 'w' do |io|
          orig = $stdout
          $stdout = io
          process.call *args
          $stdout = orig
          output = io.string unless io.string.empty?
        end

        output
      else
        `#{command} #{argv.join ' '}`.chomp
      end
    end
  end

  class Input
    def initialize(twsh)
      @twsh = twsh
    end

    def read
      read_with_context
    end

    private

    def read_with_context(context = nil, argv = [])
      prompt = context ? "#{context}> " : argv.empty? ? "twsh% " : '> '
      input = Readline.readline prompt, true
      context, argv, continue = parse "#{input}\n", context, argv
      continue ? read_with_context(context, argv) : argv
    end

    def parse(input, context = nil, argv = [])
      escape = false
      continue = false
      v = !argv.empty? ? argv.pop : ''

      new = lambda do
        argv << v unless v.empty?
        v = ''
      end

      input.each_byte do |c|
        if escape
          escape = false
          case c
          when LINE_FEED
            continue = true
            v << c if context
          when ?n
            v << "\n" if quoted? context
          when ?t
            v << "\t" if quoted? context
          when ?f
            v << "\f" if quoted? context
          when ?v
            v << "\v" if quoted? context
          else
            v << c
          end
        else
          case c
          when ?\n
            v << c if context
          when ?\s
            if context
              v << c
            else
              new.call
            end
          when ?'
            if context == :quote
              new.call
              context = nil
            elsif context
              v << c
            else
              context = :quote
            end
          when ?"
            if context == :dquote
              new.call
              context = nil
            elsif context
              v << c
            else
              context = :dquote
            end
          when ?`
            if context == :bquote
              v = @twsh.execute(parse(v)[1])
              context = nil
            elsif context
              v << c
            else
              context = :bquote
            end
          when ?\\
            escape = true
          else
            v << c
          end
        end
      end

      new.call
      [context, argv, continue || !context.nil?]
    end

    def quoted?(context)
      context == :quote || context == :dquote
    end
  end
end

Twsh::Command.setup do |c|
  c.on('exit', 'quit', 'logout') { raise Twsh::Logout }

  c.on 'echo' do |twsh, argv|
    twsh.client.update argv.join ' '
  end

  c.on 'ls' do |twsh, argv|
    n = 5

    OptionParser.new do |opt|
      opt.on('-n VAL', Integer) {|v| n = v}
      opt.parse! argv
    end

    twsh.client.home_timeline(:count => n).each do |status|
      puts "#{status.user.screen_name}: #{status.text}"
    end
  end

  c.on 'cd' do |twsh, argv|
    if argv.empty?
      Dir.chdir
    else
      Dir.chdir File.expand_path argv.first
    end
  end
end

Twsh::Shell.login ARGV, ENV

*1:Ctrl + cでもいいんですが, Readlineが捉えちゃっているのかInterruptがうまく投げられない……

2009-10-04(Sun)

Twitterのしつこいフェ○アイコンを自動でブロックするサービスを作ったよ

こちらでーす.

ふぇらふぇらほいほい

名前に関してはいろいろありますが, とりあえずBritney Fuckedのせいにしておいてください. あ, ごめんなさいすみません, あ, あ. 悪ふざけです.

使い方はOAuthの方で許可していただければそれだけでOKです. 詳しいコトはふぇらほいの方に書いてありますけどね.

使ったもの

せっかくなんで書いておきます. 余力があったらソースコードでも公開します.

Web側

全部Rubyで書いてあります. コントローラにSinatra, ビューにHamlを使っています. データベースへのアクセスはSQLを直接書いているので, 特にどうこうはありません. あとはグラフを描画するためにGruffを使っています.

サーバーはURLからもわかりますが, さくらさんです. データベースはMySQL. 最初はcoreserverの方でやっていましたけど, Gruffが利用するRMagickをインストールするコトができなかったので, しぶしぶ乗り換えた次第. ご迷惑をおかけしております.

バックグラウンド側

バックグラウンドはすべてErlangで書いています. 動作自体は各ユーザーさんのフォロワーを取得して, 例のスパムがいたらブロックするだけです.

ちなみにボクのマシンで動作させています. 当然Web側のデータベースともやり取りを行わなければいけない(ユーザーさんの取得, ブロック情報の更新)ので, その間はSSHを介してごちゃごちゃやっています*1.

ハマったところ

Web側は特に問題もないのですが, クローラなどのバックグラウンドで動作するやつを書いたコトがないので, 紆余曲折ありました.

最初の作りがあまく, 200ユーザー数を超えたあたりでとあるプロセスがヒープ領域を大量に消費してしまったり, プロセスの死亡をトラップせず, それが影響して全体が死んだり…… ユーザー数も予想外の増加数だったのでけっこう焦ったー. 公開して1日経たずにバックグラウンドのプログラムを書き換えるはめになりました.

学んだコトは:

  • いくらErlangのプロセスが軽いとは言え, そのプロセスに引き連れている値(loop(A, B, C)など)によっては考えモノ
  • メモリ的には問題なくても, 他にボトルネックとなる部分がある. 今回はTwitter APIへのリクエスト. 並列数多すぎてタイムアウトが頻発してた

とかでしょうか. 現在は並列数を最大でも20程度に抑え, プロセスも逐次spawn_linkするようにしてあります.

とりあえず

うっとうしくて, でも無視できなくて, 毎回手動で対処しているという方は一度どうでしょうかー.

*1:本当は同じネットワークにおいて, MySQLのデータベースと直接やりとりしたひ……