ruvim 0.2.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/.github/workflows/test.yml +4 -0
- data/AGENTS.md +96 -0
- data/CLAUDE.md +1 -0
- data/README.md +15 -1
- data/docs/binding.md +39 -0
- data/docs/command.md +163 -4
- data/docs/config.md +12 -4
- data/docs/done.md +21 -0
- data/docs/spec.md +214 -18
- data/docs/todo.md +1 -5
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +105 -173
- data/lib/ruvim/app.rb +1165 -70
- data/lib/ruvim/buffer.rb +47 -1
- data/lib/ruvim/cli.rb +18 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +466 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- 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 +1066 -105
- data/lib/ruvim/highlighter.rb +19 -22
- data/lib/ruvim/input.rb +40 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +52 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -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/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +109 -0
- data/lib/ruvim/screen.rb +503 -109
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +24 -0
- data/test/app_completion_test.rb +98 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +898 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- 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/dispatcher_test.rb +322 -0
- data/test/display_width_test.rb +41 -0
- data/test/editor_register_test.rb +23 -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 +165 -0
- data/test/indent_test.rb +287 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +734 -0
- data/test/screen_test.rb +304 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +49 -2
|
@@ -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
|
data/test/follow_test.rb
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
class FollowTest < Minitest::Test
|
|
7
|
+
def create_follow_app
|
|
8
|
+
@tmpfile = Tempfile.new(["follow_test", ".txt"])
|
|
9
|
+
@tmpfile.write("line1\nline2\n")
|
|
10
|
+
@tmpfile.flush
|
|
11
|
+
@path = @tmpfile.path
|
|
12
|
+
|
|
13
|
+
@app = RuVim::App.new(path: @path, clean: true)
|
|
14
|
+
@editor = @app.instance_variable_get(:@editor)
|
|
15
|
+
@dispatcher = @app.instance_variable_get(:@dispatcher)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def cleanup_follow_app
|
|
19
|
+
return unless @app
|
|
20
|
+
|
|
21
|
+
watchers = @app.instance_variable_get(:@follow_watchers)
|
|
22
|
+
watchers.each_value { |w| w.stop rescue nil }
|
|
23
|
+
watchers.clear
|
|
24
|
+
@tmpfile&.close!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_follow_starts_on_file_buffer
|
|
28
|
+
create_follow_app
|
|
29
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
30
|
+
watchers = @app.instance_variable_get(:@follow_watchers)
|
|
31
|
+
buf = @editor.current_buffer
|
|
32
|
+
|
|
33
|
+
assert !@editor.message_error?, "Unexpected error: #{@editor.message}"
|
|
34
|
+
assert_equal :live, buf.stream_state
|
|
35
|
+
assert watchers.key?(buf.id)
|
|
36
|
+
assert_includes @editor.message.to_s, "[follow]"
|
|
37
|
+
ensure
|
|
38
|
+
cleanup_follow_app
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_follow_stops_on_ctrl_c
|
|
42
|
+
create_follow_app
|
|
43
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
44
|
+
buf = @editor.current_buffer
|
|
45
|
+
assert_equal :live, buf.stream_state
|
|
46
|
+
|
|
47
|
+
@app.send(:handle_key, :ctrl_c)
|
|
48
|
+
assert_nil buf.stream_state
|
|
49
|
+
assert_includes @editor.message.to_s, "stopped"
|
|
50
|
+
ensure
|
|
51
|
+
cleanup_follow_app
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_follow_toggle_stops
|
|
55
|
+
create_follow_app
|
|
56
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
57
|
+
buf = @editor.current_buffer
|
|
58
|
+
assert_equal :live, buf.stream_state
|
|
59
|
+
|
|
60
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
61
|
+
watchers = @app.instance_variable_get(:@follow_watchers)
|
|
62
|
+
assert_nil buf.stream_state
|
|
63
|
+
refute watchers.key?(buf.id)
|
|
64
|
+
assert_includes @editor.message.to_s, "stopped"
|
|
65
|
+
ensure
|
|
66
|
+
cleanup_follow_app
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_follow_stop_removes_sentinel_empty_line
|
|
70
|
+
create_follow_app
|
|
71
|
+
buf = @editor.current_buffer
|
|
72
|
+
lines_before = buf.line_count
|
|
73
|
+
|
|
74
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
75
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
76
|
+
|
|
77
|
+
assert_equal lines_before, buf.line_count, "Sentinel empty line should be removed on stop"
|
|
78
|
+
refute_equal "", buf.lines.last, "Last line should not be empty sentinel"
|
|
79
|
+
ensure
|
|
80
|
+
cleanup_follow_app
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_follow_makes_buffer_not_modifiable
|
|
84
|
+
create_follow_app
|
|
85
|
+
buf = @editor.current_buffer
|
|
86
|
+
assert buf.modifiable?
|
|
87
|
+
|
|
88
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
89
|
+
refute buf.modifiable?, "Buffer should not be modifiable during follow"
|
|
90
|
+
|
|
91
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
92
|
+
assert buf.modifiable?, "Buffer should be modifiable after follow stops"
|
|
93
|
+
ensure
|
|
94
|
+
cleanup_follow_app
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_follow_error_on_modified_buffer
|
|
98
|
+
create_follow_app
|
|
99
|
+
buf = @editor.current_buffer
|
|
100
|
+
buf.modified = true
|
|
101
|
+
|
|
102
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
103
|
+
assert_includes @editor.message.to_s, "unsaved changes"
|
|
104
|
+
assert_nil buf.stream_state
|
|
105
|
+
ensure
|
|
106
|
+
cleanup_follow_app
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_follow_error_on_no_path_buffer
|
|
110
|
+
@app = RuVim::App.new(clean: true)
|
|
111
|
+
@editor = @app.instance_variable_get(:@editor)
|
|
112
|
+
@dispatcher = @app.instance_variable_get(:@dispatcher)
|
|
113
|
+
@editor.materialize_intro_buffer!
|
|
114
|
+
buf = @editor.current_buffer
|
|
115
|
+
buf.instance_variable_set(:@path, nil)
|
|
116
|
+
|
|
117
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
118
|
+
assert_includes @editor.message.to_s, "No file"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_follow_appends_data_from_file
|
|
122
|
+
create_follow_app
|
|
123
|
+
win = @editor.current_window
|
|
124
|
+
buf = @editor.current_buffer
|
|
125
|
+
win.cursor_y = buf.line_count - 1
|
|
126
|
+
|
|
127
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
128
|
+
|
|
129
|
+
File.open(@path, "a") { |f| f.write("line3\nline4\n") }
|
|
130
|
+
|
|
131
|
+
assert_eventually(timeout: 3) do
|
|
132
|
+
@app.send(:drain_stream_events!)
|
|
133
|
+
buf.line_count > 3
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# line2 should NOT be joined with line3
|
|
137
|
+
assert_includes buf.lines, "line2"
|
|
138
|
+
assert_includes buf.lines, "line3"
|
|
139
|
+
assert_includes buf.lines, "line4"
|
|
140
|
+
assert_equal buf.line_count - 1, win.cursor_y
|
|
141
|
+
ensure
|
|
142
|
+
cleanup_follow_app
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_follow_no_scroll_when_cursor_not_at_end
|
|
146
|
+
create_follow_app
|
|
147
|
+
buf = @editor.current_buffer
|
|
148
|
+
win = @editor.current_window
|
|
149
|
+
win.cursor_y = 0
|
|
150
|
+
|
|
151
|
+
@dispatcher.dispatch_ex(@editor, "follow")
|
|
152
|
+
|
|
153
|
+
File.open(@path, "a") { |f| f.write("line3\n") }
|
|
154
|
+
|
|
155
|
+
assert_eventually(timeout: 3) do
|
|
156
|
+
@app.send(:drain_stream_events!)
|
|
157
|
+
buf.line_count > 2
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
assert_equal 0, win.cursor_y
|
|
161
|
+
ensure
|
|
162
|
+
cleanup_follow_app
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_startup_follow_applies_to_all_buffers
|
|
166
|
+
tmp1 = Tempfile.new(["follow_multi1", ".txt"])
|
|
167
|
+
tmp1.write("aaa\n"); tmp1.flush
|
|
168
|
+
tmp2 = Tempfile.new(["follow_multi2", ".txt"])
|
|
169
|
+
tmp2.write("bbb\n"); tmp2.flush
|
|
170
|
+
|
|
171
|
+
app = RuVim::App.new(paths: [tmp1.path, tmp2.path], follow: true, clean: true)
|
|
172
|
+
editor = app.instance_variable_get(:@editor)
|
|
173
|
+
watchers = app.instance_variable_get(:@follow_watchers)
|
|
174
|
+
|
|
175
|
+
bufs = editor.buffers.values.select(&:file_buffer?)
|
|
176
|
+
assert_equal 2, bufs.size
|
|
177
|
+
bufs.each do |buf|
|
|
178
|
+
assert_equal :live, buf.stream_state, "#{buf.display_name} should be in follow mode"
|
|
179
|
+
assert watchers.key?(buf.id), "#{buf.display_name} should have a watcher"
|
|
180
|
+
end
|
|
181
|
+
ensure
|
|
182
|
+
watchers&.each_value { |w| w.stop rescue nil }
|
|
183
|
+
tmp1&.close!
|
|
184
|
+
tmp2&.close!
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def assert_eventually(timeout: 2, interval: 0.05)
|
|
190
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
191
|
+
loop do
|
|
192
|
+
return if yield
|
|
193
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
194
|
+
flunk "Timed out waiting for condition"
|
|
195
|
+
end
|
|
196
|
+
sleep interval
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|