ククログ
http://www.clear-code.com/blog/
クリアコードのログ
ClearCode Inc.
-
インターンシップ参加希望者募集開始
http://www.clear-code.com/blog/2008/12/25.html
クリアコードでは来年の夏にインターンシップを実施する予定です。(2009年8月〜9月を予定)
そこで、インターンシップ参加希望者の募集を開始しました。
詳しくは募集ページに書いていますが、ここにも簡単に概要を書いておきます。
応募条件
応募条件は「プログラミングが好きなこと」です。他は、学生であることとまとまった時間がとれることなので、「プログラミングが好きなこと」だけが条件といってよいかと思います。もっと具体的な条件にしたり、細かく条件を並べるよりも、一番大切なこと1つだけとしました。「プログラミングが好きなこと」がプログラミング技術が飛躍的に上達するために必須のことだからです。
カリキュラム
インターンシップでは、フリーソフトウェアの開発を参加者とクリアコードのエンジニアがペアプログラミングで行います。このようなカリキュラムにしたことにはいくつが理由がありますが、一番の理由はこのカリキュラムが参加者もクリアコードのエンジニアもともに成長できると信じているからです。
開発対象としてフリーソフトウェアを選んだことには2つの理由があります。1つはクリアコードのエンジニアが業務で活かしている技術はフリーソフトウェアの開発で身につけたものだからです。もう1つは業務でFirefox/ThunderbirdやRubyなどの既存のフリーソフトウェアを利用したり、新規に開発することも多いからです。フリーソフトウェアではない開発業務もありますが、その場合でもフリーソフトウェアの開発で身につけた技術は活きています。クリアコードの業務が成り立つのはフリーソフトウェアの開発に関わっているからといっても過言ではありません。そして、参加者にとってもフリーソフトウェアの開発に関わることが有益だと思っています。
インターンシップという短い期間で、参加者には2つのことを学んでほしいと考えています。1つはプログラミング技術の向上、あるいは、向上するための方法です。もう1つはソフトウェア開発業務についてです。フリーソフトウェアの開発をペアプログラミングで行うことでそれらを実現できると考えています。
ペアプログラミングで開発を行うことにより、参加者には技術面において短い期間で大きな効果がでることを期待しています。また、クリアコードのエンジニアも教えることで学んでいけると期待しています。経験がある方も多いと思いますが、教えることで自分の理解が進むことは多いものです。
また、フリーソフトウェアの開発という内容はクリアコードでのソフトウェア開発業務にも結びつきます。クリアコードでは業務の中でフリーソフトウェアを利用するだけではなく、開発することもあるからです。例えば、C言語用のテスティングフレームワークのCutterや迷惑メール対策を支援するmilter-managerなどです。
フリーソフトウェアの開発をペアプログラミングで行うというカリキュラムで、参加者もクリアコードのエンジニアもともに有意義なインターンシップになることを期待しています。
参加希望者の方へ
参加者もそうなると思いますが、このインターンシップはクリアコードにとっても初めてのインターンシップになります。よりよい内容にするためには試行錯誤を重ねていく必要があり、今はまだ最初の一歩です。そのため、現段階ではよりていねいにインターンシップを行うことができるように、今回は1名のみの募集としました。
興味がある方はインターンシップ応募申込書(Calc用|Excel用)に必要事項を記入し、minami@clear-code.comにメールで提出してください。疑問点なども同じアドレスで受け付けています。
余談ですが、インターンシップ参加希望者募集にあわせて会社紹介パンフレットを作成しました。(インターンシップページの最下部にリンクがあります。)
<p>クリアコードでは来年の夏にインターンシップを実施する予定です。(2009年8月〜9月を予定)</p>
<p>そこで、<a href="/internship/">インターンシップ参加希望者の募集</a>を開始しました。</p>
<p>詳しくは募集ページに書いていますが、ここにも簡単に概要を書いておきます。</p>
<h4>応募条件</h4>
<p>応募条件は「プログラミングが好きなこと」です。他は、学生であることとまとまった時間がとれることなので、「プログラミングが好きなこと」だけが条件といってよいかと思います。もっと具体的な条件にしたり、細かく条件を並べるよりも、一番大切なこと1つだけとしました。「プログラミングが好きなこと」がプログラミング技術が飛躍的に上達するために必須のことだからです。</p>
<h4>カリキュラム</h4>
<p>インターンシップでは、フリーソフトウェアの開発を参加者とクリアコードのエンジニアがペアプログラミングで行います。このようなカリキュラムにしたことにはいくつが理由がありますが、一番の理由はこのカリキュラムが参加者もクリアコードのエンジニアもともに成長できると信じているからです。</p>
<p>開発対象としてフリーソフトウェアを選んだことには2つの理由があります。1つはクリアコードのエンジニアが業務で活かしている技術はフリーソフトウェアの開発で身につけたものだからです。もう1つは業務でFirefox/ThunderbirdやRubyなどの既存のフリーソフトウェアを利用したり、新規に開発することも多いからです。フリーソフトウェアではない開発業務もありますが、その場合でもフリーソフトウェアの開発で身につけた技術は活きています。クリアコードの業務が成り立つのはフリーソフトウェアの開発に関わっているからといっても過言ではありません。そして、参加者にとってもフリーソフトウェアの開発に関わることが有益だと思っています。</p>
<p>インターンシップという短い期間で、参加者には2つのことを学んでほしいと考えています。1つはプログラミング技術の向上、あるいは、向上するための方法です。もう1つはソフトウェア開発業務についてです。フリーソフトウェアの開発をペアプログラミングで行うことでそれらを実現できると考えています。</p>
<p>ペアプログラミングで開発を行うことにより、参加者には技術面において短い期間で大きな効果がでることを期待しています。また、クリアコードのエンジニアも教えることで学んでいけると期待しています。経験がある方も多いと思いますが、教えることで自分の理解が進むことは多いものです。</p>
<p>また、フリーソフトウェアの開発という内容はクリアコードでのソフトウェア開発業務にも結びつきます。クリアコードでは業務の中でフリーソフトウェアを利用するだけではなく、開発することもあるからです。例えば、C言語用のテスティングフレームワークの<a href="http://cutter.sourceforge.net/">Cutter</a>や迷惑メール対策を支援する<a href="http://sourceforge.net/projects/milter-manager">milter-manager</a>などです。</p>
<p>フリーソフトウェアの開発をペアプログラミングで行うというカリキュラムで、参加者もクリアコードのエンジニアもともに有意義なインターンシップになることを期待しています。</p>
<h4>参加希望者の方へ</h4>
<p>参加者もそうなると思いますが、このインターンシップはクリアコードにとっても初めてのインターンシップになります。よりよい内容にするためには試行錯誤を重ねていく必要があり、今はまだ最初の一歩です。そのため、現段階ではよりていねいにインターンシップを行うことができるように、今回は1名のみの募集としました。</p>
<p>興味がある方はインターンシップ応募申込書(<a href="/internship/internship_entry.ods">Calc用</a>|<a href="/internship/internship_entry.xls">Excel用</a>)に必要事項を記入し、minami@clear-code.comにメールで提出してください。疑問点なども同じアドレスで受け付けています。</p>
<p>余談ですが、インターンシップ参加希望者募集にあわせて会社紹介パンフレットを作成しました。(<a href="/internship/">インターンシップ</a>ページの最下部にリンクがあります。)</p>
ClearCode Inc.
2009-01-05T08:08:05+09:00
-
会社紹介は一番最後
http://www.clear-code.com/blog/2008/12/17.html
2週間ほど前になりますが、オブジェクト倶楽部2008冬イベント『オブラブ忘年会 〜ふりかえり2008〜』のライトニングトークスで話してきました。
発表資料: xUnit 2008
一番伝えたかったことは「テスティングフレームワークはデバッグ支援の機能を提供することが重要」だということです。時間もないので、その中でも「期待値と実際の値の違いの見せ方」についてだけ触れました。(資料でいうと24枚目からの話)
表向きは上の通りなのですが、今回の発表では1つ試してみたことがあります。それは、「会社紹介は一番最後がよいのではないか」ということです。一般的には自己紹介・会社紹介をしてから内容に入ることが多い気がしますが、それとは少し異なります。
最後の会社紹介は次のアクションの誘導
先日、Email Security Expo & Conference 2008(来年の今頃はこのURLになっていそう)に参加しました。参加の目的は情報収集です。商用の迷惑メール対策用製品がどこをウリにしているのかとどのようなWebインターフェイスを提供しているかに興味がありました。
現在、IPAの2008年度 オープンソフトウェア利用促進事業 上期テーマ型(開発) 公募に採択された「迷惑メール対策ポリシーを柔軟に実現するためのmilterの開発」のために、milter-managerを開発しているのですが、それを開発する上で参考にするためです。
イベントでは製品紹介のセッションを中心に受講したのですが、その中で気づいたことがありました。
プレゼン資料の中で、一番最後に会社紹介を持ってきているセッションが多かったのです。そして、いくつかのセッションを聴いているうちにこれはとてもよい方法だということがわかりました。
この方法が有効なのは、セッションを聴いて、この製品がよさそうだなぁと思ったときです。プレゼンの一番最後に会社紹介を持ってくることで、よさそうだと思ってもらった後のアクションを誘導することができるのです。
プレゼンしている側としては、製品を紹介し、興味を持ってもらった人に問い合わせてもらいたいものです。一番最後に会社紹介を持ってくることで問い合わせ先を伝え、問い合わせるというアクションを誘導しているのです。(イベントではブースコーナーもあったので会社名さえわかれば、ブースコーナーに行って、デモを試したりより詳しい説明を受けることができた)
一番最後に会社紹介がないセッションを聴いているときにも、この製品はよさそうだなぁと思い、デモを見にいこうと思うことがありました。しかし、プレゼンの最後の頃には最初の方にあった会社紹介の内容はすでに覚えていなかったため、配布されていた資料を見直して、自分でどの会社かを調べ直す必要がありました。興味を持った人にこのような手間をかけさせると、その手間のために次のアクションを起こさないかもしれません。せっかく興味を持った人がスムーズに次のアクションに進めるように、ちょうど良いタイミングで次のアクションのための情報を提供することは有効だと思います。
試したこと
このような気づきがあったので、オブジェクト倶楽部でのライトニングトークスでは「一番最後に会社紹介をする」ということを試してみました。今回は、話す内容のベースに「動作するきれいなコードを維持し続けることが重要」ということがあったので、それと会社紹介をつなげやすそうだったということもあります。(クリアコード→きれいなコード)
今回は、発表の内容で「動作するきれいなコードを維持し続けることが重要」だと思ってくれた人が「話している人の会社もきれいなコードを維持し続けるのか、よさそうな会社だな」と思ってくれるかどうかを試してみました。つまり、今回の「次のアクション」は「動作するきれいなコードを維持し続ける」人たちがいる会社としてクリアコードを認識してもらうということでした。
何人かには成功したようなので、次に話すときも試してみようと思っています。そして、次の「次のアクション」はもう少し行動を促すことにしようと思っています。
<p>2週間ほど前になりますが、<a href="http://www.objectclub.jp/event/2008winter/">オブジェクト倶楽部2008冬イベント『オブラブ忘年会 〜ふりかえり2008〜』</a>のライトニングトークスで話してきました。</p>
<p>発表資料: <a href="http://pub.cozmixng.org/~kou/archives/object-club-2008-winter/">xUnit 2008</a></p>
<p>一番伝えたかったことは「テスティングフレームワークはデバッグ支援の機能を提供することが重要」だということです。時間もないので、その中でも「期待値と実際の値の違いの見せ方」についてだけ触れました。(資料でいうと<a href="http://pub.cozmixng.org/~kou/archives/object-club-2008-winter/xunit-2008-24.html">24枚目から</a>の話)</p>
<p>表向きは上の通りなのですが、今回の発表では1つ試してみたことがあります。それは、「会社紹介は一番最後がよいのではないか」ということです。一般的には自己紹介・会社紹介をしてから内容に入ることが多い気がしますが、それとは少し異なります。</p>
<h4>最後の会社紹介は次のアクションの誘導</h4>
<p>先日、<a href="http://www.cmptech.jp/esc/">Email Security Expo & Conference 2008</a>(来年の今頃は<a href="http://www.cmptech.jp/esc/2008/">このURL</a>になっていそう)に参加しました。参加の目的は情報収集です。商用の迷惑メール対策用製品がどこをウリにしているのかとどのようなWebインターフェイスを提供しているかに興味がありました。</p>
<p>現在、IPAの<a href="http://www.ipa.go.jp/software/open/ossc/2008/theme/koubo1.html">2008年度 オープンソフトウェア利用促進事業 上期テーマ型(開発) 公募</a>に採択された「迷惑メール対策ポリシーを柔軟に実現するためのmilterの開発」のために、<a href="http://sf.net/projects/milter-manager/">milter-manager</a>を開発しているのですが、それを開発する上で参考にするためです。</p>
<p>イベントでは製品紹介のセッションを中心に受講したのですが、その中で気づいたことがありました。
プレゼン資料の中で、一番最後に会社紹介を持ってきているセッションが多かったのです。そして、いくつかのセッションを聴いているうちにこれはとてもよい方法だということがわかりました。</p>
<p>この方法が有効なのは、セッションを聴いて、この製品がよさそうだなぁと思ったときです。プレゼンの一番最後に会社紹介を持ってくることで、よさそうだと思ってもらった後のアクションを誘導することができるのです。</p>
<p>プレゼンしている側としては、製品を紹介し、興味を持ってもらった人に問い合わせてもらいたいものです。一番最後に会社紹介を持ってくることで問い合わせ先を伝え、問い合わせるというアクションを誘導しているのです。(イベントではブースコーナーもあったので会社名さえわかれば、ブースコーナーに行って、デモを試したりより詳しい説明を受けることができた)</p>
<p>一番最後に会社紹介がないセッションを聴いているときにも、この製品はよさそうだなぁと思い、デモを見にいこうと思うことがありました。しかし、プレゼンの最後の頃には最初の方にあった会社紹介の内容はすでに覚えていなかったため、配布されていた資料を見直して、自分でどの会社かを調べ直す必要がありました。興味を持った人にこのような手間をかけさせると、その手間のために次のアクションを起こさないかもしれません。せっかく興味を持った人がスムーズに次のアクションに進めるように、ちょうど良いタイミングで次のアクションのための情報を提供することは有効だと思います。</p>
<h4>試したこと</h4>
<p>このような気づきがあったので、オブジェクト倶楽部でのライトニングトークスでは「一番最後に会社紹介をする」ということを試してみました。今回は、話す内容のベースに「動作するきれいなコードを維持し続けることが重要」ということがあったので、それと<a href="http://pub.cozmixng.org/~kou/archives/object-club-2008-winter/xunit-2008-31.html">会社紹介</a>をつなげやすそうだったということもあります。(クリアコード→きれいなコード)</p>
<p>今回は、発表の内容で「動作するきれいなコードを維持し続けることが重要」だと思ってくれた人が「話している人の会社もきれいなコードを維持し続けるのか、よさそうな会社だな」と思ってくれるかどうかを試してみました。つまり、今回の「次のアクション」は「動作するきれいなコードを維持し続ける」人たちがいる会社としてクリアコードを認識してもらうということでした。</p>
<p>何人かには成功したようなので、次に話すときも試してみようと思っています。そして、次の「次のアクション」はもう少し行動を促すことにしようと思っています。</p>
ClearCode Inc.
2008-12-17T09:40:38+09:00
-
tDiaryのデータをHTML化する
http://www.clear-code.com/blog/2008/12/5.html
tDiaryをローカルなネットワークに配置して、tDiaryが表示する内容を静的なHTMLとして公開したい場合はよくありますよね。ククログもそんなよくある使い方の1つです。
tDiaryには静的なHTMLを生成するためのsqueezeプラグインがありますが、squeezeプラグインが出力するHTMLは以下の点でCGIで表示される内容と異なります。
各日付のページしか生成しない
最新の日記n件ページや月別ページやカテゴリページは生成しない
リンクがCGI用のリンクのままで、次の日記のページに移動するリンクが壊れている
テーマファイルや画像はコピーしてくれないので、生成したHTMLの入ったディレクトリ以下だけでは完結しない
ただし、これはsqueezeプラグインが検索エンジンへの入力データとしてのHTML生成を目的としているためです。よくある使い方では、生成されたHTMLはCGIで出力されているように表示できることが目的なので、上記のようなミスマッチが発生します。
そこで、ククログではhtml-archiver.rbという静的なHTMLを生成するスクリプトを使っています。html-archiver.rbは最後の方に載せています。
html-archiver.rbの使い方
html-archiver.rbを使うと、CGIで出力されている内容と同じように表示されるHTMLが生成されます。生成例は今見ているこのページです。
使い方はこうなります。
% ruby html-archiver.rb --tdiary tdiayr.rbのあるディレクトリ --conf tdiary.confのあるディレクトリ 出力先ディレクトリ
例えば、以下のような場合を考えます。
tdiary.rbは~tdiary/work/ruby/tdiary/core/にある
tdiary.confは~tdiary/public_html/にある
HTMLは~tdiary/public_html/html/以下に出力する
この場合はこのようなコマンドになります。
% ruby html-archiver.rb --tdiary ~tdiary/work/ruby/tdiary/core/ --conf ~tdiary/public_html/ ~tdiary/public_html/html/
機能
日付ページの生成: 例
最新n件ページの生成: 例
月別ページの生成: 例
RSS 1.0の生成: 例
テーマファイルのコピー
画像のコピー
制限
ツッコミが生成されるかどうかは試していない
カテゴリ一覧ページがきちんと生成されるかは(最近は)試していない
タブインデント(tDiary本体のコーディングスタイルに合わせているため)
思ったほど使う場面が少ないかもしれない(もしかしたら、tDiaryが表示する内容を静的なHTMLとして公開することがそんなにないかもしれない)
ライセンス
GPL3あるいは3以降の新しいバージョンのGPL
html-archiver.rb
#!/usr/bin/env ruby
# -*- coding: utf-8; ruby-indent-level: 3; tab-width: 3; indent-tabs-mode: t -*-
require 'uri'
require 'cgi'
require 'fileutils'
require 'pathname'
require 'optparse'
require 'ostruct'
require 'enumerator'
require 'rss'
options = OpenStruct.new
options.tdiary_path = "./"
options.conf_dir = "./"
opts = OptionParser.new do |opts|
opts.banner += " OUTPUT_DIR"
opts.on("-t", "--tdiary=TDIARY_DIRECTORY",
"a directory that has tdiary.rb") do |path|
options.tdiary_path = path
end
opts.on("-c", "--conf=TDIARY_CONF", "a path of tdiary.conf") do |conf|
options.conf_dir = conf
end
end
opts.parse!
output_dir = ARGV.shift
Dir.chdir(options.conf_dir) do
$LOAD_PATH.unshift(File.expand_path(options.tdiary_path))
require "tdiary"
end
module HTMLArchiver
class CGI < ::CGI
def referer
nil
end
private
def env_table
{"REQUEST_METHOD" => "GET", "QUERY_STRING" => ""}
end
end
module Image
def init_image_dir
@image_dest_dir = @dest + "images"
end
end
module Base
include Image
def initialize(rhtml, dest, conf)
@ignore_parser_cache = true
cgi = CGI.new
setup_cgi(cgi, conf)
@dest = dest
init_image_dir
super(cgi, rhtml, conf)
end
def eval_rhtml(*args)
link_detect_re = /(<(?:a|link)\b.*?\bhref|<img\b.*?\bsrc)="(.*?)"/
super.gsub(link_detect_re) do |link_attribute|
prefix = $1
link = $2
uri = URI(link)
if uri.absolute? or link[0] == ?/
link_attribute
else
%Q[#{prefix}="#{relative_path}#{link}"]
end
end
end
def save
return unless can_save?
filename = output_filename
if !filename.exist? or filename.mtime != last_modified
filename.open('w') {|f| f.print(eval_rhtml)}
filename.utime(last_modified, last_modified)
end
end
protected
def output_component_name
dir = @dest + output_component_dir
name = output_component_base
FileUtils.mkdir_p(dir.to_s, :mode => 0755)
filename = dir + "#{name}.html"
[dir, name, filename]
end
def mode
self.class.to_s.split(/::/).last.downcase
end
def cookie_name; ''; end
def cookie_mail; ''; end
def load_plugins
result = super
@plugin.instance_eval(<<-EOS, __FILE__, __LINE__ + 1)
def anchor( s )
case s
when /\\A(\\d+)#?([pct]\\d*)?\\z/
day = $1
anchor = $2
if /\\A(\\d{4})(\\d{2})(\\d{2})?\\z/ =~ day
day = [$1, $2, $3].compact
day = day.collect {|component| component.to_i.to_s}
day = day.join("/")
end
if anchor then
"\#{day}.html#\#{anchor}"
else
"\#{day}.html"
end
when /\\A(\\d{8})-\\d+\\z/
@conf['latest.path'][$1]
else
""
end
end
def category_anchor(category)
href = "category/\#{u category}.html"
if @category_icon[category] and !@conf.mobile_agent?
%Q|<a href="\#{href}"><img class="category" src="\#{h @category_icon_url}\#{h @category_icon[category]}" alt="\#{h category}"></a>|
else
%Q|[<a href="\#{href}">\#{h category}</a>]|
end
end
def navi_admin
""
end
@image_dir = #{@image_dest_dir.to_s.dump}
@image_url = "#{@conf.base_url}#{@image_dest_dir.basename}"
EOS
result
end
private
def setup_cgi(cgi, conf)
end
end
class Day < TDiary::TDiaryDay
include Base
def initialize(diary, dest, conf)
@target_date = diary.date
@target_diaries = {@target_date.strftime("%Y%m%d") => diary}
super("day.rhtml", dest, conf)
end
def can_save?
not @diary.nil?
end
def output_filename
dir, name, filename = output_component_name
filename
end
def [](date)
@target_diaries[date.strftime("%Y%m%d")] or super
end
def relative_path
"../../"
end
private
def output_component_dir
Pathname(@target_date.strftime("%Y")) + @target_date.month.to_s
end
def output_component_base
@target_date.day.to_s
end
def setup_cgi(cgi, conf)
super
cgi.params["date"] = [@target_date.strftime("%Y%m%d")]
end
end
class Month < TDiary::TDiaryMonth
include Base
def initialize(date, dest, conf)
@target_date = date
super("month.rhtml", dest, conf)
end
def can_save?
not @diary.nil?
end
def output_filename
dir, name, filename = output_component_name
filename
end
def relative_path
"../"
end
private
def output_component_dir
@target_date.strftime("%Y")
end
def output_component_base
@target_date.month.to_s
end
private
def setup_cgi(cgi, conf)
super
cgi.params["date"] = [@target_date.strftime("%Y%m")]
end
end
class Category < TDiary::TDiaryView
include Base
def initialize(category, diaries, dest, conf)
@category = category
diaries = diaries.reject {|date, diary| !diary.visible?}
_, diary = diaries.sort_by {|date, diary| diary.last_modified}.last
@target_date = diary.date
super("latest.rhtml", dest, conf)
@diaries = diaries
@diary = diary
end
def can_save?
not @diary.nil?
end
def output_filename
category_dir = @dest + "category"
category_dir.mkpath
category_dir + "#{@category}.html"
end
def relative_path
"../"
end
def latest(limit=5)
@diaries.keys.sort.reverse_each do |date|
diary = @diaries[date]
yield(diary)
end
end
protected
def setup_cgi(cgi, conf)
super
cgi.params["date"] = [@target_date.strftime("%Y%m")]
end
end
class Latest < TDiary::TDiaryLatest
include Base
def initialize(date, index, dest, conf)
@target_date = date
@index = index
super("latest.rhtml", dest, conf)
end
def relative_path
if @index.zero?
""
else
"../"
end
end
def can_save?
true
end
def output_filename
if @index.zero?
@dest + "index.html"
else
latest_dir = @dest + "latest"
FileUtils.mkdir_p(latest_dir.to_s, :mode => 0755)
latest_dir + "#{@index}.html"
end
end
protected
def setup_cgi(cgi, conf)
super
return if @index.zero?
date = @target_date.strftime("%Y%m%d") + "-#{conf.latest_limit}"
cgi.params["date"] = [date]
end
end
class RSS < TDiary::TDiaryLatest
include Base
def initialize(dest, conf)
super("latest.rhtml", dest, conf)
end
def mode
"latest"
end
def relative_path
""
end
def can_save?
true
end
def output_filename
@dest + output_base_name
end
def output_base_name
"index.rdf"
end
def do_eval_rhtml(prefix)
load_plugins
make_rss
end
private
def make_rss
base_uri = @conf['html_archiver.base_url'] || @conf.base_url
rss_uri = base_uri + output_base_name
@conf.options['apply_plugin'] = true
feed = ::RSS::Maker.make("1.0") do |maker|
setup_channel(maker.channel, rss_uri, base_uri)
setup_image(maker.image, base_uri)
@diaries.keys.sort.reverse[0, 15].each do |date|
diary = @diaries[date]
maker.items.new_item do |item|
setup_item(item, diary, base_uri)
end
end
end
feed.to_s
end
def setup_channel(channel, rss_uri, base_uri)
channel.about = rss_uri
channel.link = base_uri
channel.title = @conf.html_title
channel.description = @conf.description
channel.dc_creator = @conf.author_name
channel.dc_rights = @conf.copyright
end
def setup_image(image, base_uri)
return if @conf.banner.nil?
return if @conf.banner.empty?
if /^http/ =~ @conf.banner
rdf_image = @conf.banner
else
rdf_image = base_uri + @conf.banner
end
maker.image.url = rdf_image
maker.image.title = @conf.html_title
maker.link = base_uri
end
def setup_item(item, diary, base_uri)
section = nil
diary.each_section do |_section|
section = _section
break if section
end
return if section.nil?
item.link = base_uri + @plugin.anchor(diary.date.strftime("%Y%m%d"))
item.dc_date = diary.last_modified
@plugin.instance_variable_set("@makerss_in_feed", true)
subtitle = section.subtitle_to_html
body_enter = @plugin.send(:body_enter_proc, diary.date)
body = @plugin.send(:apply_plugin, section.body_to_html)
body_leave = @plugin.send(:body_leave_proc, diary.date)
@plugin.instance_variable_set("@makerss_in_feed", false)
subtitle = @plugin.send(:apply_plugin, subtitle, true).strip
subtitle.sub!(/^(\[([^\]]+)\])+ */, '')
description = @plugin.send(:remove_tag, body).strip
subtitle = @conf.shorten(description, 20) if subtitle.empty?
item.title = subtitle
item.description = description
item.content_encoded = body
item.dc_creator = @conf.author_name
section.categories.each do |category|
item.dc_subjects.new_subject do |subject|
subject.content = category
end
end
end
end
class Main < TDiary::TDiaryBase
include Image
def initialize(cgi, dest, conf, src=nil)
super(cgi, nil, conf)
calendar
@dest = dest
@src = src || './'
init_image_dir
end
def run
@date = Time.now
load_plugins
copy_images
all_days = archive_days
archive_categories
archive_latest(all_days)
make_rss
copy_theme
end
private
def copy_images
image_src_dir = @plugin.instance_variable_get("@image_dir")
image_src_dir = Pathname(image_src_dir)
unless image_src_dir.absolute?
image_src_dir = Pathname(@src) + image_src_dir
end
@image_dest_dir.rmtree if @image_dest_dir.exist?
if image_src_dir.exist?
FileUtils.cp_r(image_src_dir.to_s, @image_dest_dir.to_s)
end
end
def archive_days
all_days = []
@years.keys.sort.each do |year|
@years[year].sort.each do |month|
month_time = Time.local(year.to_i, month.to_i)
month = Month.new(month_time, @dest, conf)
month.save
month.send(:each_day) do |diary|
all_days << diary.date
Day.new(diary, @dest, conf).save
end
end
end
all_days
end
def archive_categories
cache = @plugin.instance_variable_get("@category_cache")
cache.categorize([], @years).each do |category, diaries|
categorized_diaries = {}
diaries.keys.each do |date|
date_time = Time.local(*date.scan(/^(\d{4})(\d\d)(\d\d)$/)[0])
@io.transaction(date_time) do |diaries|
categorized_diaries[date] = diaries[date]
DIRTY_NONE
end
end
Category.new(category, categorized_diaries, @dest, conf).save
end
end
def archive_latest(all_days)
conf["latest.path"] = {}
latest_days = []
all_days.reverse.each_slice(conf.latest_limit) do |days|
latest_days << days
end
latest_days.each_with_index do |days, i|
date = days.first.strftime("%Y%m%d")
if i.zero?
latest_path = "./"
else
latest_path = "latest/#{i}.html"
end
conf["latest.path"][date] = latest_path
end
latest_days.each_with_index do |days, i|
latest = Latest.new(days.first, i, @dest, conf)
latest.save
conf["ndays.prev"] = nil
conf["ndays.next"] = nil
end
end
def make_rss
RSS.new(@dest, conf).save
end
def copy_theme
theme_dir = @dest + "theme"
theme_dir.rmtree if theme_dir.exist?
theme_dir.mkpath
tdiary_theme_dir = Pathname(File.join(TDiary::PATH, "theme"))
FileUtils.cp((tdiary_theme_dir + "base.css").to_s, theme_dir.to_s)
if @conf.theme
FileUtils.cp_r((tdiary_theme_dir + @conf.theme).to_s,
(theme_dir + @conf.theme).to_s)
end
end
end
end
cgi = HTMLArchiver::CGI.new
conf = TDiary::Config.new(cgi)
conf.show_comment = true
conf.hide_comment_form = true
def conf.bot?; false; end
output_dir ||= Pathname(conf.data_path) + "cache" + "html"
output_dir = Pathname(output_dir).expand_path
output_dir.mkpath
HTMLArchiver::Main.new(cgi, output_dir, conf, options.conf_dir).run
<p>tDiaryをローカルなネットワークに配置して、tDiaryが表示する内容を静的なHTMLとして公開したい場合はよくありますよね。ククログもそんなよくある使い方の1つです。</p>
<p>tDiaryには静的なHTMLを生成するための<a href="http://docs.tdiary.org/ja/?squeeze.rb">squeezeプラグイン</a>がありますが、squeezeプラグインが出力するHTMLは以下の点でCGIで表示される内容と異なります。</p>
<ul>
<li>各日付のページしか生成しない
<ul>
<li>最新の日記n件ページや月別ページやカテゴリページは生成しない</li>
</ul></li>
<li>リンクがCGI用のリンクのままで、次の日記のページに移動するリンクが壊れている</li>
<li>テーマファイルや画像はコピーしてくれないので、生成したHTMLの入ったディレクトリ以下だけでは完結しない</li>
</ul>
<p>ただし、これはsqueezeプラグインが検索エンジンへの入力データとしてのHTML生成を目的としているためです。よくある使い方では、生成されたHTMLはCGIで出力されているように表示できることが目的なので、上記のようなミスマッチが発生します。</p>
<p>そこで、ククログではhtml-archiver.rbという静的なHTMLを生成するスクリプトを使っています。html-archiver.rbは最後の方に載せています。</p>
<h4>html-archiver.rbの使い方</h4>
<p>html-archiver.rbを使うと、CGIで出力されている内容と同じように表示されるHTMLが生成されます。生成例は今見ているこのページです。</p>
<p>使い方はこうなります。</p>
<pre>% ruby html-archiver.rb --tdiary tdiayr.rbのあるディレクトリ --conf tdiary.confのあるディレクトリ 出力先ディレクトリ</pre>
<p>例えば、以下のような場合を考えます。</p>
<ul>
<li>tdiary.rbは~tdiary/work/ruby/tdiary/core/にある</li>
<li>tdiary.confは~tdiary/public_html/にある</li>
<li>HTMLは~tdiary/public_html/html/以下に出力する</li>
</ul>
<p>この場合はこのようなコマンドになります。</p>
<pre>% ruby html-archiver.rb --tdiary ~tdiary/work/ruby/tdiary/core/ --conf ~tdiary/public_html/ ~tdiary/public_html/html/</pre>
<h4>機能</h4>
<ul>
<li>日付ページの生成: <a href="http://www.clear-code.com/blog/2008/12/5.html">例</a></li>
<li>最新n件ページの生成: <a href="http://www.clear-code.com/blog/">例</a></li>
<li>月別ページの生成: <a href="http://www.clear-code.com/blog/2008/12.html">例</a></li>
<li>RSS 1.0の生成: <a href="http://www.clear-code.com/blog/index.rdf">例</a></li>
<li>テーマファイルのコピー</li>
<li>画像のコピー</li>
</ul>
<h4>制限</h4>
<ul>
<li>ツッコミが生成されるかどうかは試していない</li>
<li>カテゴリ一覧ページがきちんと生成されるかは(最近は)試していない</li>
<li>タブインデント(tDiary本体のコーディングスタイルに合わせているため)</li>
<li>思ったほど使う場面が少ないかもしれない(もしかしたら、tDiaryが表示する内容を静的なHTMLとして公開することがそんなにないかもしれない)</li>
</ul>
<h4>ライセンス</h4>
<p>GPL3あるいは3以降の新しいバージョンのGPL</p>
<h4>html-archiver.rb</h4>
<div class="code"><div class="code"><!-- Generator: GNU source-highlight 2.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt><span style="font-style: italic"><span style="color: #9A1900">#!/usr/bin/env ruby</span></span>
<span style="font-style: italic"><span style="color: #9A1900"># -*- coding: utf-8; ruby-indent-level: 3; tab-width: 3; indent-tabs-mode: t -*-</span></span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'uri'</span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'cgi'</span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'fileutils'</span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'pathname'</span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'optparse'</span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'ostruct'</span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'enumerator'</span>
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">'rss'</span>
options = OpenStruct.new
options.tdiary_path = <span style="color: #BB0000">"./"</span>
options.conf_dir = <span style="color: #BB0000">"./"</span>
opts = OptionParser.new <span style="color: #B05000">do</span> |opts|
opts.banner += <span style="color: #BB0000">" OUTPUT_DIR"</span>
opts.on(<span style="color: #BB0000">"-t"</span>, <span style="color: #BB0000">"--tdiary=TDIARY_DIRECTORY"</span>,
<span style="color: #BB0000">"a directory that has tdiary.rb"</span>) <span style="color: #B05000">do</span> |path|
options.tdiary_path = path
<span style="color: #B05000">end</span>
opts.on(<span style="color: #BB0000">"-c"</span>, <span style="color: #BB0000">"--conf=TDIARY_CONF"</span>, <span style="color: #BB0000">"a path of tdiary.conf"</span>) <span style="color: #B05000">do</span> |conf|
options.conf_dir = conf
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
opts.parse!
output_dir = ARGV.shift
Dir.chdir(options.conf_dir) <span style="color: #B05000">do</span>
<span style="color: #009900">$LOAD_PATH</span>.unshift(File.expand_path(options.tdiary_path))
<span style="color: #AA22AA">require</span> <span style="color: #BB0000">"tdiary"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">module</span> HTMLArchiver
<span style="color: #B05000">class</span> CGI < ::CGI
<span style="color: #B05000">def</span> referer
<span style="color: #B05000">nil</span>
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> env_table
<span style="color: #FFBB00">{</span><span style="color: #BB0000">"REQUEST_METHOD"</span> => <span style="color: #BB0000">"GET"</span>, <span style="color: #BB0000">"QUERY_STRING"</span> => <span style="color: #BB0000">""</span><span style="color: #FFBB00">}</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">module</span> Image
<span style="color: #B05000">def</span> init_image_dir
<span style="color: #009900">@image_dest_dir</span> = <span style="color: #009900">@dest</span> + <span style="color: #BB0000">"images"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">module</span> Base
<span style="color: #B05000">include</span> Image
<span style="color: #B05000">def</span> initialize(rhtml, dest, conf)
<span style="color: #009900">@ignore_parser_cache</span> = <span style="color: #B05000">true</span>
cgi = CGI.new
setup_cgi(cgi, conf)
<span style="color: #009900">@dest</span> = dest
init_image_dir
<span style="color: #B05000">super</span>(cgi, rhtml, conf)
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> eval_rhtml(*args)
link_detect_re = /(<(?:a|link)\b.*?\bhref|<img\b.*?\bsrc)="(.*?)"/
<span style="color: #B05000">super</span>.gsub(link_detect_re) <span style="color: #B05000">do</span> |link_attribute|
prefix = <span style="color: #009900">$1</span>
link = <span style="color: #009900">$2</span>
uri = URI(link)
<span style="color: #B05000">if</span> uri.absolute? <span style="color: #B05000">or</span> link[<span style="color: #BB0000">0</span>] == ?/
link_attribute
<span style="color: #B05000">else</span>
%Q[#<span style="color: #FFBB00">{</span>prefix<span style="color: #FFBB00">}</span>=<span style="color: #BB0000">"#{relative_path}#{link}"</span>]
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> save
<span style="color: #B05000">return</span> <span style="color: #B05000">unless</span> can_save?
filename = output_filename
<span style="color: #B05000">if</span> !filename.exist? <span style="color: #B05000">or</span> filename.mtime != last_modified
filename.open(<span style="color: #BB0000">'w'</span>) <span style="color: #FFBB00">{</span>|f| f.print(eval_rhtml)<span style="color: #FFBB00">}</span>
filename.utime(last_modified, last_modified)
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
protected
<span style="color: #B05000">def</span> output_component_name
dir = <span style="color: #009900">@dest</span> + output_component_dir
name = output_component_base
FileUtils.mkdir_p(dir.to_s, :mode => <span style="color: #BB0000">0755</span>)
filename = dir + <span style="color: #BB0000">"#{name}.html"</span>
[dir, name, filename]
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> mode
<span style="color: #B05000">self</span>.<span style="color: #B05000">class</span>.to_s.split(/::/).last.downcase
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> cookie_name; <span style="color: #BB0000">''</span>; <span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> cookie_mail; <span style="color: #BB0000">''</span>; <span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> load_plugins
result = <span style="color: #B05000">super</span>
<span style="color: #009900">@plugin</span>.instance_eval(<<-EOS, <span style="color: #B05000">__FILE__</span>, <span style="color: #B05000">__LINE__</span> + <span style="color: #BB0000">1</span>)
<span style="color: #B05000">def</span> anchor( s )
<span style="color: #B05000">case</span> s
<span style="color: #B05000">when</span> /\\A(\\d+)#?([pct]\\d*)?\\z/
day = <span style="color: #009900">$1</span>
anchor = <span style="color: #009900">$2</span>
<span style="color: #B05000">if</span> /\\A(\\d{4})(\\d{2})(\\d{2})?\\z/ =~ day
day = [<span style="color: #009900">$1</span>, <span style="color: #009900">$2</span>, <span style="color: #009900">$3</span>].compact
day = day.collect <span style="color: #FFBB00">{</span>|component| component.to_i.to_s<span style="color: #FFBB00">}</span>
day = day.join(<span style="color: #BB0000">"/"</span>)
<span style="color: #B05000">end</span>
<span style="color: #B05000">if</span> anchor <span style="color: #B05000">then</span>
<span style="color: #BB0000">"\#{day}.html#\#{anchor}"</span>
<span style="color: #B05000">else</span>
<span style="color: #BB0000">"\#{day}.html"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">when</span> /\\A(\\d{8})-\\d+\\z/
<span style="color: #009900">@conf</span>[<span style="color: #BB0000">'latest.path'</span>][<span style="color: #009900">$1</span>]
<span style="color: #B05000">else</span>
<span style="color: #BB0000">""</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> category_anchor(category)
href = <span style="color: #BB0000">"category/\#{u category}.html"</span>
<span style="color: #B05000">if</span> <span style="color: #009900">@category_icon</span>[category] <span style="color: #B05000">and</span> !<span style="color: #009900">@conf</span>.mobile_agent?
%Q|<span style="color: #BB0000"><a href="\#{href}"><img class="category" src="\#{h @category_icon_url}\#{h @category_icon[category]}" alt="\#{h category}"></a></span>|
<span style="color: #B05000">else</span>
%Q|[<span style="color: #BB0000"><a href="\#{href}"></span>\#<span style="color: #FFBB00">{</span>h category<span style="color: #FFBB00">}</span><span style="color: #BB0000"></a></span>]|
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> navi_admin
<span style="color: #BB0000">""</span>
<span style="color: #B05000">end</span>
<span style="color: #009900">@image_dir</span> = #<span style="color: #FFBB00">{</span><span style="color: #009900">@image_dest_dir</span>.to_s.dump<span style="color: #FFBB00">}</span>
<span style="color: #009900">@image_url</span> = <span style="color: #BB0000">"#{@conf.base_url}#{@image_dest_dir.basename}"</span>
EOS
result
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> setup_cgi(cgi, conf)
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">class</span> Day < TDiary::TDiaryDay
<span style="color: #B05000">include</span> Base
<span style="color: #B05000">def</span> initialize(diary, dest, conf)
<span style="color: #009900">@target_date</span> = diary.date
<span style="color: #009900">@target_diaries</span> = <span style="color: #FFBB00">{</span><span style="color: #009900">@target_date</span>.strftime(<span style="color: #BB0000">"%Y%m%d"</span>) => diary<span style="color: #FFBB00">}</span>
<span style="color: #B05000">super</span>(<span style="color: #BB0000">"day.rhtml"</span>, dest, conf)
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> can_save?
<span style="color: #B05000">not</span> <span style="color: #009900">@diary</span>.<span style="color: #B05000">nil</span>?
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_filename
dir, name, filename = output_component_name
filename
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> [](date)
<span style="color: #009900">@target_diaries</span>[date.strftime(<span style="color: #BB0000">"%Y%m%d"</span>)] <span style="color: #B05000">or</span> <span style="color: #B05000">super</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> relative_path
<span style="color: #BB0000">"../../"</span>
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> output_component_dir
Pathname(<span style="color: #009900">@target_date</span>.strftime(<span style="color: #BB0000">"%Y"</span>)) + <span style="color: #009900">@target_date</span>.month.to_s
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_component_base
<span style="color: #009900">@target_date</span>.day.to_s
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> setup_cgi(cgi, conf)
<span style="color: #B05000">super</span>
cgi.params[<span style="color: #BB0000">"date"</span>] = [<span style="color: #009900">@target_date</span>.strftime(<span style="color: #BB0000">"%Y%m%d"</span>)]
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">class</span> Month < TDiary::TDiaryMonth
<span style="color: #B05000">include</span> Base
<span style="color: #B05000">def</span> initialize(date, dest, conf)
<span style="color: #009900">@target_date</span> = date
<span style="color: #B05000">super</span>(<span style="color: #BB0000">"month.rhtml"</span>, dest, conf)
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> can_save?
<span style="color: #B05000">not</span> <span style="color: #009900">@diary</span>.<span style="color: #B05000">nil</span>?
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_filename
dir, name, filename = output_component_name
filename
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> relative_path
<span style="color: #BB0000">"../"</span>
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> output_component_dir
<span style="color: #009900">@target_date</span>.strftime(<span style="color: #BB0000">"%Y"</span>)
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_component_base
<span style="color: #009900">@target_date</span>.month.to_s
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> setup_cgi(cgi, conf)
<span style="color: #B05000">super</span>
cgi.params[<span style="color: #BB0000">"date"</span>] = [<span style="color: #009900">@target_date</span>.strftime(<span style="color: #BB0000">"%Y%m"</span>)]
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">class</span> Category < TDiary::TDiaryView
<span style="color: #B05000">include</span> Base
<span style="color: #B05000">def</span> initialize(category, diaries, dest, conf)
<span style="color: #009900">@category</span> = category
diaries = diaries.reject <span style="color: #FFBB00">{</span>|date, diary| !diary.visible?<span style="color: #FFBB00">}</span>
_, diary = diaries.sort_by <span style="color: #FFBB00">{</span>|date, diary| diary.last_modified<span style="color: #FFBB00">}</span>.last
<span style="color: #009900">@target_date</span> = diary.date
<span style="color: #B05000">super</span>(<span style="color: #BB0000">"latest.rhtml"</span>, dest, conf)
<span style="color: #009900">@diaries</span> = diaries
<span style="color: #009900">@diary</span> = diary
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> can_save?
<span style="color: #B05000">not</span> <span style="color: #009900">@diary</span>.<span style="color: #B05000">nil</span>?
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_filename
category_dir = <span style="color: #009900">@dest</span> + <span style="color: #BB0000">"category"</span>
category_dir.mkpath
category_dir + <span style="color: #BB0000">"#{@category}.html"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> relative_path
<span style="color: #BB0000">"../"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> latest(limit=<span style="color: #BB0000">5</span>)
<span style="color: #009900">@diaries</span>.keys.sort.reverse_each <span style="color: #B05000">do</span> |date|
diary = <span style="color: #009900">@diaries</span>[date]
<span style="color: #B05000">yield</span>(diary)
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
protected
<span style="color: #B05000">def</span> setup_cgi(cgi, conf)
<span style="color: #B05000">super</span>
cgi.params[<span style="color: #BB0000">"date"</span>] = [<span style="color: #009900">@target_date</span>.strftime(<span style="color: #BB0000">"%Y%m"</span>)]
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">class</span> Latest < TDiary::TDiaryLatest
<span style="color: #B05000">include</span> Base
<span style="color: #B05000">def</span> initialize(date, index, dest, conf)
<span style="color: #009900">@target_date</span> = date
<span style="color: #009900">@index</span> = index
<span style="color: #B05000">super</span>(<span style="color: #BB0000">"latest.rhtml"</span>, dest, conf)
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> relative_path
<span style="color: #B05000">if</span> <span style="color: #009900">@index</span>.zero?
<span style="color: #BB0000">""</span>
<span style="color: #B05000">else</span>
<span style="color: #BB0000">"../"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> can_save?
<span style="color: #B05000">true</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_filename
<span style="color: #B05000">if</span> <span style="color: #009900">@index</span>.zero?
<span style="color: #009900">@dest</span> + <span style="color: #BB0000">"index.html"</span>
<span style="color: #B05000">else</span>
latest_dir = <span style="color: #009900">@dest</span> + <span style="color: #BB0000">"latest"</span>
FileUtils.mkdir_p(latest_dir.to_s, :mode => <span style="color: #BB0000">0755</span>)
latest_dir + <span style="color: #BB0000">"#{@index}.html"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
protected
<span style="color: #B05000">def</span> setup_cgi(cgi, conf)
<span style="color: #B05000">super</span>
<span style="color: #B05000">return</span> <span style="color: #B05000">if</span> <span style="color: #009900">@index</span>.zero?
date = <span style="color: #009900">@target_date</span>.strftime(<span style="color: #BB0000">"%Y%m%d"</span>) + <span style="color: #BB0000">"-#{conf.latest_limit}"</span>
cgi.params[<span style="color: #BB0000">"date"</span>] = [date]
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">class</span> RSS < TDiary::TDiaryLatest
<span style="color: #B05000">include</span> Base
<span style="color: #B05000">def</span> initialize(dest, conf)
<span style="color: #B05000">super</span>(<span style="color: #BB0000">"latest.rhtml"</span>, dest, conf)
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> mode
<span style="color: #BB0000">"latest"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> relative_path
<span style="color: #BB0000">""</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> can_save?
<span style="color: #B05000">true</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_filename
<span style="color: #009900">@dest</span> + output_base_name
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> output_base_name
<span style="color: #BB0000">"index.rdf"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> do_eval_rhtml(prefix)
load_plugins
make_rss
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> make_rss
base_uri = <span style="color: #009900">@conf</span>[<span style="color: #BB0000">'html_archiver.base_url'</span>] || <span style="color: #009900">@conf</span>.base_url
rss_uri = base_uri + output_base_name
<span style="color: #009900">@conf</span>.options[<span style="color: #BB0000">'apply_plugin'</span>] = <span style="color: #B05000">true</span>
feed = ::RSS::Maker.make(<span style="color: #BB0000">"1.0"</span>) <span style="color: #B05000">do</span> |maker|
setup_channel(maker.channel, rss_uri, base_uri)
setup_image(maker.image, base_uri)
<span style="color: #009900">@diaries</span>.keys.sort.reverse[<span style="color: #BB0000">0</span>, <span style="color: #BB0000">15</span>].each <span style="color: #B05000">do</span> |date|
diary = <span style="color: #009900">@diaries</span>[date]
maker.items.new_item <span style="color: #B05000">do</span> |item|
setup_item(item, diary, base_uri)
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
feed.to_s
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> setup_channel(channel, rss_uri, base_uri)
channel.about = rss_uri
channel.link = base_uri
channel.title = <span style="color: #009900">@conf</span>.html_title
channel.description = <span style="color: #009900">@conf</span>.description
channel.dc_creator = <span style="color: #009900">@conf</span>.author_name
channel.dc_rights = <span style="color: #009900">@conf</span>.copyright
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> setup_image(image, base_uri)
<span style="color: #B05000">return</span> <span style="color: #B05000">if</span> <span style="color: #009900">@conf</span>.banner.<span style="color: #B05000">nil</span>?
<span style="color: #B05000">return</span> <span style="color: #B05000">if</span> <span style="color: #009900">@conf</span>.banner.empty?
<span style="color: #B05000">if</span> /^http/ =~ <span style="color: #009900">@conf</span>.banner
rdf_image = <span style="color: #009900">@conf</span>.banner
<span style="color: #B05000">else</span>
rdf_image = base_uri + <span style="color: #009900">@conf</span>.banner
<span style="color: #B05000">end</span>
maker.image.url = rdf_image
maker.image.title = <span style="color: #009900">@conf</span>.html_title
maker.link = base_uri
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> setup_item(item, diary, base_uri)
section = <span style="color: #B05000">nil</span>
diary.each_section <span style="color: #B05000">do</span> |_section|
section = _section
<span style="color: #B05000">break</span> <span style="color: #B05000">if</span> section
<span style="color: #B05000">end</span>
<span style="color: #B05000">return</span> <span style="color: #B05000">if</span> section.<span style="color: #B05000">nil</span>?
item.link = base_uri + <span style="color: #009900">@plugin</span>.anchor(diary.date.strftime(<span style="color: #BB0000">"%Y%m%d"</span>))
item.dc_date = diary.last_modified
<span style="color: #009900">@plugin</span>.instance_variable_set(<span style="color: #BB0000">"@makerss_in_feed"</span>, <span style="color: #B05000">true</span>)
subtitle = section.subtitle_to_html
body_enter = <span style="color: #009900">@plugin</span>.send(:body_enter_proc, diary.date)
body = <span style="color: #009900">@plugin</span>.send(:apply_plugin, section.body_to_html)
body_leave = <span style="color: #009900">@plugin</span>.send(:body_leave_proc, diary.date)
<span style="color: #009900">@plugin</span>.instance_variable_set(<span style="color: #BB0000">"@makerss_in_feed"</span>, <span style="color: #B05000">false</span>)
subtitle = <span style="color: #009900">@plugin</span>.send(:apply_plugin, subtitle, <span style="color: #B05000">true</span>).strip
subtitle.sub!(/^(\[([^\]]+)\])+ */, <span style="color: #BB0000">''</span>)
description = <span style="color: #009900">@plugin</span>.send(:remove_tag, body).strip
subtitle = <span style="color: #009900">@conf</span>.shorten(description, <span style="color: #BB0000">20</span>) <span style="color: #B05000">if</span> subtitle.empty?
item.title = subtitle
item.description = description
item.content_encoded = body
item.dc_creator = <span style="color: #009900">@conf</span>.author_name
section.categories.each <span style="color: #B05000">do</span> |category|
item.dc_subjects.new_subject <span style="color: #B05000">do</span> |subject|
subject.content = category
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">class</span> Main < TDiary::TDiaryBase
<span style="color: #B05000">include</span> Image
<span style="color: #B05000">def</span> initialize(cgi, dest, conf, src=<span style="color: #B05000">nil</span>)
<span style="color: #B05000">super</span>(cgi, <span style="color: #B05000">nil</span>, conf)
calendar
<span style="color: #009900">@dest</span> = dest
<span style="color: #009900">@src</span> = src || <span style="color: #BB0000">'./'</span>
init_image_dir
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> run
<span style="color: #009900">@date</span> = Time.now
load_plugins
copy_images
all_days = archive_days
archive_categories
archive_latest(all_days)
make_rss
copy_theme
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> copy_images
image_src_dir = <span style="color: #009900">@plugin</span>.instance_variable_get(<span style="color: #BB0000">"@image_dir"</span>)
image_src_dir = Pathname(image_src_dir)
<span style="color: #B05000">unless</span> image_src_dir.absolute?
image_src_dir = Pathname(<span style="color: #009900">@src</span>) + image_src_dir
<span style="color: #B05000">end</span>
<span style="color: #009900">@image_dest_dir</span>.rmtree <span style="color: #B05000">if</span> <span style="color: #009900">@image_dest_dir</span>.exist?
<span style="color: #B05000">if</span> image_src_dir.exist?
FileUtils.cp_r(image_src_dir.to_s, <span style="color: #009900">@image_dest_dir</span>.to_s)
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> archive_days
all_days = []
<span style="color: #009900">@years</span>.keys.sort.each <span style="color: #B05000">do</span> |year|
<span style="color: #009900">@years</span>[year].sort.each <span style="color: #B05000">do</span> |month|
month_time = Time.local(year.to_i, month.to_i)
month = Month.new(month_time, <span style="color: #009900">@dest</span>, conf)
month.save
month.send(:each_day) <span style="color: #B05000">do</span> |diary|
all_days << diary.date
Day.new(diary, <span style="color: #009900">@dest</span>, conf).save
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
all_days
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> archive_categories
cache = <span style="color: #009900">@plugin</span>.instance_variable_get(<span style="color: #BB0000">"@category_cache"</span>)
cache.categorize([], <span style="color: #009900">@years</span>).each <span style="color: #B05000">do</span> |category, diaries|
categorized_diaries = <span style="color: #FFBB00">{}</span>
diaries.keys.each <span style="color: #B05000">do</span> |date|
date_time = Time.local(*date.scan(/^(\d{4})(\d\d)(\d\d)$/)[<span style="color: #BB0000">0</span>])
<span style="color: #009900">@io</span>.transaction(date_time) <span style="color: #B05000">do</span> |diaries|
categorized_diaries[date] = diaries[date]
DIRTY_NONE
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
Category.new(category, categorized_diaries, <span style="color: #009900">@dest</span>, conf).save
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> archive_latest(all_days)
conf[<span style="color: #BB0000">"latest.path"</span>] = <span style="color: #FFBB00">{}</span>
latest_days = []
all_days.reverse.each_slice(conf.latest_limit) <span style="color: #B05000">do</span> |days|
latest_days << days
<span style="color: #B05000">end</span>
latest_days.each_with_index <span style="color: #B05000">do</span> |days, i|
date = days.first.strftime(<span style="color: #BB0000">"%Y%m%d"</span>)
<span style="color: #B05000">if</span> i.zero?
latest_path = <span style="color: #BB0000">"./"</span>
<span style="color: #B05000">else</span>
latest_path = <span style="color: #BB0000">"latest/#{i}.html"</span>
<span style="color: #B05000">end</span>
conf[<span style="color: #BB0000">"latest.path"</span>][date] = latest_path
<span style="color: #B05000">end</span>
latest_days.each_with_index <span style="color: #B05000">do</span> |days, i|
latest = Latest.new(days.first, i, <span style="color: #009900">@dest</span>, conf)
latest.save
conf[<span style="color: #BB0000">"ndays.prev"</span>] = <span style="color: #B05000">nil</span>
conf[<span style="color: #BB0000">"ndays.next"</span>] = <span style="color: #B05000">nil</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> make_rss
RSS.new(<span style="color: #009900">@dest</span>, conf).save
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> copy_theme
theme_dir = <span style="color: #009900">@dest</span> + <span style="color: #BB0000">"theme"</span>
theme_dir.rmtree <span style="color: #B05000">if</span> theme_dir.exist?
theme_dir.mkpath
tdiary_theme_dir = Pathname(File.join(TDiary::PATH, <span style="color: #BB0000">"theme"</span>))
FileUtils.cp((tdiary_theme_dir + <span style="color: #BB0000">"base.css"</span>).to_s, theme_dir.to_s)
<span style="color: #B05000">if</span> <span style="color: #009900">@conf</span>.theme
FileUtils.cp_r((tdiary_theme_dir + <span style="color: #009900">@conf</span>.theme).to_s,
(theme_dir + <span style="color: #009900">@conf</span>.theme).to_s)
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
cgi = HTMLArchiver::CGI.new
conf = TDiary::Config.new(cgi)
conf.show_comment = <span style="color: #B05000">true</span>
conf.hide_comment_form = <span style="color: #B05000">true</span>
<span style="color: #B05000">def</span> conf.bot?; <span style="color: #B05000">false</span>; <span style="color: #B05000">end</span>
output_dir ||= Pathname(conf.data_path) + <span style="color: #BB0000">"cache"</span> + <span style="color: #BB0000">"html"</span>
output_dir = Pathname(output_dir).expand_path
output_dir.mkpath
HTMLArchiver::Main.new(cgi, output_dir, conf, options.conf_dir).run</tt></pre>
</div></div>
ClearCode Inc.
2008-12-05T10:18:02+09:00
-
UxU(UnitTest.XUL)を利用したFirefoxアドオンのデバッグの例
http://www.clear-code.com/blog/2008/11/17.html
Firefoxアドオン開発者向け自動テストツールのUxUは、新たに発見したバグの修正にも活用することができます。本日リリースされたXUL/Migemo バージョン0.11.7で行われた修正の場合を例に、実際のデバッグ作業の流れを解説します。
状況
XUL/Migemoは、Firefoxで表示しているページ内のテキストを検索する機能を提供するアドオンですが、検索を開始する際に、「現在のスクロール位置から検索を開始する」という処理を含んでいます。0.11.6以前では、この機能を使用している時に、ページ先頭から検索が始まるべき場面で、先頭以外の場所から検索が始まってしまうことがあるという問題が起こっていました。
再現条件の特定
いくつか条件を変えて調査した結果、スクロールが発生しているページでは期待通りの結果になっているのに対して、スクロールが全く発生していないページ(ページ全体がウィンドウの現在の大きさの中に収まっているページ)では期待と異なる結果になっていることが判明しました。
原因箇所の特定
前述の処理の肝となっているのは、pXMigemoFindクラスのfindFirstVisibleNode()というメソッドです。このメソッドは、渡されたフレーム(DOMWindow)において現在のスクロール位置で見えている最初の要素を検索して返す物です。このメソッドの戻り値を確認した所、前述の条件下では戻り値が期待と異なっている事が判明しました。
このことから、今回主な修正対象になるのはこのfindFirstVisibleNode()というメソッドであるということになります。
テストケースの作成
上記メソッドの実装を見直す前に、UxU用のテストケースを作成します。これにより、これから行う修正で目指すべきゴールが明確になります。つまり、このテストが成功する状況まで持って行くことが、今回の修正のゴールとなります。
テストケースはJavaScriptのコードだけで完結する場合もありますが、今回のような場合は実際のWebページを使ってテストを行う必要があります。そこで、問題が発生する条件と発生しない条件の両方の事例としてHTMLドキュメントを用意します。
スクロールが発生しないページ(shortPage.html):ページの内容が短いため、スクロールが発生しません。
スクロールが発生するページ(longPage.html):ページの内容がある程度長いため、ウィンドウサイズによってはスクロールが発生します。
これらのドキュメントを使い、shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する、という条件の下でテストを行うテストケースをこれから作成することになります。
ところで、現在の実装で問題が起こっている場合だけでなく、すでに正常に動いている場合の事例も同時に作成していることに気がついたでしょうか? 両方の場合を常にテストすることで、「ある問題を修正したら、今度は、今までは正常に動いていた物が動かなくなった」という状況、いわゆる後退バグを未然に防ぐことができます。
*1
それではテストケースを作成します。
utils.include('pXMigemoClasses.inc.js');
var findModule;
function setUp()
{
yield utils.setUpTestWindow();
findModule = new pXMigemoFind();
findModule.target = utils.getTestWindow().gBrowser;
}
function tearDown()
{
findModule.destroy();
findModule = null;
utils.tearDownTestWindow();
}
function testFindFirstVisibleNode()
{
// ここに実際のテスト内容を記述する
}
pXMigemoFindクラスの単体テストはまだ作成されていなかったので、今回はsetUpとtearDownから作成します。すでに作成済みのテストケースがあり、それにテスト項目を追加する場合、この作業は不要となります。
pXMigemoFindクラスはtabbrowser要素を用いて初期化する必要があるため、setUpでテスト用のFirefoxウィンドウを開き、そのウィンドウのtabbrowser要素で初期化します。また、tearDownではsetUpで開いたテスト用のFirefoxウィンドウを閉じて、pXMigemoFindクラスのインスタンスを破棄します。UxUは関数名を見て自動的にその種類を判別するため、これだけで、これらの関数はテストの初期化処理と終了処理として認識されるようになります。
なお、インクルードしているpXMigemoClasses.inc.jsというファイルは、pXMigemoFindクラスやそのクラスが依存しているすべての関連クラスの定義を読み込む物です。
次に、テストの内容を作成していきます。
function testFindFirstVisibleNode()
{
var win = utils.getTestWindow();
win.resizeTo(500, 500);
assert.compare(200, '<', utils.contentWindow.innerHeight);
// ここに実際のテスト内容を記述する
}
関数名を「test」で始めると、その関数はテストの内容として自動的に認識されます。
最初に、ウィンドウの大きさを調整して、「shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する」という条件を整えておきます。ここでは、テスト自体が期待通りの条件下で実行されていることを確認するために、assert.compare()でテスト用フレームの大きさを調べています。
yield utils.addTab(baseURL+'../res/shortPage.html', { selected : true });
var frame = utils.contentWindow;
var node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
assert.equals(utils.contentDocument.documentElement, node);
item = frame.document.getElementById('p3');
node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
assert.equals(item, node);
テスト用のドキュメントを新しいタブで開き、findFirstVisibleNode()メソッドの返り値が期待通りかどうかを検証します。1つ目の検証は前方検索、2つ目は後方検索です。
同様にして、スクロールが発生する場合のテストも作成します。
yield utils.addTab(baseURL+'../res/longPage.html', { selected : true });
frame = utils.contentWindow;
node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
assert.equals(utils.contentDocument.documentElement, node);
item = frame.document.getElementById('p10');
frame.scrollTo(0, item.offsetTop);
node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
assert.equals(item, node);
frame.scrollTo(0, item.offsetTop - frame.innerHeight + item.offsetHeight);
node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
assert.equals(item, node);
item = frame.document.getElementById('p21');
frame.scrollTo(0, item.offsetTop - frame.innerHeight + item.offsetHeight);
node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
assert.equals(item, node);
スクロールされていない時、ページ途中までスクロールされている時、ページの最後までスクロールされている時の各ケースで前方検索と後方検索を行い、結果を検証します。
ここで、かなりの部分のコードが重複していることに気がついたでしょうか。このような場合、それぞれの検証の前で重複しているコードと検証とをひとまとめにして実行する関数(カスタムアサーション)を定義しておくと、テスト項目の追加が簡単になります。以下は、カスタムアサーションを使ってここまでのテスト内容を書き直した物です。
function testFindFirstVisibleNode()
{
var win = utils.getTestWindow();
win.resizeTo(500, 500);
assert.compare(200, '<', utils.contentWindow.innerHeight);
function assertScrollAndFind(aIdOrNode, aFindFlag)
{
var frame = utils.contentWindow;
var item = typeof aIdOrNode == 'string' ? frame.document.getElementById(aIdOrNode) : aIdOrNode ;
frame.scrollTo(
0,
(aFindFlag & findModule.FIND_BACK ?
item.offsetTop - frame.innerHeight + item.offsetHeight :
item.offsetTop
)
);
var node = findModule.findFirstVisibleNode(aFindFlag, frame);
assert.equals(item, node);
}
yield utils.addTab(baseURL+'../res/shortPage.html', { selected : true });
assertScrollAndFind(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
assertScrollAndFind('p3', findModule.FIND_BACK);
yield utils.addTab(baseURL+'../res/longPage.html', { selected : true });
assertScrollAndFind(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
assertScrollAndFind('p10', findModule.FIND_DEFAULT);
assertScrollAndFind('p10', findModule.FIND_BACK);
assertScrollAndFind('p21', findModule.FIND_BACK);
}
テストの実行
テストケースが完成したら、テストを実行してみましょう。実装の修正前なので、当然、このテストは「失敗」という結果が出ます。ですが、この段階では問題ありません。これから、このテストの結果が「成功」になることを目指して実装を修正していきます。
実装の修正
実装の修正内容については省略します。良いアイディアを思いついたら、それを実装に反映して、再度テストを実行してみましょう。テストに成功しないようであれば、まだ修正が必要です。
何度テストを実行しても結果が「成功」になるようになれば、実装の修正はひとまず完了です。修正内容をリポジトリにコミットするなり、修正済みの新しいバージョンとしてリリースするなりしましょう。
新たな問題が発覚した時や、仕様が変わった時は
以上で、今回発見された問題の修正は完了しました。
しかし、上記のテストだけではテストしきれないような、より複雑な条件でだけ発生するバグが新たに見つかるかもしれません。そのような場合は、テストを新たに追加して、それらがすべて「成功」するようになるまで修正してやりましょう。その時はもちろん、他のテストも同時に実行することを忘れないようにしましょう。
また、開発を進める中で、他の部分に加えた変更の影響を受けて上記のテストが失敗するようになることがあるかもしれません。そのような場合、再びテストが通るようになるように実装を修正する必要があります。
実装の仕様を変更した時にも、ここで作成したテストケースは「成功」しなくなる場合があります。このような場合は「ゴール」自体が変わったということになりますので、実装ではなくテストケースの側を修正しなくてはなりません。
まとめ
自動テストを使った開発では、メンテナンスする必要があるコードが「実装」と「テストケース」の2つになるため、一見すると、手間だけが倍増するように思えるかもしれません。
しかし、一連のテスト手順を自動化しておくことで、人の手によるテストでは見落としてしまいかねない思わぬ後退バグの発生に迅速に気づけるようになります。後退バグの発生に日々頭を悩ませている人は、是非、自動テストを開発に取り入れてみてください。
<p>Firefoxアドオン開発者向け自動テストツールの<a href="http://www.clear-code.com/software/uxu/">UxU</a>は、新たに発見したバグの修正にも活用することができます。本日リリースされた<a href="http://piro.sakura.ne.jp/xul/_xulmigemo.html">XUL/Migemo</a> バージョン0.11.7で行われた修正の場合を例に、実際のデバッグ作業の流れを解説します。</p>
<h4>状況</h4>
<p>XUL/Migemoは、Firefoxで表示しているページ内のテキストを検索する機能を提供するアドオンですが、検索を開始する際に、「現在のスクロール位置から検索を開始する」という処理を含んでいます。0.11.6以前では、この機能を使用している時に、ページ先頭から検索が始まるべき場面で、先頭以外の場所から検索が始まってしまうことがあるという問題が起こっていました。</p>
<h4>再現条件の特定</h4>
<p>いくつか条件を変えて調査した結果、スクロールが発生しているページでは期待通りの結果になっているのに対して、スクロールが全く発生していないページ(ページ全体がウィンドウの現在の大きさの中に収まっているページ)では期待と異なる結果になっていることが判明しました。</p>
<h4>原因箇所の特定</h4>
<p>前述の処理の肝となっているのは、<a href="http://www.cozmixng.org/repos/piro/xulmigemo/tags/0.11.7/components/pXMigemoFind.js">pXMigemoFindクラス</a>の<code>findFirstVisibleNode()</code>というメソッドです。このメソッドは、渡されたフレーム(DOMWindow)において現在のスクロール位置で見えている最初の要素を検索して返す物です。このメソッドの戻り値を確認した所、前述の条件下では戻り値が期待と異なっている事が判明しました。</p>
<p>このことから、今回主な修正対象になるのはこの<code>findFirstVisibleNode()</code>というメソッドであるということになります。</p>
<h4>テストケースの作成</h4>
<p>上記メソッドの実装を見直す前に、UxU用のテストケースを作成します。これにより、これから行う修正で目指すべきゴールが明確になります。つまり、このテストが成功する状況まで持って行くことが、今回の修正のゴールとなります。</p>
<p>テストケースはJavaScriptのコードだけで完結する場合もありますが、今回のような場合は実際のWebページを使ってテストを行う必要があります。そこで、問題が発生する条件と発生しない条件の両方の事例としてHTMLドキュメントを用意します。</p>
<ul>
<li><a href="http://www.cozmixng.org/repos/piro/xulmigemo/tags/0.11.7/tests/res/shortPage.html">スクロールが発生しないページ(shortPage.html)</a>:ページの内容が短いため、スクロールが発生しません。</li>
<li><a href="http://www.cozmixng.org/repos/piro/xulmigemo/tags/0.11.7/tests/res/longPage.html">スクロールが発生するページ(longPage.html)</a>:ページの内容がある程度長いため、ウィンドウサイズによってはスクロールが発生します。</li>
</ul>
<p>これらのドキュメントを使い、shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する、という条件の下でテストを行うテストケースをこれから作成することになります。</p>
<p>ところで、現在の実装で問題が起こっている場合だけでなく、<em>すでに正常に動いている場合の事例</em>も同時に作成していることに気がついたでしょうか? 両方の場合を常にテストすることで、<em>「ある問題を修正したら、今度は、今までは正常に動いていた物が動かなくなった」という状況、いわゆる後退バグを未然に防ぐことができます。</em>
<span class="footnote">*1</span></p>
<p>それではテストケースを作成します。</p>
<div class="code"><div class="code"><!-- Generator: GNU source-highlight 2.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt>utils.<span style="color: #FFBB00">include</span>(<span style="color: #BB0000">'pXMigemoClasses.inc.js'</span>);
<span style="color: #B05000">var</span> findModule;
<span style="color: #B05000">function</span> <span style="color: #FFBB00">setUp</span>()
<span style="color: #FFBB00">{</span>
yield utils.<span style="color: #FFBB00">setUpTestWindow</span>();
findModule = <span style="color: #B05000">new</span> <span style="color: #FFBB00">pXMigemoFind</span>();
findModule.target = utils.<span style="color: #FFBB00">getTestWindow</span>().gBrowser;
<span style="color: #FFBB00">}</span>
<span style="color: #B05000">function</span> <span style="color: #FFBB00">tearDown</span>()
<span style="color: #FFBB00">{</span>
findModule.<span style="color: #FFBB00">destroy</span>();
findModule = <span style="color: #B05000">null</span>;
utils.<span style="color: #FFBB00">tearDownTestWindow</span>();
<span style="color: #FFBB00">}</span>
<span style="color: #B05000">function</span> <span style="color: #FFBB00">testFindFirstVisibleNode</span>()
<span style="color: #FFBB00">{</span>
<span style="font-style: italic"><span style="color: #9A1900">// ここに実際のテスト内容を記述する</span></span>
<span style="color: #FFBB00">}</span>
</tt></pre>
</div></div>
<p>pXMigemoFindクラスの単体テストはまだ作成されていなかったので、今回はsetUpとtearDownから作成します。すでに作成済みのテストケースがあり、それにテスト項目を追加する場合、この作業は不要となります。</p>
<p>pXMigemoFindクラスはtabbrowser要素を用いて初期化する必要があるため、setUpでテスト用のFirefoxウィンドウを開き、そのウィンドウのtabbrowser要素で初期化します。また、tearDownではsetUpで開いたテスト用のFirefoxウィンドウを閉じて、pXMigemoFindクラスのインスタンスを破棄します。UxUは関数名を見て自動的にその種類を判別するため、これだけで、これらの関数はテストの初期化処理と終了処理として認識されるようになります。</p>
<p>なお、インクルードしているpXMigemoClasses.inc.jsというファイルは、pXMigemoFindクラスやそのクラスが依存しているすべての関連クラスの定義を読み込む物です。</p>
<p>次に、テストの内容を作成していきます。</p>
<div class="code"><div class="code"><!-- Generator: GNU source-highlight 2.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt><span style="color: #B05000">function</span> <span style="color: #FFBB00">testFindFirstVisibleNode</span>()
<span style="color: #FFBB00">{</span>
<span style="color: #B05000">var</span> win = utils.<span style="color: #FFBB00">getTestWindow</span>();
win.<span style="color: #FFBB00">resizeTo</span>(<span style="color: #BB0000">500</span>, <span style="color: #BB0000">500</span>);
assert.<span style="color: #FFBB00">compare</span>(<span style="color: #BB0000">200</span>, <span style="color: #BB0000">'<'</span>, utils.contentWindow.innerHeight);
<span style="font-style: italic"><span style="color: #9A1900">// ここに実際のテスト内容を記述する</span></span>
<span style="color: #FFBB00">}</span>
</tt></pre>
</div></div>
<p>関数名を「test」で始めると、その関数はテストの内容として自動的に認識されます。</p>
<p>最初に、ウィンドウの大きさを調整して、「shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する」という条件を整えておきます。ここでは、テスト自体が期待通りの条件下で実行されていることを確認するために、<code>assert.compare()</code>でテスト用フレームの大きさを調べています。</p>
<div class="code"><div class="code"><!-- Generator: GNU source-highlight 2.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt> yield utils.<span style="color: #FFBB00">addTab</span>(baseURL+<span style="color: #BB0000">'../res/shortPage.html'</span>, <span style="color: #FFBB00">{</span> selected : <span style="color: #B05000">true</span> <span style="color: #FFBB00">}</span>);
<span style="color: #B05000">var</span> frame = utils.contentWindow;
<span style="color: #B05000">var</span> node = findModule.<span style="color: #FFBB00">findFirstVisibleNode</span>(findModule.FIND_DEFAULT, frame);
assert.<span style="color: #FFBB00">equals</span>(utils.contentDocument.documentElement, node);
item = frame.document.<span style="color: #FFBB00">getElementById</span>(<span style="color: #BB0000">'p3'</span>);
node = findModule.<span style="color: #FFBB00">findFirstVisibleNode</span>(findModule.FIND_BACK, frame);
assert.<span style="color: #FFBB00">equals</span>(item, node);
</tt></pre>
</div></div>
<p>テスト用のドキュメントを新しいタブで開き、<code>findFirstVisibleNode()</code>メソッドの返り値が期待通りかどうかを検証します。1つ目の検証は前方検索、2つ目は後方検索です。</p>
<p>同様にして、スクロールが発生する場合のテストも作成します。</p>
<div class="code"><div class="code"><!-- Generator: GNU source-highlight 2.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt> yield utils.<span style="color: #FFBB00">addTab</span>(baseURL+<span style="color: #BB0000">'../res/longPage.html'</span>, <span style="color: #FFBB00">{</span> selected : <span style="color: #B05000">true</span> <span style="color: #FFBB00">}</span>);
frame = utils.contentWindow;
node = findModule.<span style="color: #FFBB00">findFirstVisibleNode</span>(findModule.FIND_DEFAULT, frame);
assert.<span style="color: #FFBB00">equals</span>(utils.contentDocument.documentElement, node);
item = frame.document.<span style="color: #FFBB00">getElementById</span>(<span style="color: #BB0000">'p10'</span>);
frame.<span style="color: #FFBB00">scrollTo</span>(<span style="color: #BB0000">0</span>, item.offsetTop);
node = findModule.<span style="color: #FFBB00">findFirstVisibleNode</span>(findModule.FIND_DEFAULT, frame);
assert.<span style="color: #FFBB00">equals</span>(item, node);
frame.<span style="color: #FFBB00">scrollTo</span>(<span style="color: #BB0000">0</span>, item.offsetTop - frame.innerHeight + item.offsetHeight);
node = findModule.<span style="color: #FFBB00">findFirstVisibleNode</span>(findModule.FIND_BACK, frame);
assert.<span style="color: #FFBB00">equals</span>(item, node);
item = frame.document.<span style="color: #FFBB00">getElementById</span>(<span style="color: #BB0000">'p21'</span>);
frame.<span style="color: #FFBB00">scrollTo</span>(<span style="color: #BB0000">0</span>, item.offsetTop - frame.innerHeight + item.offsetHeight);
node = findModule.<span style="color: #FFBB00">findFirstVisibleNode</span>(findModule.FIND_BACK, frame);
assert.<span style="color: #FFBB00">equals</span>(item, node);
</tt></pre>
</div></div>
<p>スクロールされていない時、ページ途中までスクロールされている時、ページの最後までスクロールされている時の各ケースで前方検索と後方検索を行い、結果を検証します。</p>
<p>ここで、かなりの部分のコードが重複していることに気がついたでしょうか。このような場合、それぞれの検証の前で重複しているコードと検証とをひとまとめにして実行する関数(カスタムアサーション)を定義しておくと、テスト項目の追加が簡単になります。以下は、カスタムアサーションを使ってここまでのテスト内容を書き直した物です。</p>
<div class="code"><div class="code"><!-- Generator: GNU source-highlight 2.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt><span style="color: #B05000">function</span> <span style="color: #FFBB00">testFindFirstVisibleNode</span>()
<span style="color: #FFBB00">{</span>
<span style="color: #B05000">var</span> win = utils.<span style="color: #FFBB00">getTestWindow</span>();
win.<span style="color: #FFBB00">resizeTo</span>(<span style="color: #BB0000">500</span>, <span style="color: #BB0000">500</span>);
assert.<span style="color: #FFBB00">compare</span>(<span style="color: #BB0000">200</span>, <span style="color: #BB0000">'<'</span>, utils.contentWindow.innerHeight);
<span style="color: #B05000">function</span> <span style="color: #FFBB00">assertScrollAndFind</span>(aIdOrNode, aFindFlag)
<span style="color: #FFBB00">{</span>
<span style="color: #B05000">var</span> frame = utils.contentWindow;
<span style="color: #B05000">var</span> item = <span style="color: #B05000">typeof</span> aIdOrNode == <span style="color: #BB0000">'string'</span> ? frame.document.<span style="color: #FFBB00">getElementById</span>(aIdOrNode) : aIdOrNode ;
frame.<span style="color: #FFBB00">scrollTo</span>(
<span style="color: #BB0000">0</span>,
(aFindFlag & findModule.FIND_BACK ?
item.offsetTop - frame.innerHeight + item.offsetHeight :
item.offsetTop
)
);
<span style="color: #B05000">var</span> node = findModule.<span style="color: #FFBB00">findFirstVisibleNode</span>(aFindFlag, frame);
assert.<span style="color: #FFBB00">equals</span>(item, node);
<span style="color: #FFBB00">}</span>
yield utils.<span style="color: #FFBB00">addTab</span>(baseURL+<span style="color: #BB0000">'../res/shortPage.html'</span>, <span style="color: #FFBB00">{</span> selected : <span style="color: #B05000">true</span> <span style="color: #FFBB00">}</span>);
<span style="color: #FFBB00">assertScrollAndFind</span>(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
<span style="color: #FFBB00">assertScrollAndFind</span>(<span style="color: #BB0000">'p3'</span>, findModule.FIND_BACK);
yield utils.<span style="color: #FFBB00">addTab</span>(baseURL+<span style="color: #BB0000">'../res/longPage.html'</span>, <span style="color: #FFBB00">{</span> selected : <span style="color: #B05000">true</span> <span style="color: #FFBB00">}</span>);
<span style="color: #FFBB00">assertScrollAndFind</span>(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
<span style="color: #FFBB00">assertScrollAndFind</span>(<span style="color: #BB0000">'p10'</span>, findModule.FIND_DEFAULT);
<span style="color: #FFBB00">assertScrollAndFind</span>(<span style="color: #BB0000">'p10'</span>, findModule.FIND_BACK);
<span style="color: #FFBB00">assertScrollAndFind</span>(<span style="color: #BB0000">'p21'</span>, findModule.FIND_BACK);
<span style="color: #FFBB00">}</span>
</tt></pre>
</div></div>
<h4>テストの実行</h4>
<p>テストケースが完成したら、テストを実行してみましょう。実装の修正前なので、当然、このテストは「失敗」という結果が出ます。ですが、この段階では問題ありません。これから、このテストの結果が「成功」になることを目指して実装を修正していきます。</p>
<h4>実装の修正</h4>
<p>実装の修正内容については省略します。良いアイディアを思いついたら、それを実装に反映して、再度テストを実行してみましょう。テストに成功しないようであれば、まだ修正が必要です。</p>
<p>何度テストを実行しても結果が「成功」になるようになれば、実装の修正はひとまず完了です。修正内容をリポジトリにコミットするなり、修正済みの新しいバージョンとしてリリースするなりしましょう。</p>
<h4>新たな問題が発覚した時や、仕様が変わった時は</h4>
<p>以上で、今回発見された問題の修正は完了しました。</p>
<p>しかし、上記のテストだけではテストしきれないような、より複雑な条件でだけ発生するバグが新たに見つかるかもしれません。そのような場合は、テストを新たに追加して、それらがすべて「成功」するようになるまで修正してやりましょう。その時はもちろん、他のテストも同時に実行することを忘れないようにしましょう。</p>
<p>また、開発を進める中で、他の部分に加えた変更の影響を受けて上記のテストが失敗するようになることがあるかもしれません。そのような場合、再びテストが通るようになるように実装を修正する必要があります。</p>
<p>実装の仕様を変更した時にも、ここで作成したテストケースは「成功」しなくなる場合があります。このような場合は「ゴール」自体が変わったということになりますので、実装ではなくテストケースの側を修正しなくてはなりません。</p>
<h4>まとめ</h4>
<p>自動テストを使った開発では、メンテナンスする必要があるコードが「実装」と「テストケース」の2つになるため、一見すると、手間だけが倍増するように思えるかもしれません。</p>
<p>しかし、一連のテスト手順を自動化しておくことで、人の手によるテストでは見落としてしまいかねない思わぬ後退バグの発生に迅速に気づけるようになります。後退バグの発生に日々頭を悩ませている人は、是非、自動テストを開発に取り入れてみてください。</p>
ClearCode Inc.
2008-11-17T16:36:56+09:00
-
tDiaryのSubversionバックエンド
http://www.clear-code.com/blog/2008/11/13.html
みなさんはtDiaryの日記データをどのようにバックアップしているのでしょうか。cronでアーカイブしていたり、dbi_ioでデータベースに保存して、データベースの内容をアーカイブしていたりしているのでしょうか。
開発者の人なら日記のデータもソースコードと同じようにバックアップしたいですよね。つまり、バージョン管理をして、レポジトリの内容をアーカイブするバックアップです。そんな人はこのようなSubversionIOはいかがでしょうか。保存する毎にSubversionリポジトリに日記データをコミットします。
require 'tdiary/defaultio'
module TDiary
class SubversionIO < DefaultIO
def transaction( date, &block )
dirty = TDiaryBase::DIRTY_NONE
result = super( date ) do |diaries|
dirty = block.call( diaries )
diaries = diaries.reject {|_, diary| /\A\s*\Z/ =~ diary.to_src}
dirty
end
unless (dirty & TDiaryBase::DIRTY_DIARY).zero?
run( "svn", "add", File.dirname( @dfile ) )
run( "svn", "add", @dfile )
Dir.chdir( @data_path ) do
run( "svn", "ci", "-m", "update #{date.strftime('%Y-%m-%d')}" )
end
end
result
end
private
def run( *command )
command = command.collect {|arg| escape_arg( arg )}.join(' ')
result = `#{command} 2>&1`
unless $?.success?
raise "Failed to run #{command}: #{result}"
end
result
end
def escape_arg( arg )
"'#{arg.gsub( /'/, '\\\'' )}'"
end
end
end
# Local Variables:
# ruby-indent-level: 3
# tab-width: 3
# indent-tabs-mode: t
# End:
使い方は以下の2ステップです。
tdiary.confで@io_classに指定する
日記のデータディレクトリをSubversionのワーキングコピーにする
tdiary.confの設定
まず、上記のソースコードをsubversionio.rbとしてどこかに保存してください。ここでは/home/tdiary/lib/以下に保存したとします。
次にtdiary.confに以下の内容を追記します。
subversion_io_dir = "/home/tdiary/lib" # <- 保存した場所にあわせて変更
require "#{subversion_io_dir}/subversionio"
@io_class = TDiary::SubversionIO
データディレクトリの設定
データディレクトリ(tdiary.conf内の@data_pathで指定したディレクトリ)は/home/tdiary/data/として進めます。また、作業しているユーザはtDiaryのCGIを動かすユーザとします。(ここではtdiaryユーザ)
まず、レポジトリに日記データ用のパスを作ります。既存のSubversionリポジトリを利用する場合は、例えばこのようになります。
[tdiary]% svn mkdir -m 'create tDiary data path' https://.../repos/tdiary-data
ローカルに新しくSubversionのリポジトリを作成して、そこに日記データをコミットするようにする場合はこのようになります。新しく作成するリポジトリは/home/tdiary/repos/に作ることにします。
[tdiary]% svnadmin create /home/tdiary/repos
Subversionリポジトリから日記データ保存用のパスを、tDiaryのデータディレクトリにチェックアウトします。今すでにあるtDiaryのデータディレクトリはどこかによけておきます。
[tdiary]% mv /home/tdiary/data /home/tdiary/data.bak
[tdiary]% svn co file:///home/tdiary/repos /home/tdiary/data
既存のデータをワーキングコピーに移動し、日記データだけをレポジトリにコミットします。
[tdiary]% cd /home/tdiary/data
[tdiary]% cp -rp ../data.bak/* ./
[tdiary]% svn add 200*
[tdiary]% svn add 199* # <- もし2000年より前のデータがあるなら
[tdiary]% svn ci -m 'import'
完了
以上で設定は完了です。日記を保存するとリポジトリにコミットされます。
制限
日記本文しか対応していません。ツッコミや画像には対応していません。
svnコマンドをインストールしている必要があります。
tDiary本体のコーディングスタイルに合わせているためタブインデントになっています。
ライセンス
AGPL3あるいは3以降の新しいバージョンのAGPL
<p>みなさんは<a href="http://www.tdiary.org/">tDiary</a>の日記データをどのようにバックアップしているのでしょうか。cronでアーカイブしていたり、dbi_ioでデータベースに保存して、データベースの内容をアーカイブしていたりしているのでしょうか。</p>
<p>開発者の人なら日記のデータもソースコードと同じようにバックアップしたいですよね。つまり、バージョン管理をして、レポジトリの内容をアーカイブするバックアップです。そんな人はこのようなSubversionIOはいかがでしょうか。保存する毎にSubversionリポジトリに日記データをコミットします。</p>
<div class="code"><div class="code"><!-- Generator: GNU source-highlight 2.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt><span style="color: #AA22AA">require</span> <span style="color: #BB0000">'tdiary/defaultio'</span>
<span style="color: #B05000">module</span> TDiary
<span style="color: #B05000">class</span> SubversionIO < DefaultIO
<span style="color: #B05000">def</span> transaction( date, &block )
dirty = TDiaryBase::DIRTY_NONE
result = <span style="color: #B05000">super</span>( date ) <span style="color: #B05000">do</span> |diaries|
dirty = block.call( diaries )
diaries = diaries.reject <span style="color: #FFBB00">{</span>|_, diary| /\A\s*\Z/ =~ diary.to_src<span style="color: #FFBB00">}</span>
dirty
<span style="color: #B05000">end</span>
<span style="color: #B05000">unless</span> (dirty & TDiaryBase::DIRTY_DIARY).zero?
run( <span style="color: #BB0000">"svn"</span>, <span style="color: #BB0000">"add"</span>, File.dirname( <span style="color: #009900">@dfile</span> ) )
run( <span style="color: #BB0000">"svn"</span>, <span style="color: #BB0000">"add"</span>, <span style="color: #009900">@dfile</span> )
Dir.chdir( <span style="color: #009900">@data_path</span> ) <span style="color: #B05000">do</span>
run( <span style="color: #BB0000">"svn"</span>, <span style="color: #BB0000">"ci"</span>, <span style="color: #BB0000">"-m"</span>, <span style="color: #BB0000">"update #{date.strftime('%Y-%m-%d')}"</span> )
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
result
<span style="color: #B05000">end</span>
private
<span style="color: #B05000">def</span> run( *command )
command = command.collect <span style="color: #FFBB00">{</span>|arg| escape_arg( arg )<span style="color: #FFBB00">}</span>.join(<span style="color: #BB0000">' '</span>)
result = `#<span style="color: #FFBB00">{</span>command<span style="color: #FFBB00">}</span> <span style="color: #BB0000">2</span>>&<span style="color: #BB0000">1</span>`
<span style="color: #B05000">unless</span> $?.success?
<span style="color: #B05000">raise</span> <span style="color: #BB0000">"Failed to run #{command}: #{result}"</span>
<span style="color: #B05000">end</span>
result
<span style="color: #B05000">end</span>
<span style="color: #B05000">def</span> escape_arg( arg )
<span style="color: #BB0000">"'#{arg.gsub( /'/, '\\\'' )}'"</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="color: #B05000">end</span>
<span style="font-style: italic"><span style="color: #9A1900"># Local Variables:</span></span>
<span style="font-style: italic"><span style="color: #9A1900"># ruby-indent-level: 3</span></span>
<span style="font-style: italic"><span style="color: #9A1900"># tab-width: 3</span></span>
<span style="font-style: italic"><span style="color: #9