ruvim 0.3.0 → 0.4.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/AGENTS.md +18 -6
- data/README.md +15 -1
- data/docs/binding.md +16 -0
- data/docs/command.md +78 -4
- data/docs/config.md +10 -2
- data/docs/spec.md +60 -9
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +18 -8
- data/lib/ruvim/app.rb +290 -8
- data/lib/ruvim/buffer.rb +14 -2
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/editor.rb +12 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +176 -42
- data/lib/ruvim/highlighter.rb +3 -1
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +34 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view.rb +16 -0
- data/lib/ruvim/screen.rb +9 -12
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim.rb +10 -0
- data/test/app_completion_test.rb +25 -0
- data/test/app_scenario_test.rb +169 -0
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/display_width_test.rb +41 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +44 -0
- data/test/indent_test.rb +86 -0
- data/test/rich_view_test.rb +256 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +17 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class ClipboardTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
RuVim::Clipboard.reset_backend!
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def teardown
|
|
9
|
+
RuVim::Clipboard.reset_backend!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_available_returns_true_when_backend_exists
|
|
13
|
+
RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[echo], read: %w[echo] })
|
|
14
|
+
assert RuVim::Clipboard.available?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_available_returns_false_when_no_backend
|
|
18
|
+
RuVim::Clipboard.instance_variable_set(:@backend, false)
|
|
19
|
+
refute RuVim::Clipboard.available?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_read_returns_nil_when_no_read_cmd
|
|
23
|
+
RuVim::Clipboard.instance_variable_set(:@backend, { write: nil, read: nil })
|
|
24
|
+
assert_nil RuVim::Clipboard.read
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_read_returns_output_on_success
|
|
28
|
+
RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[true], read: %w[echo hello] })
|
|
29
|
+
result = RuVim::Clipboard.read
|
|
30
|
+
assert_equal "hello\n", result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_read_returns_nil_on_command_failure
|
|
34
|
+
RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[true], read: %w[false] })
|
|
35
|
+
assert_nil RuVim::Clipboard.read
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_write_returns_false_when_no_write_cmd
|
|
39
|
+
RuVim::Clipboard.instance_variable_set(:@backend, { write: nil, read: nil })
|
|
40
|
+
refute RuVim::Clipboard.write("test")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_write_sends_text_to_command
|
|
44
|
+
RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[cat], read: %w[true] })
|
|
45
|
+
assert RuVim::Clipboard.write("hello")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_pbcopy_backend_format
|
|
49
|
+
expected = { write: %w[pbcopy], read: %w[pbpaste] }
|
|
50
|
+
assert_equal expected, RuVim::Clipboard.pbcopy_backend
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_wayland_backend_format
|
|
54
|
+
expected = { write: %w[wl-copy], read: %w[wl-paste -n] }
|
|
55
|
+
assert_equal expected, RuVim::Clipboard.wayland_backend
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_xclip_backend_format
|
|
59
|
+
expected = { write: %w[xclip -selection clipboard -in], read: %w[xclip -selection clipboard -out] }
|
|
60
|
+
assert_equal expected, RuVim::Clipboard.xclip_backend
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_xsel_backend_format
|
|
64
|
+
expected = { write: %w[xsel --clipboard --input], read: %w[xsel --clipboard --output] }
|
|
65
|
+
assert_equal expected, RuVim::Clipboard.xsel_backend
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class CommandLineTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@cl = RuVim::CommandLine.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_initial_state
|
|
9
|
+
assert_equal ":", @cl.prefix
|
|
10
|
+
assert_equal "", @cl.text
|
|
11
|
+
assert_equal 0, @cl.cursor
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_reset_with_custom_prefix
|
|
15
|
+
@cl.reset(prefix: "/")
|
|
16
|
+
assert_equal "/", @cl.prefix
|
|
17
|
+
assert_equal "", @cl.text
|
|
18
|
+
assert_equal 0, @cl.cursor
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_insert_appends_at_cursor
|
|
22
|
+
@cl.insert("abc")
|
|
23
|
+
assert_equal "abc", @cl.text
|
|
24
|
+
assert_equal 3, @cl.cursor
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_insert_at_middle
|
|
28
|
+
@cl.insert("ac")
|
|
29
|
+
@cl.instance_variable_set(:@cursor, 1)
|
|
30
|
+
@cl.insert("b")
|
|
31
|
+
assert_equal "abc", @cl.text
|
|
32
|
+
assert_equal 2, @cl.cursor
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_backspace_at_zero_does_nothing
|
|
36
|
+
@cl.backspace
|
|
37
|
+
assert_equal "", @cl.text
|
|
38
|
+
assert_equal 0, @cl.cursor
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_backspace_deletes_character
|
|
42
|
+
@cl.insert("abc")
|
|
43
|
+
@cl.backspace
|
|
44
|
+
assert_equal "ab", @cl.text
|
|
45
|
+
assert_equal 2, @cl.cursor
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_move_left
|
|
49
|
+
@cl.insert("abc")
|
|
50
|
+
@cl.move_left
|
|
51
|
+
assert_equal 2, @cl.cursor
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_move_left_at_zero_stays
|
|
55
|
+
@cl.move_left
|
|
56
|
+
assert_equal 0, @cl.cursor
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_move_right
|
|
60
|
+
@cl.insert("abc")
|
|
61
|
+
@cl.instance_variable_set(:@cursor, 1)
|
|
62
|
+
@cl.move_right
|
|
63
|
+
assert_equal 2, @cl.cursor
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_move_right_at_end_stays
|
|
67
|
+
@cl.insert("abc")
|
|
68
|
+
@cl.move_right
|
|
69
|
+
assert_equal 3, @cl.cursor
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_content_includes_prefix
|
|
73
|
+
@cl.insert("hello")
|
|
74
|
+
assert_equal ":hello", @cl.content
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_content_with_custom_prefix
|
|
78
|
+
@cl.reset(prefix: "/")
|
|
79
|
+
@cl.insert("search")
|
|
80
|
+
assert_equal "/search", @cl.content
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_clear_resets_text_and_cursor
|
|
84
|
+
@cl.insert("hello")
|
|
85
|
+
@cl.clear
|
|
86
|
+
assert_equal "", @cl.text
|
|
87
|
+
assert_equal 0, @cl.cursor
|
|
88
|
+
assert_equal ":", @cl.prefix
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_replace_text
|
|
92
|
+
@cl.insert("old")
|
|
93
|
+
@cl.replace_text("new text")
|
|
94
|
+
assert_equal "new text", @cl.text
|
|
95
|
+
assert_equal 8, @cl.cursor
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_replace_span_end_cursor
|
|
99
|
+
@cl.insert("hello world")
|
|
100
|
+
@cl.replace_span(6, 11, "ruby")
|
|
101
|
+
assert_equal "hello ruby", @cl.text
|
|
102
|
+
assert_equal 10, @cl.cursor
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_replace_span_start_cursor
|
|
106
|
+
@cl.insert("hello world")
|
|
107
|
+
@cl.replace_span(6, 11, "ruby", cursor_at: :start)
|
|
108
|
+
assert_equal "hello ruby", @cl.text
|
|
109
|
+
assert_equal 6, @cl.cursor
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_replace_span_integer_cursor
|
|
113
|
+
@cl.insert("hello world")
|
|
114
|
+
@cl.replace_span(6, 11, "ruby", cursor_at: 8)
|
|
115
|
+
assert_equal "hello ruby", @cl.text
|
|
116
|
+
assert_equal 8, @cl.cursor
|
|
117
|
+
end
|
|
118
|
+
end
|
data/test/config_dsl_test.rb
CHANGED
|
@@ -75,4 +75,91 @@ class ConfigDSLTest < Minitest::Test
|
|
|
75
75
|
assert_equal :match, match.status
|
|
76
76
|
assert match.invocation.id.start_with?("user.keymap.global.")
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
def test_nmap_with_command_id_string
|
|
80
|
+
@command_registry.register("test.cmd", call: ->(_ctx, **) {}, desc: "test", source: :builtin)
|
|
81
|
+
@dsl.nmap("T", "test.cmd")
|
|
82
|
+
match = @keymaps.resolve(:normal, ["T"])
|
|
83
|
+
assert_equal :match, match.status
|
|
84
|
+
assert_equal "test.cmd", match.invocation.id
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_nmap_without_command_id_or_block_raises
|
|
88
|
+
# ConfigDSL < BasicObject, so raise becomes NoMethodError for ::ArgumentError
|
|
89
|
+
assert_raises(NoMethodError, ArgumentError) { @dsl.nmap("T") }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_imap_without_block_or_id_raises
|
|
93
|
+
assert_raises(NoMethodError, ArgumentError) { @dsl.imap("T") }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_command_registers_user_command
|
|
97
|
+
@dsl.command("my.cmd", desc: "custom") { |_ctx, **| }
|
|
98
|
+
spec = @command_registry.fetch("my.cmd")
|
|
99
|
+
assert_equal :user, spec.source
|
|
100
|
+
assert_equal "custom", spec.desc
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_command_without_block_raises
|
|
104
|
+
assert_raises(NoMethodError, ArgumentError) { @dsl.command("my.cmd") }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_nmap_with_filetype
|
|
108
|
+
dsl = RuVim::ConfigDSL.new(
|
|
109
|
+
command_registry: @command_registry,
|
|
110
|
+
ex_registry: @ex_registry,
|
|
111
|
+
keymaps: @keymaps,
|
|
112
|
+
command_host: RuVim::GlobalCommands.instance,
|
|
113
|
+
filetype: "ruby"
|
|
114
|
+
)
|
|
115
|
+
dsl.nmap("K", desc: "ft test") { |_ctx, **| }
|
|
116
|
+
editor = fresh_editor
|
|
117
|
+
editor.current_buffer.options["filetype"] = "ruby"
|
|
118
|
+
match = @keymaps.resolve_with_context(:normal, ["K"], editor: editor)
|
|
119
|
+
assert_equal :match, match.status
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_set_option_requires_editor
|
|
123
|
+
assert_raises(NoMethodError, ArgumentError) { @dsl.set("number") }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_set_boolean_option
|
|
127
|
+
editor = fresh_editor
|
|
128
|
+
dsl = RuVim::ConfigDSL.new(
|
|
129
|
+
command_registry: @command_registry,
|
|
130
|
+
ex_registry: @ex_registry,
|
|
131
|
+
keymaps: @keymaps,
|
|
132
|
+
command_host: RuVim::GlobalCommands.instance,
|
|
133
|
+
editor: editor
|
|
134
|
+
)
|
|
135
|
+
dsl.set("number")
|
|
136
|
+
assert editor.get_option("number")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def test_set_no_prefix_disables_option
|
|
140
|
+
editor = fresh_editor
|
|
141
|
+
dsl = RuVim::ConfigDSL.new(
|
|
142
|
+
command_registry: @command_registry,
|
|
143
|
+
ex_registry: @ex_registry,
|
|
144
|
+
keymaps: @keymaps,
|
|
145
|
+
command_host: RuVim::GlobalCommands.instance,
|
|
146
|
+
editor: editor
|
|
147
|
+
)
|
|
148
|
+
dsl.set("number")
|
|
149
|
+
dsl.set("nonumber")
|
|
150
|
+
refute editor.get_option("number")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_set_with_value
|
|
154
|
+
editor = fresh_editor
|
|
155
|
+
dsl = RuVim::ConfigDSL.new(
|
|
156
|
+
command_registry: @command_registry,
|
|
157
|
+
ex_registry: @ex_registry,
|
|
158
|
+
keymaps: @keymaps,
|
|
159
|
+
command_host: RuVim::GlobalCommands.instance,
|
|
160
|
+
editor: editor
|
|
161
|
+
)
|
|
162
|
+
dsl.set("tabstop=4")
|
|
163
|
+
assert_equal 4, editor.get_option("tabstop")
|
|
164
|
+
end
|
|
78
165
|
end
|
data/test/display_width_test.rb
CHANGED
|
@@ -15,4 +15,45 @@ class DisplayWidthTest < Minitest::Test
|
|
|
15
15
|
ENV["RUVIM_AMBIGUOUS_WIDTH"] = prev
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
def test_zero_codepoint_returns_zero
|
|
20
|
+
assert_equal 0, RuVim::DisplayWidth.uncached_codepoint_width(0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_expand_tabs_basic
|
|
24
|
+
assert_equal " hello", RuVim::DisplayWidth.expand_tabs("\thello", tabstop: 2)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_expand_tabs_mid_column
|
|
28
|
+
assert_equal "a hello", RuVim::DisplayWidth.expand_tabs("a\thello", tabstop: 2)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_expand_tabs_with_tabstop_4
|
|
32
|
+
assert_equal " hello", RuVim::DisplayWidth.expand_tabs("\thello", tabstop: 4)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_expand_tabs_preserves_non_tab_chars
|
|
36
|
+
assert_equal "hello", RuVim::DisplayWidth.expand_tabs("hello", tabstop: 2)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_expand_tabs_with_start_col
|
|
40
|
+
result = RuVim::DisplayWidth.expand_tabs("\tx", tabstop: 4, start_col: 1)
|
|
41
|
+
assert_equal " x", result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_wide_codepoint_cjk
|
|
45
|
+
assert_equal 2, RuVim::DisplayWidth.cell_width("漢")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_wide_codepoint_fullwidth_form
|
|
49
|
+
assert_equal 2, RuVim::DisplayWidth.cell_width("A")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_combining_mark_returns_zero
|
|
53
|
+
assert_equal 0, RuVim::DisplayWidth.cell_width("\u0300")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_zero_width_joiner_returns_zero
|
|
57
|
+
assert_equal 0, RuVim::DisplayWidth.cell_width("\u200D")
|
|
58
|
+
end
|
|
18
59
|
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require "ruvim/file_watcher"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
|
|
7
|
+
class FileWatcherTest < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
@tmpfile = Tempfile.new("file_watcher_test")
|
|
10
|
+
@tmpfile.write("initial\n")
|
|
11
|
+
@tmpfile.flush
|
|
12
|
+
@path = @tmpfile.path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def teardown
|
|
16
|
+
@watcher&.stop
|
|
17
|
+
@tmpfile&.close!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_polling_watcher_detects_append
|
|
21
|
+
received = Queue.new
|
|
22
|
+
@watcher = RuVim::FileWatcher::PollingWatcher.new(@path) do |type, data|
|
|
23
|
+
received << [type, data]
|
|
24
|
+
end
|
|
25
|
+
@watcher.start
|
|
26
|
+
|
|
27
|
+
File.open(@path, "a") { |f| f.write("appended\n") }
|
|
28
|
+
|
|
29
|
+
event = nil
|
|
30
|
+
assert_eventually(timeout: 2) { event = received.pop(true) rescue nil; !event.nil? }
|
|
31
|
+
assert_equal :data, event[0]
|
|
32
|
+
assert_includes event[1], "appended"
|
|
33
|
+
ensure
|
|
34
|
+
@watcher&.stop
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_polling_watcher_stop
|
|
38
|
+
@watcher = RuVim::FileWatcher::PollingWatcher.new(@path) { |_, _| }
|
|
39
|
+
@watcher.start
|
|
40
|
+
assert @watcher.alive?
|
|
41
|
+
@watcher.stop
|
|
42
|
+
refute @watcher.alive?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_polling_watcher_detects_truncation
|
|
46
|
+
received = Queue.new
|
|
47
|
+
@watcher = RuVim::FileWatcher::PollingWatcher.new(@path) do |type, data|
|
|
48
|
+
received << [type, data]
|
|
49
|
+
end
|
|
50
|
+
@watcher.start
|
|
51
|
+
|
|
52
|
+
# Append first so offset advances
|
|
53
|
+
File.open(@path, "a") { |f| f.write("extra\n") }
|
|
54
|
+
assert_eventually(timeout: 2) { received.pop(true) rescue nil }
|
|
55
|
+
|
|
56
|
+
# Truncate the file
|
|
57
|
+
File.write(@path, "")
|
|
58
|
+
|
|
59
|
+
event = nil
|
|
60
|
+
assert_eventually(timeout: 3) { event = received.pop(true) rescue nil; event&.first == :truncated }
|
|
61
|
+
assert_equal :truncated, event[0]
|
|
62
|
+
assert_nil event[1]
|
|
63
|
+
ensure
|
|
64
|
+
@watcher&.stop
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_polling_watcher_detects_deletion
|
|
68
|
+
received = Queue.new
|
|
69
|
+
@watcher = RuVim::FileWatcher::PollingWatcher.new(@path) do |type, data|
|
|
70
|
+
received << [type, data]
|
|
71
|
+
end
|
|
72
|
+
@watcher.start
|
|
73
|
+
|
|
74
|
+
File.delete(@path)
|
|
75
|
+
|
|
76
|
+
event = nil
|
|
77
|
+
assert_eventually(timeout: 3) { event = received.pop(true) rescue nil; event&.first == :deleted }
|
|
78
|
+
assert_equal :deleted, event[0]
|
|
79
|
+
assert_nil event[1]
|
|
80
|
+
ensure
|
|
81
|
+
@watcher&.stop
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_polling_watcher_waits_for_missing_file
|
|
85
|
+
missing_path = File.join(Dir.pwd, "follow_missing_test_#{$$}.log")
|
|
86
|
+
File.delete(missing_path) if File.exist?(missing_path)
|
|
87
|
+
|
|
88
|
+
received = Queue.new
|
|
89
|
+
watcher = RuVim::FileWatcher::PollingWatcher.new(missing_path) do |type, data|
|
|
90
|
+
received << [type, data]
|
|
91
|
+
end
|
|
92
|
+
watcher.start
|
|
93
|
+
|
|
94
|
+
sleep 0.2
|
|
95
|
+
assert watcher.alive?
|
|
96
|
+
|
|
97
|
+
File.write(missing_path, "hello\n")
|
|
98
|
+
|
|
99
|
+
event = nil
|
|
100
|
+
assert_eventually(timeout: 3) { event = received.pop(true) rescue nil; !event.nil? }
|
|
101
|
+
assert_equal :data, event[0]
|
|
102
|
+
assert_includes event[1], "hello"
|
|
103
|
+
ensure
|
|
104
|
+
watcher&.stop
|
|
105
|
+
File.delete(missing_path) if File.exist?(missing_path)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_inotify_watcher_detects_append
|
|
109
|
+
skip "inotify not available" unless RuVim::FileWatcher::InotifyWatcher.available?
|
|
110
|
+
|
|
111
|
+
received = Queue.new
|
|
112
|
+
@watcher = RuVim::FileWatcher::InotifyWatcher.new(@path) do |type, data|
|
|
113
|
+
received << [type, data]
|
|
114
|
+
end
|
|
115
|
+
@watcher.start
|
|
116
|
+
|
|
117
|
+
File.open(@path, "a") { |f| f.write("inotify appended\n") }
|
|
118
|
+
|
|
119
|
+
event = nil
|
|
120
|
+
assert_eventually(timeout: 2) { event = received.pop(true) rescue nil; !event.nil? }
|
|
121
|
+
assert_equal :data, event[0]
|
|
122
|
+
assert_includes event[1], "inotify appended"
|
|
123
|
+
ensure
|
|
124
|
+
@watcher&.stop
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_inotify_watcher_stop
|
|
128
|
+
skip "inotify not available" unless RuVim::FileWatcher::InotifyWatcher.available?
|
|
129
|
+
|
|
130
|
+
@watcher = RuVim::FileWatcher::InotifyWatcher.new(@path) { |_, _| }
|
|
131
|
+
@watcher.start
|
|
132
|
+
assert @watcher.alive?
|
|
133
|
+
@watcher.stop
|
|
134
|
+
refute @watcher.alive?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_inotify_watcher_detects_truncation
|
|
138
|
+
skip "inotify not available" unless RuVim::FileWatcher::InotifyWatcher.available?
|
|
139
|
+
|
|
140
|
+
received = Queue.new
|
|
141
|
+
@watcher = RuVim::FileWatcher::InotifyWatcher.new(@path) do |type, data|
|
|
142
|
+
received << [type, data]
|
|
143
|
+
end
|
|
144
|
+
@watcher.start
|
|
145
|
+
|
|
146
|
+
File.open(@path, "a") { |f| f.write("extra\n") }
|
|
147
|
+
assert_eventually(timeout: 2) { received.pop(true) rescue nil }
|
|
148
|
+
|
|
149
|
+
File.write(@path, "")
|
|
150
|
+
|
|
151
|
+
event = nil
|
|
152
|
+
assert_eventually(timeout: 3) { event = received.pop(true) rescue nil; event&.first == :truncated }
|
|
153
|
+
assert_equal :truncated, event[0]
|
|
154
|
+
ensure
|
|
155
|
+
@watcher&.stop
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_create_prefers_inotify
|
|
159
|
+
watcher = RuVim::FileWatcher.create(@path) { |_, _| }
|
|
160
|
+
if RuVim::FileWatcher::InotifyWatcher.available?
|
|
161
|
+
assert_kind_of RuVim::FileWatcher::InotifyWatcher, watcher
|
|
162
|
+
else
|
|
163
|
+
assert_kind_of RuVim::FileWatcher::PollingWatcher, watcher
|
|
164
|
+
end
|
|
165
|
+
ensure
|
|
166
|
+
watcher&.stop
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def test_create_falls_back_to_polling_for_missing_file
|
|
170
|
+
missing_path = File.join(Dir.pwd, "follow_create_test_#{$$}.log")
|
|
171
|
+
File.delete(missing_path) if File.exist?(missing_path)
|
|
172
|
+
|
|
173
|
+
watcher = RuVim::FileWatcher.create(missing_path) { |_, _| }
|
|
174
|
+
assert_kind_of RuVim::FileWatcher::PollingWatcher, watcher
|
|
175
|
+
ensure
|
|
176
|
+
watcher&.stop
|
|
177
|
+
File.delete(missing_path) if File.exist?(missing_path)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def test_polling_backoff_resets_on_change
|
|
181
|
+
@watcher = RuVim::FileWatcher::PollingWatcher.new(@path) { |_, _| }
|
|
182
|
+
assert_equal RuVim::FileWatcher::PollingWatcher::MIN_INTERVAL, @watcher.current_interval
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def assert_eventually(timeout: 2, interval: 0.05)
|
|
188
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
189
|
+
loop do
|
|
190
|
+
return if yield
|
|
191
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
192
|
+
flunk "Timed out waiting for condition"
|
|
193
|
+
end
|
|
194
|
+
sleep interval
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|