bitclust-core 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/data/bitclust/template/class-index +0 -1
- data/data/bitclust/template.epub/class +5 -5
- data/data/bitclust/template.epub/class-index +0 -1
- data/data/bitclust/template.epub/contents +7 -4
- data/data/bitclust/template.epub/layout +4 -4
- data/data/bitclust/template.offline/class-index +0 -1
- data/lib/bitclust/generators/epub.rb +69 -5
- data/lib/bitclust/irb.rb +14 -8
- data/lib/bitclust/markdown_bridge.rb +11 -6
- data/lib/bitclust/markdown_tree.rb +118 -11
- data/lib/bitclust/methoddatabase.rb +17 -4
- data/lib/bitclust/nameutils.rb +13 -0
- data/lib/bitclust/preprocessor.rb +0 -36
- data/lib/bitclust/remote.rb +405 -0
- data/lib/bitclust/runner.rb +5 -10
- data/lib/bitclust/screen.rb +2 -1
- data/lib/bitclust/searcher.rb +43 -24
- data/lib/bitclust/subcommands/chm_command.rb +1 -1
- data/lib/bitclust/subcommands/epub_command.rb +2 -2
- data/lib/bitclust/subcommands/extract_command.rb +7 -0
- data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
- data/lib/bitclust/subcommands/methods_command.rb +5 -28
- data/lib/bitclust/subcommands/query_command.rb +1 -0
- data/lib/bitclust/subcommands/server_command.rb +21 -10
- data/lib/bitclust/subcommands/setup_command.rb +28 -10
- data/lib/bitclust/syntax_highlighter.rb +65 -4
- data/lib/bitclust/user_dirs.rb +62 -0
- data/lib/bitclust/version.rb +1 -1
- data/test/test_chm_command.rb +22 -0
- data/test/test_epub_command.rb +60 -0
- data/test/test_epub_generator.rb +330 -0
- data/test/test_extract_command.rb +45 -0
- data/test/test_irb_plugin.rb +100 -6
- data/test/test_markdown_bridge.rb +35 -0
- data/test/test_markdown_tree.rb +144 -0
- data/test/test_mdparser.rb +39 -0
- data/test/test_methods_command.rb +32 -0
- data/test/test_opensearchdescription_screen.rb +39 -0
- data/test/test_query_command.rb +30 -0
- data/test/test_remote.rb +374 -0
- data/test/test_runner.rb +61 -1
- data/test/test_searcher.rb +63 -0
- data/test/test_server_command.rb +41 -0
- data/test/test_setup_command.rb +143 -0
- data/test/test_syntax_highlighter.rb +124 -0
- data/test/test_user_dirs.rb +62 -0
- metadata +37 -2
- data/lib/bitclust/ridatabase.rb +0 -359
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'test/unit'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'rexml/document'
|
|
6
|
+
require 'bitclust'
|
|
7
|
+
require 'bitclust/generators/epub'
|
|
8
|
+
require 'bitclust/screen'
|
|
9
|
+
require 'bitclust/subcommands/statichtml_command'
|
|
10
|
+
|
|
11
|
+
# contents.opf のマニフェストは、statichtml が OEBPS 以下に生成する
|
|
12
|
+
# class/method/library/doc/function ページと css/image リソースを
|
|
13
|
+
# すべて(ユニークな id で)列挙しなければならない(EPUB の仕様上必須)。
|
|
14
|
+
class TestEpubGeneratorContentsOpf < Test::Unit::TestCase
|
|
15
|
+
def build_epub_generator
|
|
16
|
+
datadir = File.expand_path('../data/bitclust', __dir__)
|
|
17
|
+
BitClust::Generators::EPUB.new(:templatedir => Pathname.new("#{datadir}/template.epub"))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def setup
|
|
21
|
+
@dir = Dir.mktmpdir
|
|
22
|
+
@epub_directory = Pathname.new(@dir)
|
|
23
|
+
@oebps = @epub_directory + "OEBPS"
|
|
24
|
+
FileUtils.mkdir_p(@oebps + "class")
|
|
25
|
+
FileUtils.mkdir_p(@oebps + "method/-array/i")
|
|
26
|
+
FileUtils.mkdir_p(@oebps + "doc")
|
|
27
|
+
FileUtils.mkdir_p(@oebps + "library")
|
|
28
|
+
FileUtils.mkdir_p(@oebps + "function")
|
|
29
|
+
FileUtils.mkdir_p(@oebps + "images")
|
|
30
|
+
|
|
31
|
+
File.write(@oebps + "class/-array.xhtml", "<html/>")
|
|
32
|
+
File.write(@oebps + "class/index.xhtml", "<html/>") # クラス一覧ページ
|
|
33
|
+
File.write(@oebps + "method/-array/i/each.xhtml", "<html/>")
|
|
34
|
+
File.write(@oebps + "doc/index.xhtml", "<html/>") # マニュアルのトップページ
|
|
35
|
+
File.write(@oebps + "doc/help.xhtml", "<html/>")
|
|
36
|
+
File.write(@oebps + "library/index.xhtml", "<html/>")
|
|
37
|
+
File.write(@oebps + "library/json.xhtml", "<html/>")
|
|
38
|
+
File.write(@oebps + "function/index.xhtml", "<html/>")
|
|
39
|
+
File.write(@oebps + "function/rb_ary_new.xhtml", "<html/>")
|
|
40
|
+
File.write(@oebps + "style.css", "body{}")
|
|
41
|
+
File.write(@oebps + "images/rurema.png", "\x89PNG\r\n")
|
|
42
|
+
File.write(@epub_directory + "nav.xhtml", "<html/>")
|
|
43
|
+
|
|
44
|
+
@generator = build_epub_generator
|
|
45
|
+
@generator.send(:generate_contents_opf, @epub_directory)
|
|
46
|
+
@opf_path = @epub_directory + "contents.opf"
|
|
47
|
+
@xml = File.read(@opf_path)
|
|
48
|
+
@doc = REXML::Document.new(@xml)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def teardown
|
|
52
|
+
FileUtils.rm_rf(@dir)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def items
|
|
56
|
+
REXML::XPath.match(@doc, "//manifest/item")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_contents_opf_is_well_formed_xml
|
|
60
|
+
assert_nothing_raised { REXML::Document.new(@xml) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_manifest_lists_every_xhtml_page_under_oebps
|
|
64
|
+
hrefs = items.map {|item| item.attributes['href'] }
|
|
65
|
+
%w[
|
|
66
|
+
OEBPS/class/-array.xhtml
|
|
67
|
+
OEBPS/class/index.xhtml
|
|
68
|
+
OEBPS/method/-array/i/each.xhtml
|
|
69
|
+
OEBPS/doc/index.xhtml
|
|
70
|
+
OEBPS/doc/help.xhtml
|
|
71
|
+
OEBPS/library/index.xhtml
|
|
72
|
+
OEBPS/library/json.xhtml
|
|
73
|
+
OEBPS/function/index.xhtml
|
|
74
|
+
OEBPS/function/rb_ary_new.xhtml
|
|
75
|
+
nav.xhtml
|
|
76
|
+
].each do |href|
|
|
77
|
+
assert_include(hrefs, href)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_manifest_lists_non_xhtml_resources_that_exist
|
|
82
|
+
hrefs = items.map {|item| item.attributes['href'] }
|
|
83
|
+
assert_include(hrefs, "OEBPS/style.css")
|
|
84
|
+
assert_include(hrefs, "OEBPS/images/rurema.png")
|
|
85
|
+
style_item = items.find {|item| item.attributes['href'] == "OEBPS/style.css" }
|
|
86
|
+
assert_equal("text/css", style_item.attributes['media-type'])
|
|
87
|
+
image_item = items.find {|item| item.attributes['href'] == "OEBPS/images/rurema.png" }
|
|
88
|
+
assert_equal("image/png", image_item.attributes['media-type'])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_manifest_item_ids_are_unique
|
|
92
|
+
ids = items.map {|item| item.attributes['id'] }
|
|
93
|
+
assert_equal(ids.size, ids.uniq.size, "duplicate manifest item ids: #{ids}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_manifest_item_ids_are_valid_xml_ncnames
|
|
97
|
+
ids = items.map {|item| item.attributes['id'] }
|
|
98
|
+
assert_false(ids.empty?)
|
|
99
|
+
ids.each do |id|
|
|
100
|
+
assert_match(/\A[A-Za-z_][A-Za-z0-9_.-]*\z/, id, "not a valid NCName: #{id.inspect}")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# doc/index.xhtml はマニフェスト中に決め打ちの id="index" として
|
|
105
|
+
# 一度だけ登場し、doc 以下を走査する側から重複登録されない。
|
|
106
|
+
def test_doc_index_is_listed_exactly_once_with_the_index_id
|
|
107
|
+
index_items = items.select {|item| item.attributes['href'] == "OEBPS/doc/index.xhtml" }
|
|
108
|
+
assert_equal(1, index_items.size)
|
|
109
|
+
assert_equal("index", index_items.first.attributes['id'])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_manifest_references_xhtml_not_html_for_the_index
|
|
113
|
+
hrefs = items.map {|item| item.attributes['href'] }
|
|
114
|
+
assert_not_include(hrefs, "OEBPS/doc/index.html")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# 目次ページ (nav) ・マニュアルのトップページ・クラスページだけが
|
|
118
|
+
# spine(読み進める順序)に載る。約 11,700 あるメソッドページなどを
|
|
119
|
+
# spine に加えると読書順序が壊れるので、マニフェストには載るが spine
|
|
120
|
+
# には載らない。
|
|
121
|
+
def test_spine_still_excludes_method_library_doc_and_function_pages
|
|
122
|
+
spine_idrefs = REXML::XPath.match(@doc, "//spine/itemref").map {|el| el.attributes['idref'] }
|
|
123
|
+
assert_equal(%w[toc index], spine_idrefs.first(2))
|
|
124
|
+
|
|
125
|
+
class_item_ids = items.select {|item| item.attributes['href'].to_s.start_with?('OEBPS/class/') }
|
|
126
|
+
.map {|item| item.attributes['id'] }
|
|
127
|
+
assert_equal(%w[toc index] + class_item_ids, spine_idrefs)
|
|
128
|
+
|
|
129
|
+
other_ids = items.map {|item| item.attributes['id'] } - %w[toc index] - class_item_ids
|
|
130
|
+
assert_false(other_ids.empty?)
|
|
131
|
+
other_ids.each do |id|
|
|
132
|
+
assert_not_include(spine_idrefs, id)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# EPUB のパッケージ(contents.opf)に載る前に、statichtml が出力ルートへ
|
|
138
|
+
# 書く 2 行のリダイレクトスタブ(<meta http-equiv=refresh> + <a>、
|
|
139
|
+
# ルート要素を持たず XML として整形式でない)を消す。
|
|
140
|
+
class TestEpubGeneratorIndexStub < Test::Unit::TestCase
|
|
141
|
+
def build_epub_generator
|
|
142
|
+
datadir = File.expand_path('../data/bitclust', __dir__)
|
|
143
|
+
BitClust::Generators::EPUB.new(:templatedir => Pathname.new("#{datadir}/template.epub"))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def setup
|
|
147
|
+
@dir = Dir.mktmpdir
|
|
148
|
+
@contents_directory = Pathname.new(@dir) + "OEBPS"
|
|
149
|
+
FileUtils.mkdir_p(@contents_directory)
|
|
150
|
+
@generator = build_epub_generator
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def teardown
|
|
154
|
+
FileUtils.rm_rf(@dir)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_removes_the_redirect_stub_statichtml_writes_at_the_root
|
|
158
|
+
stub_path = @contents_directory + "index.xhtml"
|
|
159
|
+
File.write(stub_path, "<meta http-equiv=\"refresh\" content=\"0; URL=doc/index.xhtml\">\n<a href=\"doc/index.xhtml\">Go</a>\n")
|
|
160
|
+
assert_raise(REXML::ParseException) { REXML::Document.new(File.read(stub_path)) }
|
|
161
|
+
|
|
162
|
+
@generator.send(:remove_index_stub, @contents_directory)
|
|
163
|
+
|
|
164
|
+
assert_false(stub_path.exist?)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_is_a_noop_when_the_stub_is_absent
|
|
168
|
+
stub_path = @contents_directory + "index.xhtml"
|
|
169
|
+
assert_nothing_raised { @generator.send(:remove_index_stub, @contents_directory) }
|
|
170
|
+
assert_false(stub_path.exist?)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# doc/index.xhtml (マニュアルのトップページ) と紛れないことを確認する
|
|
174
|
+
def test_does_not_touch_the_doc_index_page
|
|
175
|
+
doc_dir = @contents_directory + "doc"
|
|
176
|
+
FileUtils.mkdir_p(doc_dir)
|
|
177
|
+
doc_index = doc_dir + "index.xhtml"
|
|
178
|
+
File.write(doc_index, "<html/>")
|
|
179
|
+
@generator.send(:remove_index_stub, @contents_directory)
|
|
180
|
+
assert_true(doc_index.exist?)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# data/bitclust/template.epub が整形式 XHTML を出力すること(layout の
|
|
185
|
+
# <meta>/<link> の自己終了、class のインデックス一覧での "<=>" や "&" を
|
|
186
|
+
# 含むメソッド名のエスケープ)を検証する。
|
|
187
|
+
#
|
|
188
|
+
# Screen#run_template は ERB 化したテンプレート本体をレシーバの
|
|
189
|
+
# self.class(ClassScreen / MethodScreen そのもの)に「respond_to? なら
|
|
190
|
+
# コンパイルしない」というガード付きでキャッシュする。同一プロセス内で
|
|
191
|
+
# template.offline 用の別テスト(例: test_class_screen.rb)が先に走ると、
|
|
192
|
+
# そちらのコンパイル結果を再利用してしまい、このテストが実際には
|
|
193
|
+
# template.epub を検証できなくなる(test_method_screen.rb に既知の注記
|
|
194
|
+
# あり)。
|
|
195
|
+
#
|
|
196
|
+
# 専用のサブクラスに分けるだけでは防げない: respond_to? は継承した
|
|
197
|
+
# メソッドにも true を返すので、スーパークラス(本物の ClassScreen /
|
|
198
|
+
# MethodScreen)側で他のテストが先に class_template 等をコンパイル済みだと、
|
|
199
|
+
# このサブクラスの run_template はガードに阻まれて自分の
|
|
200
|
+
# @template_repository(= template.epub)で再コンパイルしない。
|
|
201
|
+
# そこで、最初の render より前に明示的に def_method して
|
|
202
|
+
# class_template / method_template / layout をこのサブクラス自身に
|
|
203
|
+
# 直接定義しておく。直接定義されたメソッドは継承したものより優先されるので、
|
|
204
|
+
# 他のテストの実行順序に関係なく template.epub の内容を使わせられる。
|
|
205
|
+
class EpubTestClassScreen < BitClust::ClassScreen; end
|
|
206
|
+
class EpubTestMethodScreen < BitClust::MethodScreen; end
|
|
207
|
+
class EpubTestClassIndexScreen < BitClust::ClassIndexScreen; end
|
|
208
|
+
|
|
209
|
+
epub_template_repository = BitClust::TemplateRepository.new(
|
|
210
|
+
"#{File.expand_path('../data/bitclust', __dir__)}/template.epub"
|
|
211
|
+
)
|
|
212
|
+
[
|
|
213
|
+
[EpubTestClassScreen, 'class', 'class_template'],
|
|
214
|
+
[EpubTestMethodScreen, 'method', 'method_template'],
|
|
215
|
+
[EpubTestClassIndexScreen, 'class-index', 'class_index_template'],
|
|
216
|
+
].each do |klass, id, method_name|
|
|
217
|
+
ERB.new(epub_template_repository.load(id)).def_method(klass, method_name, "#{id}.erb")
|
|
218
|
+
end
|
|
219
|
+
[EpubTestClassScreen, EpubTestMethodScreen, EpubTestClassIndexScreen].each do |klass|
|
|
220
|
+
ERB.new(epub_template_repository.load('layout')).def_method(klass, 'layout', 'layout.erb')
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
class TestEpubTemplateWellFormedness < Test::Unit::TestCase
|
|
224
|
+
SRC = <<'HERE'
|
|
225
|
+
= module Helper
|
|
226
|
+
比較演算子を提供するヘルパーモジュール
|
|
227
|
+
= class Spaceship < Object
|
|
228
|
+
extend Helper
|
|
229
|
+
比較演算子とビット演算子を持つサンプルクラス
|
|
230
|
+
== Instance Methods
|
|
231
|
+
--- <=>(other) -> Integer
|
|
232
|
+
|
|
233
|
+
比較する。
|
|
234
|
+
|
|
235
|
+
--- &(other) -> Spaceship
|
|
236
|
+
|
|
237
|
+
論理積を返す。
|
|
238
|
+
HERE
|
|
239
|
+
|
|
240
|
+
def setup
|
|
241
|
+
datadir = File.expand_path('../data/bitclust', __dir__)
|
|
242
|
+
@lib, @db = BitClust::RRDParser.parse(SRC, 'testlib', { 'version' => '3.4' })
|
|
243
|
+
urlmapper = BitClust::Subcommands::StatichtmlCommand::URLMapperEx.new(
|
|
244
|
+
:suffix => '.xhtml',
|
|
245
|
+
:css_url => 'style.css',
|
|
246
|
+
:favicon_url => 'rurema.png'
|
|
247
|
+
)
|
|
248
|
+
urlmapper.bitclust_html_base = '..'
|
|
249
|
+
@manager = BitClust::ScreenManager.new(
|
|
250
|
+
:templatedir => "#{datadir}/template.epub",
|
|
251
|
+
:catalogdir => "#{datadir}/catalog",
|
|
252
|
+
:encoding => 'utf-8',
|
|
253
|
+
:default_encoding => 'utf-8',
|
|
254
|
+
:urlmapper => urlmapper,
|
|
255
|
+
:target_version => '3.4'
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def class_entry
|
|
260
|
+
@lib.fetch_class('Spaceship')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def method_entry(name)
|
|
264
|
+
class_entry.fetch_method(BitClust::MethodSpec.parse("Spaceship##{name}"))
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def render_class_page
|
|
268
|
+
@manager.send(:new_screen, EpubTestClassScreen, class_entry, :database => @db).body
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def render_method_page(name)
|
|
272
|
+
@manager.send(:new_screen, EpubTestMethodScreen, [method_entry(name)], :database => @db).body
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def render_class_index_page
|
|
276
|
+
@manager.send(:new_screen, EpubTestClassIndexScreen, [class_entry], :database => @db).body
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def test_class_page_escapes_spaceship_operator_in_the_index_list
|
|
280
|
+
html = render_class_page
|
|
281
|
+
assert_include(html, '<=>')
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def test_class_page_escapes_ampersand_operator_in_the_index_list
|
|
285
|
+
html = render_class_page
|
|
286
|
+
# "&" 単体がエスケープされて "&" として出る(リンクテキストの終端)
|
|
287
|
+
assert_match(/&<\/a>/, html)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def test_class_page_is_well_formed_xml
|
|
291
|
+
html = render_class_page
|
|
292
|
+
assert_nothing_raised { REXML::Document.new(html) }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_method_page_for_spaceship_operator_is_well_formed_xml
|
|
296
|
+
html = render_method_page('<=>')
|
|
297
|
+
assert_nothing_raised { REXML::Document.new(html) }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def test_method_page_for_ampersand_operator_is_well_formed_xml
|
|
301
|
+
html = render_method_page('&')
|
|
302
|
+
assert_nothing_raised { REXML::Document.new(html) }
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_layout_void_elements_are_self_closing
|
|
306
|
+
html = render_class_page
|
|
307
|
+
assert_not_match(%r{<meta[^>]*[^/]>}, html)
|
|
308
|
+
assert_not_match(%r{<link[^>]*[^/]>}, html)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# template.epub/class の "extend:" 行も <br>(非自己終了)を出しており、
|
|
312
|
+
# そのクラスページ全体が整形式 XML にならなかった(実データベースの
|
|
313
|
+
# Array・CSV クラスで再現した。どちらも extend/aliases 行を持つ)。
|
|
314
|
+
def test_class_page_extend_line_uses_a_self_closing_br
|
|
315
|
+
html = render_class_page
|
|
316
|
+
assert_include(html, 'extend: ')
|
|
317
|
+
assert_not_match(%r{<br[^>]*[^/]>}, html)
|
|
318
|
+
assert_nothing_raised { REXML::Document.new(html) }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# template.epub/class-index には対応する <ul> の無い孤立した </ul> が
|
|
322
|
+
# あり、実データベースで OEBPS/class/index.xhtml が整形式にならなかった
|
|
323
|
+
# (template.offline/class-index にも同じ孤立タグがあるが、HTML5 として
|
|
324
|
+
# 読まれる分には実害がないため未修正のまま。EPUB の XHTML では致命的)。
|
|
325
|
+
def test_class_index_page_has_no_orphaned_closing_tag
|
|
326
|
+
html = render_class_index_page
|
|
327
|
+
assert_not_include(html, '</ul>')
|
|
328
|
+
assert_nothing_raised { REXML::Document.new(html) }
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'stringio'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'bitclust'
|
|
5
|
+
require 'bitclust/subcommands/extract_command'
|
|
6
|
+
require 'bitclust/subcommands/htmlfile_command'
|
|
7
|
+
|
|
8
|
+
class TestRdOnlyCommandsRejectMarkdown < Test::Unit::TestCase
|
|
9
|
+
def with_md_file
|
|
10
|
+
Dir.mktmpdir do |dir|
|
|
11
|
+
path = File.join(dir, 'Array.md')
|
|
12
|
+
File.write(path, "---\nlibrary: _builtin\n---\n# class Array < Object\n")
|
|
13
|
+
yield path
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def capture_stderr
|
|
18
|
+
err = StringIO.new
|
|
19
|
+
orig, $stderr = $stderr, err
|
|
20
|
+
yield
|
|
21
|
+
err.string
|
|
22
|
+
ensure
|
|
23
|
+
$stderr = orig
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_extract_explains_markdown_is_unsupported
|
|
27
|
+
with_md_file do |path|
|
|
28
|
+
cmd = BitClust::Subcommands::ExtractCommand.new
|
|
29
|
+
cmd.parse([path])
|
|
30
|
+
msg = capture_stderr { assert_raise(SystemExit) { cmd.exec([path], {:prefix => nil, :capi => false}) } }
|
|
31
|
+
assert_match(/Markdown/, msg)
|
|
32
|
+
assert_match(/refm/, msg)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_htmlfile_explains_markdown_is_unsupported
|
|
37
|
+
with_md_file do |path|
|
|
38
|
+
cmd = BitClust::Subcommands::HtmlfileCommand.new
|
|
39
|
+
cmd.parse([path])
|
|
40
|
+
msg = capture_stderr { assert_raise(SystemExit) { cmd.exec([path], {:prefix => nil, :capi => false}) } }
|
|
41
|
+
assert_match(/Markdown/, msg)
|
|
42
|
+
assert_match(/rake generate|bitclust server/, msg)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/test/test_irb_plugin.rb
CHANGED
|
@@ -3,11 +3,14 @@ require 'test/unit'
|
|
|
3
3
|
require 'tmpdir'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
require 'stringio'
|
|
6
|
+
require 'json'
|
|
6
7
|
require 'bitclust'
|
|
7
8
|
require 'bitclust/methoddatabase'
|
|
8
9
|
require 'bitclust/searcher'
|
|
10
|
+
require 'bitclust/irb'
|
|
9
11
|
|
|
10
|
-
# bitclust-irb gem の irb コマンド(refe)
|
|
12
|
+
# bitclust-irb gem の irb コマンド(refe)。リモート検索そのものは
|
|
13
|
+
# test_remote.rb で検証する。
|
|
11
14
|
#
|
|
12
15
|
# テストリスト:
|
|
13
16
|
# [x] TerminalView が io: に出力する(既定は従来どおり $stdout)
|
|
@@ -16,6 +19,11 @@ require 'bitclust/searcher'
|
|
|
16
19
|
# [x] 空のパターンは使い方を表示して例外にしない
|
|
17
20
|
# [x] 見つからないパターンはメッセージを io へ書いて例外にしない
|
|
18
21
|
# [x] require で IRB::Command::Base のサブクラスとして登録される
|
|
22
|
+
# [x] Searcher#local_database? は既定の場所に DB が無ければ false・あれば true
|
|
23
|
+
# [x] Irb.lookup: db が与えられていれば remote を使わない
|
|
24
|
+
# [x] Irb.lookup: ローカル DB が無ければ remote で検索する
|
|
25
|
+
# [x] Irb.lookup: remote が nil なら bitclust setup を案内する
|
|
26
|
+
# [x] refe の CLI(Searcher#exec)も DB が無ければ remote で検索する
|
|
19
27
|
class TestBitClustIrbPlugin < Test::Unit::TestCase
|
|
20
28
|
FOO_MD = <<~'MD'
|
|
21
29
|
---
|
|
@@ -32,6 +40,16 @@ class TestBitClustIrbPlugin < Test::Unit::TestCase
|
|
|
32
40
|
bar の説明。
|
|
33
41
|
MD
|
|
34
42
|
|
|
43
|
+
BASE_URL = 'https://docs.ruby-lang.org/ja/latest/'
|
|
44
|
+
INDEX_JS = "var search_data = #{JSON.generate(index: [
|
|
45
|
+
{ name: 'String', full_name: 'String', type: 'class', path: 'class/String.html' },
|
|
46
|
+
{ name: 'gsub', full_name: 'String#gsub', type: 'instance_method', path: 'method/String/i/gsub.html' },
|
|
47
|
+
])};\n"
|
|
48
|
+
PAGES = {
|
|
49
|
+
'js/search_data.js' => INDEX_JS,
|
|
50
|
+
'method/String/i/gsub.md' => "# String#gsub\n\n置き換えます。\n",
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
35
53
|
def build_markdown_db(dir)
|
|
36
54
|
api = File.join(dir, 'manual', 'api')
|
|
37
55
|
FileUtils.mkdir_p(File.join(api, 'foo'))
|
|
@@ -56,15 +74,44 @@ class TestBitClustIrbPlugin < Test::Unit::TestCase
|
|
|
56
74
|
end
|
|
57
75
|
end
|
|
58
76
|
|
|
77
|
+
# 既定の DB 探索(環境変数・設定ファイル)がどれも当たらない状態
|
|
78
|
+
DB_ENV_KEYS = %w[REFE2_SERVER BITCLUST_SERVER REFE2_DATADIR BITCLUST_DATADIR
|
|
79
|
+
HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME].freeze
|
|
80
|
+
|
|
81
|
+
def without_local_db
|
|
82
|
+
saved = DB_ENV_KEYS.to_h {|k| [k, ENV[k]] }
|
|
83
|
+
Dir.mktmpdir do |home|
|
|
84
|
+
DB_ENV_KEYS.each {|k| ENV.delete(k) }
|
|
85
|
+
ENV['HOME'] = home
|
|
86
|
+
yield home
|
|
87
|
+
end
|
|
88
|
+
ensure
|
|
89
|
+
saved.each {|k, v| v ? ENV[k] = v : ENV.delete(k) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def fake_remote(dir, pages = PAGES)
|
|
93
|
+
fetcher = lambda do |uri, _headers|
|
|
94
|
+
rel = uri.to_s.delete_prefix(BASE_URL)
|
|
95
|
+
pages.key?(rel) ? [200, pages[rel].dup] : [404, '']
|
|
96
|
+
end
|
|
97
|
+
BitClust::Remote.new(base_url: BASE_URL, fetcher: fetcher, cache_dir: File.join(dir, 'cache'))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def never_remote(dir)
|
|
101
|
+
BitClust::Remote.new(base_url: BASE_URL, cache_dir: File.join(dir, 'cache'),
|
|
102
|
+
fetcher: ->(_uri, _headers) { flunk 'remote must not be used' })
|
|
103
|
+
end
|
|
104
|
+
|
|
59
105
|
def test_terminal_view_writes_to_given_io
|
|
60
106
|
out = StringIO.new
|
|
61
107
|
view = BitClust::TerminalView.new(BitClust::Plain.new, {}, io: out)
|
|
62
108
|
view.send(:puts, 'hello')
|
|
63
109
|
assert_equal("hello\n", out.string)
|
|
110
|
+
assert_same(out, view.io)
|
|
111
|
+
assert_same($stdout, BitClust::TerminalView.new(BitClust::Plain.new, {}).io)
|
|
64
112
|
end
|
|
65
113
|
|
|
66
114
|
def test_lookup_method
|
|
67
|
-
require 'bitclust/irb'
|
|
68
115
|
with_db do |db|
|
|
69
116
|
out = StringIO.new
|
|
70
117
|
BitClust::Irb.lookup('Foo#bar', db: db, io: out)
|
|
@@ -74,7 +121,6 @@ class TestBitClustIrbPlugin < Test::Unit::TestCase
|
|
|
74
121
|
end
|
|
75
122
|
|
|
76
123
|
def test_lookup_class
|
|
77
|
-
require 'bitclust/irb'
|
|
78
124
|
with_db do |db|
|
|
79
125
|
out = StringIO.new
|
|
80
126
|
BitClust::Irb.lookup('Foo', db: db, io: out)
|
|
@@ -84,7 +130,6 @@ class TestBitClustIrbPlugin < Test::Unit::TestCase
|
|
|
84
130
|
end
|
|
85
131
|
|
|
86
132
|
def test_lookup_empty_pattern_shows_usage
|
|
87
|
-
require 'bitclust/irb'
|
|
88
133
|
with_db do |db|
|
|
89
134
|
out = StringIO.new
|
|
90
135
|
assert_nothing_raised do
|
|
@@ -95,7 +140,6 @@ class TestBitClustIrbPlugin < Test::Unit::TestCase
|
|
|
95
140
|
end
|
|
96
141
|
|
|
97
142
|
def test_lookup_not_found_reports_instead_of_raising
|
|
98
|
-
require 'bitclust/irb'
|
|
99
143
|
with_db do |db|
|
|
100
144
|
out = StringIO.new
|
|
101
145
|
assert_nothing_raised do
|
|
@@ -111,8 +155,58 @@ class TestBitClustIrbPlugin < Test::Unit::TestCase
|
|
|
111
155
|
rescue LoadError
|
|
112
156
|
omit 'irb >= 1.13 is not available'
|
|
113
157
|
end
|
|
114
|
-
require 'bitclust/irb'
|
|
115
158
|
assert(BitClust::Irb::RefeCommand < ::IRB::Command::Base)
|
|
116
159
|
assert_match(/るりま|リファレンス/, BitClust::Irb::RefeCommand.description)
|
|
117
160
|
end
|
|
161
|
+
|
|
162
|
+
def test_searcher_local_database_p
|
|
163
|
+
without_local_db do
|
|
164
|
+
assert_false(BitClust::Searcher.new.local_database?)
|
|
165
|
+
with_db do |db|
|
|
166
|
+
ENV['BITCLUST_DATADIR'] = db.instance_variable_get(:@prefix)
|
|
167
|
+
assert_true(BitClust::Searcher.new.local_database?)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_lookup_uses_local_db_when_given
|
|
173
|
+
with_db do |db|
|
|
174
|
+
Dir.mktmpdir do |dir|
|
|
175
|
+
out = StringIO.new
|
|
176
|
+
BitClust::Irb.lookup('Foo#bar', db: db, io: out, remote: never_remote(dir))
|
|
177
|
+
assert_match(/bar の説明。/, out.string)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_lookup_falls_back_to_remote_without_local_db
|
|
183
|
+
without_local_db do |home|
|
|
184
|
+
out = StringIO.new
|
|
185
|
+
BitClust::Irb.lookup('String#gsub', io: out, remote: fake_remote(home))
|
|
186
|
+
assert_match(/置き換えます。/, out.string)
|
|
187
|
+
assert_match(%r{method/String/i/gsub\.html}, out.string)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_lookup_without_remote_and_local_db_shows_setup_hint
|
|
192
|
+
without_local_db do
|
|
193
|
+
out = StringIO.new
|
|
194
|
+
assert_nothing_raised do
|
|
195
|
+
BitClust::Irb.lookup('String#gsub', io: out, remote: nil)
|
|
196
|
+
end
|
|
197
|
+
assert_match(/bitclust setup/, out.string)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def test_refe_cli_falls_back_to_remote_without_local_db
|
|
202
|
+
without_local_db do |home|
|
|
203
|
+
out = StringIO.new
|
|
204
|
+
searcher = BitClust::Searcher.new
|
|
205
|
+
searcher.remote = fake_remote(home)
|
|
206
|
+
searcher.parse(%w[String#gsub])
|
|
207
|
+
view = BitClust::TerminalView.new(BitClust::Plain.new, {}, io: out)
|
|
208
|
+
searcher.run_query(nil, %w[String#gsub], view)
|
|
209
|
+
assert_match(/置き換えます。/, out.string)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
118
212
|
end
|
|
@@ -91,6 +91,41 @@ class TestMarkdownBridge < Test::Unit::TestCase
|
|
|
91
91
|
out["thread.rd"]
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
def test_membership_version_and_else_wrap_include_sites
|
|
95
|
+
# bitclust#331: #@version V は #@if (version == "V")、その #@else は
|
|
96
|
+
# #@if (version != "V") で再具現化する。範囲の #@else は 2 つの
|
|
97
|
+
# membership(A 未満 / B 以上)になるので include サイトも 2 つ
|
|
98
|
+
out = build_from(
|
|
99
|
+
"builtin.md" => "---\ntype: library\n---\nbuiltin 概要。\n",
|
|
100
|
+
"onlylib.md" => "---\ntype: library\n---\nonlylib 概要。\n",
|
|
101
|
+
"range.md" => "---\ntype: library\n---\nrange 概要。\n",
|
|
102
|
+
"onlylib/Shared.md" =>
|
|
103
|
+
"---\n" \
|
|
104
|
+
"library:\n" \
|
|
105
|
+
"\#@version 3.2\n" \
|
|
106
|
+
" - onlylib\n" \
|
|
107
|
+
"\#@else\n" \
|
|
108
|
+
" - builtin\n" \
|
|
109
|
+
"\#@end\n" \
|
|
110
|
+
"\#@version 3.1...3.3\n" \
|
|
111
|
+
" - range\n" \
|
|
112
|
+
"\#@else\n" \
|
|
113
|
+
" - range\n" \
|
|
114
|
+
"\#@end\n" \
|
|
115
|
+
"---\n" \
|
|
116
|
+
"# class Shared < Object\nS。\n"
|
|
117
|
+
)
|
|
118
|
+
assert_equal "onlylib 概要。\n\n\#@if (version == \"3.2\")\n\#@include(onlylib/Shared)\n\#@end\n",
|
|
119
|
+
out["onlylib.rd"]
|
|
120
|
+
assert_equal "builtin 概要。\n\n\#@if (version != \"3.2\")\n\#@include(onlylib/Shared)\n\#@end\n",
|
|
121
|
+
out["builtin.rd"]
|
|
122
|
+
assert_equal "range 概要。\n\n" \
|
|
123
|
+
"\#@until 3.3\n\#@since 3.1\n\#@include(onlylib/Shared)\n\#@end\n\#@end\n" \
|
|
124
|
+
"\#@until 3.1\n\#@include(onlylib/Shared)\n\#@end\n" \
|
|
125
|
+
"\#@since 3.3\n\#@include(onlylib/Shared)\n\#@end\n",
|
|
126
|
+
out["range.rd"]
|
|
127
|
+
end
|
|
128
|
+
|
|
94
129
|
def test_libraries_manifest
|
|
95
130
|
out = build
|
|
96
131
|
assert_equal "bar/baz\ndual\nfoo\n\#@until 3.1\ngated\n\#@end\nsub/sub\n", out["LIBRARIES"]
|