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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/data/bitclust/template/class-index +0 -1
  3. data/data/bitclust/template.epub/class +5 -5
  4. data/data/bitclust/template.epub/class-index +0 -1
  5. data/data/bitclust/template.epub/contents +7 -4
  6. data/data/bitclust/template.epub/layout +4 -4
  7. data/data/bitclust/template.offline/class-index +0 -1
  8. data/lib/bitclust/generators/epub.rb +69 -5
  9. data/lib/bitclust/irb.rb +14 -8
  10. data/lib/bitclust/markdown_bridge.rb +11 -6
  11. data/lib/bitclust/markdown_tree.rb +118 -11
  12. data/lib/bitclust/methoddatabase.rb +17 -4
  13. data/lib/bitclust/nameutils.rb +13 -0
  14. data/lib/bitclust/preprocessor.rb +0 -36
  15. data/lib/bitclust/remote.rb +405 -0
  16. data/lib/bitclust/runner.rb +5 -10
  17. data/lib/bitclust/screen.rb +2 -1
  18. data/lib/bitclust/searcher.rb +43 -24
  19. data/lib/bitclust/subcommands/chm_command.rb +1 -1
  20. data/lib/bitclust/subcommands/epub_command.rb +2 -2
  21. data/lib/bitclust/subcommands/extract_command.rb +7 -0
  22. data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
  23. data/lib/bitclust/subcommands/methods_command.rb +5 -28
  24. data/lib/bitclust/subcommands/query_command.rb +1 -0
  25. data/lib/bitclust/subcommands/server_command.rb +21 -10
  26. data/lib/bitclust/subcommands/setup_command.rb +28 -10
  27. data/lib/bitclust/syntax_highlighter.rb +65 -4
  28. data/lib/bitclust/user_dirs.rb +62 -0
  29. data/lib/bitclust/version.rb +1 -1
  30. data/test/test_chm_command.rb +22 -0
  31. data/test/test_epub_command.rb +60 -0
  32. data/test/test_epub_generator.rb +330 -0
  33. data/test/test_extract_command.rb +45 -0
  34. data/test/test_irb_plugin.rb +100 -6
  35. data/test/test_markdown_bridge.rb +35 -0
  36. data/test/test_markdown_tree.rb +144 -0
  37. data/test/test_mdparser.rb +39 -0
  38. data/test/test_methods_command.rb +32 -0
  39. data/test/test_opensearchdescription_screen.rb +39 -0
  40. data/test/test_query_command.rb +30 -0
  41. data/test/test_remote.rb +374 -0
  42. data/test/test_runner.rb +61 -1
  43. data/test/test_searcher.rb +63 -0
  44. data/test/test_server_command.rb +41 -0
  45. data/test/test_setup_command.rb +143 -0
  46. data/test/test_syntax_highlighter.rb +124 -0
  47. data/test/test_user_dirs.rb +62 -0
  48. metadata +37 -2
  49. 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, '&lt;=&gt;')
282
+ end
283
+
284
+ def test_class_page_escapes_ampersand_operator_in_the_index_list
285
+ html = render_class_page
286
+ # "&" 単体がエスケープされて "&amp;" として出る(リンクテキストの終端)
287
+ assert_match(/&amp;<\/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
@@ -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"]