bitclust-core 1.6.1 → 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/Rakefile +1 -0
- data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
- data/data/bitclust/template/class-index +0 -1
- data/data/bitclust/template/function-index +1 -0
- data/data/bitclust/template/layout +1 -0
- 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/data/bitclust/template.offline/function-index +1 -0
- data/data/bitclust/template.offline/layout +3 -0
- data/lib/bitclust/classentry.rb +1 -1
- data/lib/bitclust/docentry.rb +1 -1
- data/lib/bitclust/functionentry.rb +1 -1
- data/lib/bitclust/generators/epub.rb +69 -5
- data/lib/bitclust/irb.rb +74 -0
- data/lib/bitclust/markdown_bridge.rb +11 -6
- data/lib/bitclust/markdown_exporter.rb +311 -0
- data/lib/bitclust/markdown_tree.rb +118 -11
- data/lib/bitclust/mdcompiler.rb +15 -1
- data/lib/bitclust/mdparser.rb +42 -1
- data/lib/bitclust/methoddatabase.rb +17 -4
- data/lib/bitclust/methodentry.rb +24 -0
- data/lib/bitclust/methodsignature.rb +1 -1
- data/lib/bitclust/nameutils.rb +13 -0
- data/lib/bitclust/preprocessor.rb +4 -37
- data/lib/bitclust/rbs_overload_matcher.rb +179 -0
- data/lib/bitclust/rbs_sig_importer.rb +276 -0
- data/lib/bitclust/rbs_signatures.rb +125 -0
- data/lib/bitclust/rdcompiler.rb +12 -1
- data/lib/bitclust/remote.rb +405 -0
- data/lib/bitclust/runner.rb +7 -10
- data/lib/bitclust/screen.rb +6 -1
- data/lib/bitclust/search_index_generator.rb +28 -10
- data/lib/bitclust/searcher.rb +54 -25
- 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/rbssig_command.rb +99 -0
- data/lib/bitclust/subcommands/server_command.rb +21 -10
- data/lib/bitclust/subcommands/setup_command.rb +32 -11
- data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
- data/lib/bitclust/syntax_highlighter.rb +65 -4
- data/lib/bitclust/user_dirs.rb +62 -0
- data/lib/bitclust/version.rb +1 -1
- data/lib/bitclust-irb.rb +2 -0
- 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 +212 -0
- data/test/test_link_checker.rb +1 -1
- data/test/test_markdown_bridge.rb +35 -0
- data/test/test_markdown_exporter.rb +260 -0
- data/test/test_markdown_tree.rb +144 -0
- data/test/test_mdcompiler.rb +47 -5
- data/test/test_mdparser.rb +223 -4
- data/test/test_methodentry.rb +111 -0
- data/test/test_methods_command.rb +32 -0
- data/test/test_methodsignature.rb +3 -1
- data/test/test_opensearchdescription_screen.rb +39 -0
- data/test/test_preprocessor.rb +26 -0
- data/test/test_query_command.rb +30 -0
- data/test/test_rbs_overload_matcher.rb +170 -0
- data/test/test_rbs_sig_importer.rb +237 -0
- data/test/test_rbssig_command.rb +186 -0
- data/test/test_rdcompiler.rb +99 -1
- data/test/test_remote.rb +374 -0
- data/test/test_runner.rb +61 -1
- data/test/test_search_index_generator.rb +81 -0
- data/test/test_searcher.rb +63 -0
- data/test/test_server_command.rb +41 -0
- data/test/test_setup_command.rb +164 -0
- data/test/test_statichtml_command.rb +137 -0
- data/test/test_syntax_highlighter.rb +124 -0
- data/test/test_user_dirs.rb +62 -0
- data/theme/default/js/run.js +14 -0
- data/theme/default/js/search_init.js +3 -1
- data/theme/default/js/search_page.js +7 -1
- data/theme/default/js/version_switcher.js +147 -0
- data/theme/default/search.css +21 -0
- data/theme/default/style.css +83 -7
- data/theme/lillia/style.css +26 -0
- metadata +71 -2
- data/lib/bitclust/ridatabase.rb +0 -359
data/test/test_rdcompiler.rb
CHANGED
|
@@ -28,9 +28,102 @@ class TestRDCompiler < Test::Unit::TestCase
|
|
|
28
28
|
mock(method_entry).names.any_times{ names }
|
|
29
29
|
mock(method_entry).since_map.any_times{ since_map }
|
|
30
30
|
mock(method_entry).until_map.any_times{ until_map }
|
|
31
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
31
32
|
assert_equal(expected, @c.compile_method(method_entry))
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
# RBS シグネチャ表示: rbs_sig property を持つエントリだけが、メソッド
|
|
36
|
+
# 見出しの <dt> 群の直後・説明 <dd> の直前に 1 オーバーロード 1 行の
|
|
37
|
+
# <dt class="rbs-signature"> を出す(dd だと説明の一部に見える。pre を
|
|
38
|
+
# 使うと script.js が COPY ボタンを付けてしまうので code のみ)。
|
|
39
|
+
# 型名セグメントは DB に存在するクラスだけリンク化し、テキストは
|
|
40
|
+
# エスケープした上で -> を → にする。
|
|
41
|
+
def test_method_with_rbs_signatures_renders_dt_lines
|
|
42
|
+
stub(@db).fetch_class('String') { :exists }
|
|
43
|
+
stub(@db).fetch_class('Integer') { raise BitClust::ClassNotFound, 'Integer' }
|
|
44
|
+
overloads = [
|
|
45
|
+
{'segments' => [['t', '('], ['c', 'Integer'], ['t', ') -> '], ['c', 'String']]},
|
|
46
|
+
{'segments' => [['t', '(String & Integer) -> void']]},
|
|
47
|
+
]
|
|
48
|
+
method_entry = rbs_method_entry("--- index(val) -> Integer\n\n説明\n", overloads)
|
|
49
|
+
|
|
50
|
+
html = @c.compile_method(method_entry)
|
|
51
|
+
assert_include html,
|
|
52
|
+
%Q[(Integer) → <a href="dummy/class/String">String</a>]
|
|
53
|
+
assert_include html, '(String & Integer) → void'
|
|
54
|
+
assert_match(
|
|
55
|
+
%r!</dt>\n(<dt class="rbs-signature"><code>.+</code></dt>\n){2}<dd class="method-description">!,
|
|
56
|
+
html)
|
|
57
|
+
assert_not_match(/<pre|<dd class="rbs-/, html)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 引数パターンでチャンクが分かれるメソッド(Array.new 型)では、各
|
|
61
|
+
# オーバーロードが引数名・アリティ・ブロック有無で最も近いチャンクの
|
|
62
|
+
# 見出し直下にだけ出る(全チャンクへの重複表示をしない)
|
|
63
|
+
def test_method_with_rbs_signatures_assigns_overloads_to_chunks
|
|
64
|
+
src = <<~RD
|
|
65
|
+
--- index(sub) -> Integer | nil
|
|
66
|
+
|
|
67
|
+
文字列で探す。
|
|
68
|
+
|
|
69
|
+
--- index(pattern) {|s| ... } -> Integer | nil
|
|
70
|
+
|
|
71
|
+
ブロックで探す。
|
|
72
|
+
RD
|
|
73
|
+
overloads = [
|
|
74
|
+
{'segments' => [['t', '(String sub) -> Integer?']],
|
|
75
|
+
'params' => ['sub'], 'arity' => [1, 1]},
|
|
76
|
+
{'segments' => [['t', '(Regexp pattern) { (String) -> bool } -> Integer?']],
|
|
77
|
+
'params' => ['pattern'], 'arity' => [1, 1], 'block' => 'req'},
|
|
78
|
+
]
|
|
79
|
+
method_entry = rbs_method_entry(src, overloads)
|
|
80
|
+
|
|
81
|
+
html = @c.compile_method(method_entry)
|
|
82
|
+
assert_match(
|
|
83
|
+
%r!<code>index\(sub\).*?</dt>\n<dt class="rbs-signature"><code>\(String sub\) → Integer\?</code></dt>\n<dd!m,
|
|
84
|
+
html)
|
|
85
|
+
assert_match(
|
|
86
|
+
%r!<code>index\(pattern\).*?</dt>\n<dt class="rbs-signature"><code>\(Regexp pattern\) \{ \(String\) → bool \} → Integer\?</code></dt>\n<dd!m,
|
|
87
|
+
html)
|
|
88
|
+
assert_equal 2, html.scan('rbs-signature').size
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 旧形式の DB(メタ情報なし)ではチャンク振り分けができないので、
|
|
92
|
+
# 先頭チャンクにまとめて出す(重複はしない)
|
|
93
|
+
def test_method_with_rbs_signatures_without_meta_falls_back_to_first_chunk
|
|
94
|
+
src = <<~RD
|
|
95
|
+
--- index(sub) -> Integer | nil
|
|
96
|
+
|
|
97
|
+
文字列で探す。
|
|
98
|
+
|
|
99
|
+
--- index(pattern) {|s| ... } -> Integer | nil
|
|
100
|
+
|
|
101
|
+
ブロックで探す。
|
|
102
|
+
RD
|
|
103
|
+
overloads = [
|
|
104
|
+
{'segments' => [['t', '(String sub) -> Integer?']]},
|
|
105
|
+
{'segments' => [['t', '(Regexp pattern) { (String) -> bool } -> Integer?']]},
|
|
106
|
+
]
|
|
107
|
+
html = @c.compile_method(rbs_method_entry(src, overloads))
|
|
108
|
+
assert_equal 2, html.scan('rbs-signature').size
|
|
109
|
+
assert_match(
|
|
110
|
+
%r!<code>index\(sub\).*?</dt>\n(<dt class="rbs-signature">.*\n){2}<dd!,
|
|
111
|
+
html)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def rbs_method_entry(src, overloads)
|
|
115
|
+
method_entry = Object.new
|
|
116
|
+
mock(method_entry).source { src }
|
|
117
|
+
mock(method_entry).index_id.any_times { 'dummy' }
|
|
118
|
+
mock(method_entry).defined?.any_times { true }
|
|
119
|
+
mock(method_entry).id.any_times { 'String/i.index._builtin' }
|
|
120
|
+
mock(method_entry).names.any_times { [] }
|
|
121
|
+
mock(method_entry).since_map.any_times { {} }
|
|
122
|
+
mock(method_entry).until_map.any_times { {} }
|
|
123
|
+
mock(method_entry).rbs_signature_overloads.any_times { overloads }
|
|
124
|
+
method_entry
|
|
125
|
+
end
|
|
126
|
+
|
|
34
127
|
# badge のカタログ訳(「Ruby %s から」等)を検証するテストで使う、
|
|
35
128
|
# 日本語カタログを読み込んだ compiler を作る(setup の @c はカタログ無しの
|
|
36
129
|
# C ロケールなので、_() はキーそのまま(英語)を返してしまう)
|
|
@@ -230,6 +323,7 @@ HERE
|
|
|
230
323
|
mock(method_entry).names.any_times { ['mf'] }
|
|
231
324
|
mock(method_entry).since_map.any_times { {} }
|
|
232
325
|
mock(method_entry).until_map.any_times { {} }
|
|
326
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
233
327
|
mock(method_entry).klass.any_times { klass }
|
|
234
328
|
mock(method_entry).display_typemark.any_times { '?.' }
|
|
235
329
|
|
|
@@ -250,6 +344,7 @@ HERE
|
|
|
250
344
|
mock(method_entry).names.any_times { ['mf'] }
|
|
251
345
|
mock(method_entry).since_map.any_times { {} }
|
|
252
346
|
mock(method_entry).until_map.any_times { {} }
|
|
347
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
253
348
|
|
|
254
349
|
html = @c.compile_method(method_entry)
|
|
255
350
|
assert_include(html, '<code>mf</code>')
|
|
@@ -979,7 +1074,7 @@ HERE
|
|
|
979
1074
|
|
|
980
1075
|
data("library root" => ['[[lib:/]]', '<a href="dummy/library/">ライブラリ一覧</a>'],
|
|
981
1076
|
"builtin library" => ['[[lib:_builtin]]', '<a href="dummy/library/_builtin">組み込みライブラリ</a>'],
|
|
982
|
-
"C API root" => ['[[f:/]]', '<a href="dummy/function/"
|
|
1077
|
+
"C API root" => ['[[f:/]]', '<a href="dummy/function/">C API 関数一覧</a>'])
|
|
983
1078
|
def test_bracket_link_with_catalog(data)
|
|
984
1079
|
target, expected = data
|
|
985
1080
|
prefix = File.expand_path('../data/bitclust/catalog', __dir__)
|
|
@@ -1029,6 +1124,7 @@ HERE
|
|
|
1029
1124
|
mock(method_entry).names.any_times { [] }
|
|
1030
1125
|
mock(method_entry).since_map.any_times { {} }
|
|
1031
1126
|
mock(method_entry).until_map.any_times { {} }
|
|
1127
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
1032
1128
|
html = @c.compile_method(method_entry)
|
|
1033
1129
|
assert_not_include(html, 'UNKNOWN_META_INFO')
|
|
1034
1130
|
assert_not_include(html, '{:')
|
|
@@ -1052,6 +1148,7 @@ HERE
|
|
|
1052
1148
|
mock(method_entry).names.any_times { [] }
|
|
1053
1149
|
mock(method_entry).since_map.any_times { {} }
|
|
1054
1150
|
mock(method_entry).until_map.any_times { {} }
|
|
1151
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
1055
1152
|
html = @c.compile_method(method_entry)
|
|
1056
1153
|
assert_not_include(html, '{:')
|
|
1057
1154
|
assert_include(html, 'to_a2')
|
|
@@ -1074,6 +1171,7 @@ HERE
|
|
|
1074
1171
|
mock(method_entry).names.any_times { [] }
|
|
1075
1172
|
mock(method_entry).since_map.any_times { {} }
|
|
1076
1173
|
mock(method_entry).until_map.any_times { {} }
|
|
1174
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
1077
1175
|
html = @c.compile_method(method_entry)
|
|
1078
1176
|
assert_not_include(html, '{:')
|
|
1079
1177
|
assert_include(html, 'このメソッドは定義されていません。')
|
data/test/test_remote.rb
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'test/unit'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'time'
|
|
8
|
+
require 'bitclust'
|
|
9
|
+
require 'bitclust/searcher'
|
|
10
|
+
require 'bitclust/remote'
|
|
11
|
+
|
|
12
|
+
# ローカル DB が無いときのリモート検索(docs.ruby-lang.org の検索索引
|
|
13
|
+
# search_data.js と Markdown 配信をキャッシュ付きで使う)。
|
|
14
|
+
#
|
|
15
|
+
# テストリスト:
|
|
16
|
+
# Cache:
|
|
17
|
+
# [x] 未取得なら GET して保存し本文を返す
|
|
18
|
+
# [x] 1 日以内のキャッシュはネットワークに行かずに返す
|
|
19
|
+
# [x] 1 日以上古ければ If-Modified-Since 付きで確認し、304 なら touch して返す
|
|
20
|
+
# [x] 200 なら上書き保存する
|
|
21
|
+
# [x] 404 はキャッシュを消して nil
|
|
22
|
+
# [x] 接続エラー時にキャッシュがあれば古くても返して stale にする。無ければ例外
|
|
23
|
+
# [x] 本文は UTF-8 として扱う
|
|
24
|
+
# Index:
|
|
25
|
+
# [x] search_data.js を読んでクラス・メソッド(path から クラス・種別・名前)・ライブラリ・関数にする
|
|
26
|
+
# [x] メソッド指定は refe と同じ規則(クラスは大小無視の前方一致→完全一致、種別は typemark)
|
|
27
|
+
# [x] Kernel.#printf と Kernel?.printf は同じ module function。Kernel.printf は特異メソッドなので該当なし
|
|
28
|
+
# [x] 種別なし 2 語は全種別から探す
|
|
29
|
+
# [x] 3 語の種別は typemark として検査する(不正・$ は InvalidKey)
|
|
30
|
+
# [x] $ 始まりは特殊変数
|
|
31
|
+
# [x] A::B はクラス → 定数の順
|
|
32
|
+
# [x] 大文字 1 語はクラス → メソッド名の順、小文字 1 語はメソッド名 → クラス → ライブラリ → 関数の順
|
|
33
|
+
# [x] 語なしは全クラス、class_only: はクラスだけ
|
|
34
|
+
# [x] document・heading は検索対象にしない
|
|
35
|
+
# Remote#lookup:
|
|
36
|
+
# [x] 1 件に決まれば .md を取得して本文と出典 URL を書く
|
|
37
|
+
# [x] 複数なら名前一覧(line: なら 1 行 1 件、describe_all: なら全件の本文)
|
|
38
|
+
# [x] 見つからなければメッセージと検索ページ URL
|
|
39
|
+
# [x] 索引が取れずキャッシュも無ければメッセージだけ(例外にしない)
|
|
40
|
+
# [x] Remote.default は BITCLUST_REMOTE_URL が指定があればその URL
|
|
41
|
+
# [x] Remote.default は BITCLUST_REMOTE_URL が none(大小無視)か空なら nil(Windows では空の環境変数を作れない)
|
|
42
|
+
class TestRemote < Test::Unit::TestCase
|
|
43
|
+
BASE_URL = 'https://docs.ruby-lang.org/ja/latest/'
|
|
44
|
+
|
|
45
|
+
INDEX = {
|
|
46
|
+
index: [
|
|
47
|
+
{ name: 'String', full_name: 'String', type: 'class', path: 'class/String.html' },
|
|
48
|
+
{ name: 'StringIO', full_name: 'StringIO', type: 'class', path: 'class/StringIO.html' },
|
|
49
|
+
{ name: 'Kernel', full_name: 'Kernel', type: 'module', path: 'class/Kernel.html' },
|
|
50
|
+
{ name: 'File', full_name: 'File', type: 'class', path: 'class/File.html' },
|
|
51
|
+
{ name: 'File::Stat', full_name: 'File::Stat', type: 'class', path: 'class/File=3a=3aStat.html' },
|
|
52
|
+
{ name: 'ARGF', full_name: 'ARGF', type: 'object', path: 'class/ARGF.html' },
|
|
53
|
+
{ name: 'gsub', full_name: 'String#gsub', type: 'instance_method', path: 'method/String/i/gsub.html' },
|
|
54
|
+
{ name: 'gsub!', full_name: 'String#gsub!', type: 'instance_method', path: 'method/String/i/gsub=21.html' },
|
|
55
|
+
{ name: 'size', full_name: 'String#size', type: 'instance_method', path: 'method/String/i/size.html' },
|
|
56
|
+
{ name: 'size', full_name: 'StringIO#size', type: 'instance_method', path: 'method/StringIO/i/size.html' },
|
|
57
|
+
{ name: 'size', full_name: 'File#size', type: 'instance_method', path: 'method/File/i/size.html' },
|
|
58
|
+
{ name: 'size', full_name: 'File.size', type: 'class_method', path: 'method/File/s/size.html', match_name: 'File::size' },
|
|
59
|
+
{ name: 'new', full_name: 'String.new', type: 'class_method', path: 'method/String/s/new.html', match_name: 'String::new' },
|
|
60
|
+
{ name: 'printf', full_name: 'Kernel?.printf', type: 'class_method', path: 'method/Kernel/m/printf.html', match_name: 'Kernel::#printf' },
|
|
61
|
+
{ name: 'SEPARATOR', full_name: 'File::SEPARATOR', type: 'constant', path: 'method/File/c/SEPARATOR.html' },
|
|
62
|
+
{ name: '$stdout', full_name: '$stdout', type: 'variable', path: 'method/Kernel/v/stdout.html' },
|
|
63
|
+
{ name: 'json', full_name: 'json', type: 'library', path: 'library/json.html' },
|
|
64
|
+
{ name: 'net/http', full_name: 'net/http', type: 'library', path: 'library/net=2fhttp.html' },
|
|
65
|
+
{ name: 'rb_str_new', full_name: 'rb_str_new', type: 'function', path: 'function/rb_str_new.html' },
|
|
66
|
+
{ name: 'ReFe', full_name: 'ReFe', type: 'document', path: 'doc/ReFe.html' },
|
|
67
|
+
{ name: 'Zzz', full_name: 'Zzz (Ruby用語集)', type: 'heading', path: 'doc/glossary.html#Zzz' },
|
|
68
|
+
],
|
|
69
|
+
}.freeze
|
|
70
|
+
INDEX_JS = "var search_data = #{JSON.generate(INDEX)};\n"
|
|
71
|
+
|
|
72
|
+
PAGES = {
|
|
73
|
+
'js/search_data.js' => INDEX_JS,
|
|
74
|
+
'method/String/i/gsub.md' => "# String#gsub\n\n### def gsub(pattern, replace) -> String\n\n置き換えます。\n",
|
|
75
|
+
'method/String/i/size.md' => "# String#size\n\n長さです。\n",
|
|
76
|
+
'method/File/s/size.md' => "# File.size\n\nサイズです。\n",
|
|
77
|
+
'method/File/i/size.md' => "# File#size\n\nサイズです。\n",
|
|
78
|
+
'method/StringIO/i/size.md' => "# StringIO#size\n\nサイズです。\n",
|
|
79
|
+
'class/String.md' => "# class String < Object\n\n文字列のクラスです。\n",
|
|
80
|
+
'library/json.md' => "# library json\n\nJSON です。\n",
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
T0 = Time.utc(2026, 9, 2, 12, 0, 0)
|
|
84
|
+
|
|
85
|
+
def setup
|
|
86
|
+
@dir = Dir.mktmpdir
|
|
87
|
+
@saved_env = ENV['BITCLUST_REMOTE_URL']
|
|
88
|
+
ENV.delete('BITCLUST_REMOTE_URL')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def teardown
|
|
92
|
+
FileUtils.rm_rf(@dir)
|
|
93
|
+
@saved_env ? ENV['BITCLUST_REMOTE_URL'] = @saved_env : ENV.delete('BITCLUST_REMOTE_URL')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# pages: base_url からの相対パス => 本文。無いパスは 404。requests に
|
|
97
|
+
# [URL, ヘッダ] を記録する
|
|
98
|
+
def fake_fetcher(pages = PAGES, requests = [])
|
|
99
|
+
lambda do |uri, headers|
|
|
100
|
+
requests << [uri.to_s, headers]
|
|
101
|
+
rel = uri.to_s.delete_prefix(BASE_URL)
|
|
102
|
+
pages.key?(rel) ? [200, pages[rel].dup] : [404, '<Error><Code>AccessDenied</Code></Error>']
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def remote(pages = PAGES, requests = [], now: T0)
|
|
107
|
+
BitClust::Remote.new(base_url: BASE_URL, fetcher: fake_fetcher(pages, requests),
|
|
108
|
+
cache_dir: @dir, clock: -> { now })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def index
|
|
112
|
+
BitClust::Remote::Index.parse(INDEX_JS)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def names(entries)
|
|
116
|
+
entries.map(&:full_name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def cache(fetcher, now: T0)
|
|
120
|
+
BitClust::Remote::Cache.new(dir: @dir, fetcher: fetcher, clock: -> { now })
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def gsub_uri
|
|
124
|
+
URI("#{BASE_URL}method/String/i/gsub.md")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# --- Cache ---
|
|
128
|
+
|
|
129
|
+
def test_cache_fetches_and_stores
|
|
130
|
+
requests = []
|
|
131
|
+
hit = cache(fake_fetcher(PAGES, requests)).fetch(gsub_uri)
|
|
132
|
+
assert_match(/置き換えます。/, hit.body)
|
|
133
|
+
assert_false(hit.stale)
|
|
134
|
+
assert_equal([[gsub_uri.to_s, {}]], requests)
|
|
135
|
+
path = File.join(@dir, 'docs.ruby-lang.org', 'ja', 'latest', 'method', 'String', 'i', 'gsub.md')
|
|
136
|
+
assert_equal(PAGES['method/String/i/gsub.md'], File.read(path, encoding: 'UTF-8'))
|
|
137
|
+
assert_equal(T0, File.mtime(path))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_cache_reuses_fresh_copy_without_network
|
|
141
|
+
requests = []
|
|
142
|
+
c = cache(fake_fetcher(PAGES, requests))
|
|
143
|
+
c.fetch(gsub_uri)
|
|
144
|
+
later = cache(fake_fetcher(PAGES, requests), now: T0 + 23 * 3600)
|
|
145
|
+
hit = later.fetch(gsub_uri)
|
|
146
|
+
assert_match(/置き換えます。/, hit.body)
|
|
147
|
+
assert_equal(1, requests.size)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_cache_revalidates_old_copy_with_if_modified_since
|
|
151
|
+
requests = []
|
|
152
|
+
cache(fake_fetcher(PAGES, requests)).fetch(gsub_uri)
|
|
153
|
+
not_modified = lambda do |uri, headers|
|
|
154
|
+
requests << [uri.to_s, headers]
|
|
155
|
+
[304, '']
|
|
156
|
+
end
|
|
157
|
+
now = T0 + 25 * 3600
|
|
158
|
+
hit = cache(not_modified, now: now).fetch(gsub_uri)
|
|
159
|
+
assert_match(/置き換えます。/, hit.body)
|
|
160
|
+
assert_false(hit.stale)
|
|
161
|
+
assert_equal(2, requests.size)
|
|
162
|
+
assert_equal({ 'If-Modified-Since' => T0.httpdate }, requests.last[1])
|
|
163
|
+
assert_equal(now, File.mtime(hit.path))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_cache_overwrites_on_200
|
|
167
|
+
cache(fake_fetcher(PAGES)).fetch(gsub_uri)
|
|
168
|
+
updated = PAGES.merge('method/String/i/gsub.md' => "# String#gsub\n\n新しい説明。\n")
|
|
169
|
+
hit = cache(fake_fetcher(updated), now: T0 + 2 * 86400).fetch(gsub_uri)
|
|
170
|
+
assert_match(/新しい説明。/, hit.body)
|
|
171
|
+
assert_equal("# String#gsub\n\n新しい説明。\n", File.read(hit.path, encoding: 'UTF-8'))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_cache_removes_copy_on_404
|
|
175
|
+
c = cache(fake_fetcher(PAGES))
|
|
176
|
+
hit = c.fetch(gsub_uri)
|
|
177
|
+
assert_nil(cache(fake_fetcher({}), now: T0 + 2 * 86400).fetch(gsub_uri))
|
|
178
|
+
assert_false(File.exist?(hit.path))
|
|
179
|
+
assert_nil(cache(fake_fetcher({})).fetch(URI("#{BASE_URL}class/Nope.md")))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_cache_serves_stale_copy_when_offline
|
|
183
|
+
failing = ->(_uri, _headers) { raise SocketError, 'getaddrinfo failed' }
|
|
184
|
+
assert_raise(SocketError) { cache(failing).fetch(gsub_uri) }
|
|
185
|
+
cache(fake_fetcher(PAGES)).fetch(gsub_uri)
|
|
186
|
+
hit = cache(failing, now: T0 + 2 * 86400).fetch(gsub_uri)
|
|
187
|
+
assert_match(/置き換えます。/, hit.body)
|
|
188
|
+
assert_true(hit.stale)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_cache_body_is_utf8
|
|
192
|
+
binary = { 'class/String.md' => "# class String < Object\n\n文字列のクラスです。\n".b }
|
|
193
|
+
hit = cache(fake_fetcher(binary)).fetch(URI("#{BASE_URL}class/String.md"))
|
|
194
|
+
assert_equal(Encoding::UTF_8, hit.body.encoding)
|
|
195
|
+
assert_true(hit.body.valid_encoding?)
|
|
196
|
+
again = cache(fake_fetcher({})).fetch(URI("#{BASE_URL}class/String.md"))
|
|
197
|
+
assert_equal(Encoding::UTF_8, again.body.encoding)
|
|
198
|
+
assert_match(/文字列のクラスです。/, again.body)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# --- Index ---
|
|
202
|
+
|
|
203
|
+
def test_index_parses_entries
|
|
204
|
+
i = index
|
|
205
|
+
e = i.search(%w[String#gsub]).first
|
|
206
|
+
assert_equal(['String', 'i', 'gsub', 'method/String/i/gsub.html'], [e.klass, e.typechar, e.name, e.path])
|
|
207
|
+
assert_equal(%w[File.size], names(i.search(%w[File.size])))
|
|
208
|
+
assert_equal(%w[net/http], names(i.search(%w[net/http])))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def test_index_method_spec_follows_refe_rules
|
|
212
|
+
i = index
|
|
213
|
+
assert_equal(%w[String#gsub], names(i.search(%w[String#gsub])))
|
|
214
|
+
assert_equal(%w[String#gsub], names(i.search(%w[str#gsub])))
|
|
215
|
+
assert_equal(%w[String#gsub String#gsub!], names(i.search(%w[Str#gs])))
|
|
216
|
+
assert_equal(%w[String#gsub!], names(i.search(%w[String#gsub!])))
|
|
217
|
+
assert_equal(%w[String.new], names(i.search(%w[String.new])))
|
|
218
|
+
assert_equal(%w[String#gsub], names(i.search(%w[String,gsub])))
|
|
219
|
+
assert_equal([], names(i.search(%w[String#nope])))
|
|
220
|
+
assert_equal([], names(i.search(%w[Nope#gsub])))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_index_module_function_typemarks
|
|
224
|
+
i = index
|
|
225
|
+
assert_equal(%w[Kernel?.printf], names(i.search(%w[Kernel.#printf])))
|
|
226
|
+
assert_equal(%w[Kernel?.printf], names(i.search(%w[Kernel?.printf])))
|
|
227
|
+
assert_equal([], names(i.search(%w[Kernel.printf])))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_index_two_words_search_all_types
|
|
231
|
+
i = index
|
|
232
|
+
assert_equal(%w[File#size File.size], names(i.search(%w[File size])))
|
|
233
|
+
assert_equal(%w[String#size StringIO#size], names(i.search(%w[str size])))
|
|
234
|
+
assert_equal([], names(i.search(%w[String nope])))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_index_three_words_check_typemark
|
|
238
|
+
i = index
|
|
239
|
+
assert_equal(%w[File.size], names(i.search(%w[File . size])))
|
|
240
|
+
assert_equal(%w[File#size], names(i.search(%w[File # size])))
|
|
241
|
+
assert_raise(BitClust::InvalidKey) { i.search(%w[File x size]) }
|
|
242
|
+
assert_raise(BitClust::InvalidKey) { i.search(%w[Kernel $ stdout]) }
|
|
243
|
+
assert_raise(BitClust::InvalidKey) { i.search(%w[a b c d]) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def test_index_special_variable
|
|
247
|
+
i = index
|
|
248
|
+
assert_equal(%w[$stdout], names(i.search(%w[$stdout])))
|
|
249
|
+
assert_equal(%w[$stdout], names(i.search(%w[$std])))
|
|
250
|
+
assert_equal([], names(i.search(%w[$nope])))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_index_scoped_name_prefers_class_then_constant
|
|
254
|
+
i = index
|
|
255
|
+
assert_equal(%w[File::Stat], names(i.search(%w[File::Stat])))
|
|
256
|
+
assert_equal(%w[File::SEPARATOR], names(i.search(%w[File::SEPARATOR])))
|
|
257
|
+
assert_equal([], names(i.search(%w[File::NOPE])))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def test_index_single_word_order
|
|
261
|
+
i = index
|
|
262
|
+
assert_equal(%w[String], names(i.search(%w[String])))
|
|
263
|
+
assert_equal(%w[String StringIO], names(i.search(%w[S])))
|
|
264
|
+
assert_equal(%w[File#size File.size String#size StringIO#size], names(i.search(%w[size])).sort)
|
|
265
|
+
assert_equal(%w[Kernel?.printf], names(i.search(%w[printf])))
|
|
266
|
+
assert_equal(%w[json], names(i.search(%w[json])))
|
|
267
|
+
assert_equal(%w[rb_str_new], names(i.search(%w[rb_str_new])))
|
|
268
|
+
assert_equal(%w[ARGF], names(i.search(%w[argf])))
|
|
269
|
+
assert_equal([], names(i.search(%w[nothing])))
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_index_no_words_and_class_only
|
|
273
|
+
i = index
|
|
274
|
+
assert_equal(%w[ARGF File File::Stat Kernel String StringIO], names(i.search([])).sort)
|
|
275
|
+
assert_equal(%w[String StringIO], names(i.search(%w[S], class_only: true)))
|
|
276
|
+
assert_equal([], names(i.search(%w[size], class_only: true)))
|
|
277
|
+
assert_raise(BitClust::InvalidKey) { i.search(%w[a b], class_only: true) }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def test_index_ignores_documents_and_headings
|
|
281
|
+
i = index
|
|
282
|
+
assert_equal([], names(i.search(%w[ReFe])))
|
|
283
|
+
assert_equal([], names(i.search(%w[Zzz])))
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# --- Remote#lookup ---
|
|
287
|
+
|
|
288
|
+
def test_lookup_single_hit_shows_page_with_source_url
|
|
289
|
+
requests = []
|
|
290
|
+
out = StringIO.new
|
|
291
|
+
assert_true(remote(PAGES, requests).lookup(%w[String#gsub], out))
|
|
292
|
+
assert_match(/\A# String#gsub$/, out.string)
|
|
293
|
+
assert_match(/置き換えます。/, out.string)
|
|
294
|
+
assert_match(%r{^https://docs.ruby-lang.org/ja/latest/method/String/i/gsub.html$}, out.string)
|
|
295
|
+
assert_equal(["#{BASE_URL}js/search_data.js", "#{BASE_URL}method/String/i/gsub.md"], requests.map(&:first))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def test_lookup_multiple_hits_lists_names
|
|
299
|
+
out = StringIO.new
|
|
300
|
+
assert_true(remote.lookup(%w[size], out))
|
|
301
|
+
assert_equal("File#size File.size String#size StringIO#size \n", out.string)
|
|
302
|
+
out = StringIO.new
|
|
303
|
+
remote.lookup(%w[size], out, line: true)
|
|
304
|
+
assert_equal("File#size\nFile.size\nString#size\nStringIO#size\n", out.string)
|
|
305
|
+
out = StringIO.new
|
|
306
|
+
remote.lookup(%w[size], out, describe_all: true)
|
|
307
|
+
assert_match(/# File#size.*# File\.size.*# String#size.*# StringIO#size/m, out.string)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_lookup_single_hit_with_line_prints_name_only
|
|
311
|
+
out = StringIO.new
|
|
312
|
+
remote.lookup(%w[String#gsub], out, line: true)
|
|
313
|
+
assert_equal("String#gsub\n", out.string)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def test_lookup_class_only
|
|
317
|
+
out = StringIO.new
|
|
318
|
+
remote.lookup(%w[String], out, class_only: true)
|
|
319
|
+
assert_match(/文字列のクラスです。/, out.string)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_lookup_not_found_shows_search_url
|
|
323
|
+
out = StringIO.new
|
|
324
|
+
assert_nothing_raised do
|
|
325
|
+
assert_false(remote.lookup(%w[NoSuchClass#nope], out))
|
|
326
|
+
end
|
|
327
|
+
assert_match(/no such method: NoSuchClass#nope/, out.string)
|
|
328
|
+
assert_match(%r{https://docs.ruby-lang.org/ja/search/\?q=NoSuchClass%23nope}, out.string)
|
|
329
|
+
out = StringIO.new
|
|
330
|
+
assert_false(remote.lookup(%w[Nope], out, class_only: true))
|
|
331
|
+
assert_match(/no such class: Nope/, out.string)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def test_lookup_without_index_reports_instead_of_raising
|
|
335
|
+
failing = BitClust::Remote.new(base_url: BASE_URL, cache_dir: @dir, clock: -> { T0 },
|
|
336
|
+
fetcher: ->(_uri, _headers) { raise SocketError, 'getaddrinfo failed' })
|
|
337
|
+
out = StringIO.new
|
|
338
|
+
assert_nothing_raised do
|
|
339
|
+
assert_false(failing.lookup(%w[String#gsub], out))
|
|
340
|
+
end
|
|
341
|
+
assert_match(/docs\.ruby-lang\.org/, out.string)
|
|
342
|
+
assert_match(/getaddrinfo/, out.string)
|
|
343
|
+
assert_match(/bitclust setup/, out.string)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def test_lookup_uses_cached_index_when_offline
|
|
347
|
+
remote.lookup(%w[String#gsub], StringIO.new)
|
|
348
|
+
failing = BitClust::Remote.new(base_url: BASE_URL, cache_dir: @dir, clock: -> { T0 + 3 * 86400 },
|
|
349
|
+
fetcher: ->(_uri, _headers) { raise SocketError, 'getaddrinfo failed' })
|
|
350
|
+
out = StringIO.new
|
|
351
|
+
assert_true(failing.lookup(%w[String#gsub], out))
|
|
352
|
+
assert_match(/置き換えます。/, out.string)
|
|
353
|
+
assert_match(/オフライン/, out.string)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def test_default_follows_environment
|
|
357
|
+
assert_equal(BitClust::Remote::DEFAULT_BASE_URL, BitClust::Remote.default.base_url)
|
|
358
|
+
ENV['BITCLUST_REMOTE_URL'] = 'http://localhost:10080/ja/latest'
|
|
359
|
+
assert_equal('http://localhost:10080/ja/latest/', BitClust::Remote.default.base_url)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Windows のシェル(cmd.exe・PowerShell)では空の環境変数を作れないので、
|
|
363
|
+
# 無効化は "none" でもできる
|
|
364
|
+
def test_default_is_disabled_by_none_or_empty
|
|
365
|
+
%w[none NONE None].each do |value|
|
|
366
|
+
ENV['BITCLUST_REMOTE_URL'] = value
|
|
367
|
+
assert_nil(BitClust::Remote.default, value)
|
|
368
|
+
end
|
|
369
|
+
ENV['BITCLUST_REMOTE_URL'] = ''
|
|
370
|
+
assert_nil(BitClust::Remote.default)
|
|
371
|
+
ENV['BITCLUST_REMOTE_URL'] = 'http://none.example/'
|
|
372
|
+
assert_equal('http://none.example/', BitClust::Remote.default.base_url)
|
|
373
|
+
end
|
|
374
|
+
end
|
data/test/test_runner.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
require 'bitclust'
|
|
2
2
|
require 'bitclust/runner'
|
|
3
|
+
require 'bitclust/user_dirs'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'fileutils'
|
|
3
6
|
|
|
4
7
|
class TestRunner < Test::Unit::TestCase
|
|
5
8
|
def setup
|
|
@@ -116,7 +119,10 @@ class TestRunner < Test::Unit::TestCase
|
|
|
116
119
|
original_stderr = $stderr
|
|
117
120
|
$stderr = StringIO.new
|
|
118
121
|
assert_raise(SystemExit) { @runner.run(["init"]) }
|
|
119
|
-
|
|
122
|
+
message = $stderr.string
|
|
123
|
+
assert_include(message, "--database (-d)")
|
|
124
|
+
assert_include(message, "bitclust setup")
|
|
125
|
+
assert_include(message, BitClust::UserDirs.config_candidates.join(' or '))
|
|
120
126
|
ensure
|
|
121
127
|
$stderr = original_stderr
|
|
122
128
|
end
|
|
@@ -146,3 +152,57 @@ class TestRunner < Test::Unit::TestCase
|
|
|
146
152
|
end
|
|
147
153
|
end
|
|
148
154
|
end
|
|
155
|
+
|
|
156
|
+
# Runner#load_config が UserDirs 経由で設定ファイルを読むこと(実ファイルで検証)。
|
|
157
|
+
#
|
|
158
|
+
# テストリスト:
|
|
159
|
+
# [x] XDG の場所(~/.config/bitclust/config)にあればそれを読む
|
|
160
|
+
# [x] 従来の場所(~/.bitclust/config)にあればそれを読む
|
|
161
|
+
# [x] どちらも無ければ nil
|
|
162
|
+
class TestRunnerLoadConfig < Test::Unit::TestCase
|
|
163
|
+
ENV_KEYS = %w[HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME
|
|
164
|
+
REFE2_SERVER BITCLUST_SERVER REFE2_DATADIR BITCLUST_DATADIR].freeze
|
|
165
|
+
|
|
166
|
+
def setup
|
|
167
|
+
@saved = ENV_KEYS.to_h {|k| [k, ENV[k]] }
|
|
168
|
+
@home = Dir.mktmpdir
|
|
169
|
+
ENV['HOME'] = @home
|
|
170
|
+
(ENV_KEYS - %w[HOME]).each {|k| ENV.delete(k) }
|
|
171
|
+
@runner = BitClust::Runner.new
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def teardown
|
|
175
|
+
@saved.each {|k, v| v ? ENV[k] = v : ENV.delete(k) }
|
|
176
|
+
FileUtils.rm_rf(@home)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def write_config(path, prefix, version)
|
|
180
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
181
|
+
File.write(path, { :default_version => version, :database_prefix => prefix }.to_yaml)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def test_load_config_from_xdg_location
|
|
185
|
+
write_config(File.join(@home, '.config', 'bitclust', 'config'), '/xdg/db', '4.0')
|
|
186
|
+
config = @runner.load_config
|
|
187
|
+
assert_equal('/xdg/db', config[:database_prefix])
|
|
188
|
+
assert_equal('4.0', config[:default_version])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_load_config_from_legacy_location
|
|
192
|
+
write_config(File.join(@home, '.bitclust', 'config'), '/legacy/db', '3.4')
|
|
193
|
+
config = @runner.load_config
|
|
194
|
+
assert_equal('/legacy/db', config[:database_prefix])
|
|
195
|
+
assert_equal('3.4', config[:default_version])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_load_config_prefers_xdg_over_legacy
|
|
199
|
+
write_config(File.join(@home, '.bitclust', 'config'), '/legacy/db', '3.4')
|
|
200
|
+
write_config(File.join(@home, '.config', 'bitclust', 'config'), '/xdg/db', '4.0')
|
|
201
|
+
config = @runner.load_config
|
|
202
|
+
assert_equal('/xdg/db', config[:database_prefix])
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def test_load_config_nil_without_any_config_file
|
|
206
|
+
assert_nil(@runner.load_config)
|
|
207
|
+
end
|
|
208
|
+
end
|