ちゃんと書くと、C言語で書かれたreadlineをwrapしているreadlineモジュールじゃなくて、rubyinstallerに標準添付されたPure RubyScriptなモジュール。中身はUNIXのコードとWindowsのコードが入り乱れていて、rbreadline.rbなんか8686行もある大作。
まぁPure RubyScriptでCの真似事をしようってんだからこうなるよね...って感じ。
ただバイト長とキャラクタ数と、文字幅の扱いが間違ってて、Windows-31JなDBCSなんかではちゃんと動かない。パッチ書いて「問題があるんだよ。気付いて!」ってつもりでpull request送ったらいきなりIssue trackerでcloseされてカチンと来たので「その態度はいかがなもんかと思う」的なコメントをした。そしたら「まずテストを書け」との事だったので書いた。
しかしながら、上記の間違いを直すとしてreadlineの正しい動きをテストするって難しい。関数単体なら出来るけどUIのテストはどの言語でも苦しむ。おまけにreadlineはreadline()メソッドを呼び出してる最中は、テスターが止まってしまう。終えるにはユーザの入力が必要。
じゃぁユーザの入力を横取りしてやんよ!って事で、rl_get_char()をぶんどる事にした。
module RbReadline
#...
alias :old_rl_get_char :rl_get_char
def rl_get_char(*a)
c = old_rl_get_char(*a)
c.force_encoding("ASCII-8BIT") if c
@last_xy = xy
return (c || EOF)
end
def rl_get_char=(val)
for c in val.reverse
_rl_unget_char(c)
end
end
module_function :old_rl_get_char, :rl_get_char, :"rl_get_char="
end
こんな感じにRbReadline.rl_get_char()をMix-inで上書きしてやる。こうすれば、外からキー入力を差し替えられる。
RbReadline.rl_get_char = ["あ", "い", "う", "え", "お"]
buf = Readline.readline("$")
これさえ出来れば、文字入力させて最後のカーソル位置が正しい位置にいる事を確認出来る。カーソルの位置はGetConsoleScreenBufferInfo()で得られるのでWin32API使ってゴリゴリ取った。また、最終的な入力結果が文字化けしていない事を確認する為にReadConsoleOutputCharacter()も使った。全体のコードは以下の様になった。
# encoding: CP932
require 'test/unit'
require 'rb-readline'
require 'Win32API'
module RbReadline
@GetStdHandle = Win32API.new("kernel32","GetStdHandle",['L'],'L')
@hConsoleHandle = @GetStdHandle.Call(STD_OUTPUT_HANDLE)
@GetConsoleScreenBufferInfo = Win32API.new("kernel32","GetConsoleScreenBufferInfo",['L','P'],'L')
@ReadConsoleOutputCharacter = Win32API.new("kernel32","ReadConsoleOutputCharacter",['L','P','L','L','P'],'I')
alias :old_rl_get_char :rl_get_char
def rl_get_char(*a)
c = old_rl_get_char(*a)
c.force_encoding("ASCII-8BIT") if c
@last_xy = xy
return (c || EOF)
end
def rl_get_char=(val)
for c in val.reverse
_rl_unget_char(c)
end
end
def last_xy
@last_xy
end
def last_xy=(val)
@last_xy = val
end
def xy
csbi = 0.chr * 24
@GetConsoleScreenBufferInfo.Call(@hConsoleHandle,csbi)
[csbi[4,2].unpack('s*').first, csbi[6,4].unpack('s*').first]
end
def get_line(l)
line = 0.chr * 80
length = 80
coord = l << 16
num_read = ' ' * 4
@ReadConsoleOutputCharacter.Call(@hConsoleHandle,line,length,coord,num_read)
line.force_encoding("Windows-31J")
end
module_function :old_rl_get_char, :rl_get_char, :"rl_get_char="
end
class TestReadline < Test::Unit::TestCase
def setup
Readline::HISTORY << "世界".force_encoding("ISO-8859-1")
Readline::HISTORY << "abc".force_encoding("ISO-8859-1")
Readline::HISTORY << "bcdef".force_encoding("ISO-8859-1")
RbReadline.rl_get_char = []
RbReadline.last_xy = RbReadline.xy
puts
end
def test_cursor_position_normal
a = RbReadline.xy
buf = Readline.readline("$")
b = RbReadline.last_xy
assert_equal 2, b[0] - a[0]
assert_equal 0, b[1] - a[1]
end
def test_cursor_position_mix
a = RbReadline.xy
buf = Readline.readline("$$$")
b = RbReadline.last_xy
assert_equal 5, b[0] - a[0]
assert_equal 0, b[1] - a[1]
end
def test_cursor_position_insert_single_width
a = RbReadline.xy
RbReadline.rl_get_char = ["a", "b", "c", "d", "e"]
buf = Readline.readline("$")
b = RbReadline.last_xy
assert_equal 7, b[0] - a[0]
assert_equal 0, b[1] - a[1]
end
def test_cursor_position_insert_double_width
a = RbReadline.xy
RbReadline.rl_get_char = ["あ", "い", "う", "え", "お"]
buf = Readline.readline("$")
b = RbReadline.last_xy
assert_equal 12, b[0] - a[0]
assert_equal 0, b[1] - a[1]
end
def test_cursor_position_previous_history
a = RbReadline.xy
RbReadline.rl_get_char = ["a", "\340H", "\340H"]
buf = Readline.readline("$")
b = RbReadline.last_xy
assert_equal 5, b[0] - a[0]
assert_equal 0, b[1] - a[1]
end
def test_cursor_position_next_history
a = RbReadline.xy
RbReadline.rl_get_char = ["a", "\340H", "\340H", "\340P"]
buf = Readline.readline("$")
b = RbReadline.last_xy
assert_equal 7, b[0] - a[0]
assert_equal 0, b[1] - a[1]
end
def test_cursor_position_history_include_multibyte
a = RbReadline.xy
RbReadline.rl_get_char = ["a", "\340H", "\340H", "\340H"]
buf = Readline.readline("$")
b = RbReadline.last_xy
assert_equal 6, b[0] - a[0]
assert_equal 0, b[1] - a[1]
end
def test_cursor_position_insert_into
a = RbReadline.xy
RbReadline.rl_get_char = ["あ", "い", "う", "\340K", "\340K", "a"]
buf = Readline.readline("$")
b = RbReadline.last_xy
assert_equal 5, b[0] - a[0]
assert_equal 0, b[1] - a[1]
assert_equal "$あaいう", RbReadline.get_line(b[1])
end
end
僕がpull requestしたpatchもtest_cursor_position_insert_intoはNG出るのでこれを直していこうと思う。