canopus 0.4.0 → 0.5.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/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zaniah/ui"
|
|
4
|
+
|
|
5
|
+
module Canopus
|
|
6
|
+
module Workspace::TestAware
|
|
7
|
+
TEST_LABEL_LIMIT = 200
|
|
8
|
+
TEST_COLORS = {idle: :muted, running: :accent, success: "#3fb950",
|
|
9
|
+
failure: :error, skipped: :muted}.freeze
|
|
10
|
+
TEST_MARKS = {idle: "▶", running: "●", success: "✓", failure: "×", skipped: "○"}.freeze
|
|
11
|
+
|
|
12
|
+
TestClick = Data.define(:workspace, :tests) do
|
|
13
|
+
def call(_editor, _row) = workspace.run_tests(tests)
|
|
14
|
+
end
|
|
15
|
+
private_constant :TestClick
|
|
16
|
+
|
|
17
|
+
attr_reader :tests
|
|
18
|
+
|
|
19
|
+
def test_results = @test_results.dup.freeze
|
|
20
|
+
|
|
21
|
+
def initialize_tests
|
|
22
|
+
@tests = [].freeze
|
|
23
|
+
@test_generation = 0
|
|
24
|
+
@test_discovery_started = @test_discovery_pending = @test_discovery_requested = false
|
|
25
|
+
@test_discovery_thread = @test_discovery_token = nil
|
|
26
|
+
@test_discovery = nil
|
|
27
|
+
@tests_by_path = {}.freeze
|
|
28
|
+
@test_results, @test_executions, @test_outputs = {}, {}, {}
|
|
29
|
+
@test_diagnostic_snapshot = {}.freeze
|
|
30
|
+
@decorations.register(:test) { |buffer, rows| test_decorations(buffer, rows) }
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_tree
|
|
35
|
+
@test_tree ||= Zaniah::UI::TreeView.new(test_nodes, height: 600).on_select do |value, _event, _context|
|
|
36
|
+
select_test(value)
|
|
37
|
+
end
|
|
38
|
+
refresh_tests unless @test_discovery_started
|
|
39
|
+
@test_tree
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def refresh_tests
|
|
43
|
+
raise Error, "Workspace closed" if @closed
|
|
44
|
+
|
|
45
|
+
@test_generation += 1
|
|
46
|
+
@test_discovery_started = @test_discovery_pending = @test_discovery_requested = true
|
|
47
|
+
@test_tree&.replace([test_status_node(:discovering, "Discovering tests…")])
|
|
48
|
+
@panels.badge("test", nil)
|
|
49
|
+
start_test_discovery
|
|
50
|
+
@window&.request_frame
|
|
51
|
+
@test_discovery_thread
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_discovery_pending? = !!@test_discovery_pending
|
|
55
|
+
|
|
56
|
+
def run_test(test) = run_tests([test])
|
|
57
|
+
|
|
58
|
+
def run_tests(tests)
|
|
59
|
+
unless tests.is_a?(Array) && !tests.empty? && tests.length <= TestRunner::Discovery::MAX_TESTS &&
|
|
60
|
+
tests.all? { |test| test.is_a?(TestRunner::Test) && @tests.include?(test) } &&
|
|
61
|
+
tests.all? { |test| test.framework == tests.first.framework && test.path == tests.first.path &&
|
|
62
|
+
test.line == tests.first.line }
|
|
63
|
+
raise Error, "Unknown tests"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
tests = tests.dup.freeze
|
|
67
|
+
test = tests.first
|
|
68
|
+
task = TestRunner::Execution.task(test, root: @root, tests: tests)
|
|
69
|
+
previous = tests.filter_map { |candidate| @test_outputs[candidate] }.uniq
|
|
70
|
+
output = start_task(task, nil)
|
|
71
|
+
previous.each do |entry|
|
|
72
|
+
@task_runner.stop(entry)
|
|
73
|
+
old = @test_executions.delete(entry.id)
|
|
74
|
+
old&.cancel
|
|
75
|
+
old&.tests&.each do |candidate|
|
|
76
|
+
@test_outputs.delete(candidate) if @test_outputs[candidate].equal?(entry)
|
|
77
|
+
@test_results[candidate] = old.finish(nil) if @tests.include?(candidate)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
execution = TestRunner::Execution.new(test, output, generation: @test_generation, tests: tests)
|
|
81
|
+
@test_executions[output.id] = execution
|
|
82
|
+
tests.each do |candidate|
|
|
83
|
+
@test_outputs[candidate] = output
|
|
84
|
+
@test_results[candidate] = TestRunner::Result.new(:running, nil, nil, nil).freeze
|
|
85
|
+
end
|
|
86
|
+
refresh_test_results
|
|
87
|
+
output
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def toggle_tests
|
|
91
|
+
if @panels.visible?("test")
|
|
92
|
+
@panels.hide("test")
|
|
93
|
+
else
|
|
94
|
+
@panels.show("test")
|
|
95
|
+
refresh_tests unless @test_discovery_started
|
|
96
|
+
end
|
|
97
|
+
@window&.request_frame
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def close_tests
|
|
101
|
+
@test_generation += 1
|
|
102
|
+
@test_discovery_requested = @test_discovery_pending = false
|
|
103
|
+
@test_discovery_token = nil
|
|
104
|
+
thread, @test_discovery_thread = @test_discovery_thread, nil
|
|
105
|
+
thread.join if thread && thread != Thread.current
|
|
106
|
+
@test_executions.clear
|
|
107
|
+
@test_outputs.clear
|
|
108
|
+
@test_results.clear
|
|
109
|
+
publish_test_diagnostics
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def start_test_discovery
|
|
116
|
+
return if @closed || @test_discovery_thread&.alive? || !@test_discovery_requested
|
|
117
|
+
|
|
118
|
+
generation = @test_generation
|
|
119
|
+
@test_discovery_requested = false
|
|
120
|
+
token = @test_discovery_token = Object.new.freeze
|
|
121
|
+
thread = Thread.new do
|
|
122
|
+
result = error = nil
|
|
123
|
+
begin
|
|
124
|
+
discovery = @test_discovery || TestRunner::Discovery.new(root: @root)
|
|
125
|
+
result = discovery.discover(cancelled: -> { @closed || generation != @test_generation })
|
|
126
|
+
rescue StandardError => failure
|
|
127
|
+
error = failure
|
|
128
|
+
ensure
|
|
129
|
+
post { complete_test_discovery(token, generation, result, error) } unless @closed
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
thread.report_on_exception = false
|
|
133
|
+
@test_discovery_thread = thread
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def complete_test_discovery(token, generation, result, error)
|
|
137
|
+
return unless @test_discovery_token.equal?(token)
|
|
138
|
+
|
|
139
|
+
@test_discovery_thread = @test_discovery_token = nil
|
|
140
|
+
if generation != @test_generation || @test_discovery_requested
|
|
141
|
+
start_test_discovery
|
|
142
|
+
elsif error
|
|
143
|
+
@tests = [].freeze
|
|
144
|
+
@tests_by_path = {}.freeze
|
|
145
|
+
@test_results.clear
|
|
146
|
+
@test_discovery_pending = false
|
|
147
|
+
@test_tree&.replace([test_status_node(:error, test_label("Cannot discover tests: #{error.message}"))])
|
|
148
|
+
self.message = test_label("Cannot discover tests: #{error.message}")
|
|
149
|
+
@decorations.invalidate(:test)
|
|
150
|
+
publish_test_diagnostics
|
|
151
|
+
else
|
|
152
|
+
@tests = result
|
|
153
|
+
@tests_by_path = @tests.group_by(&:path).transform_values { |tests| tests.freeze }.freeze
|
|
154
|
+
known = @tests.to_h { |test| [test, true] }
|
|
155
|
+
@test_results.delete_if { |test, result| !known.key?(test) || result.status == :running }
|
|
156
|
+
@test_discovery_pending = false
|
|
157
|
+
@test_tree&.replace(test_nodes)
|
|
158
|
+
@panels.badge("test", @tests.empty? ? nil : @tests.length)
|
|
159
|
+
@decorations.invalidate(:test)
|
|
160
|
+
publish_test_diagnostics
|
|
161
|
+
end
|
|
162
|
+
@window&.request_frame
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_nodes
|
|
167
|
+
return [test_status_node(:discovering, "Discovering tests…")].freeze if @test_discovery_pending
|
|
168
|
+
return [test_status_node(:empty, "No tests found")].freeze if @tests.empty?
|
|
169
|
+
|
|
170
|
+
@tests.group_by(&:path).map do |path, tests|
|
|
171
|
+
children = tests.map do |test|
|
|
172
|
+
{id: [:test, test.framework, test.path, test.offset].freeze,
|
|
173
|
+
label: test_case_label(test), value: test}.freeze
|
|
174
|
+
end.freeze
|
|
175
|
+
{id: [:test_file, path].freeze, label: test_label(path), value: nil, children: children}.freeze
|
|
176
|
+
end.freeze
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def test_status_node(kind, label)
|
|
180
|
+
{id: [:test_status, kind].freeze, label: label.freeze, value: nil}.freeze
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_label(value)
|
|
184
|
+
bounded_test_label([value])
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_case_label(test)
|
|
188
|
+
mark = TEST_MARKS.fetch(@test_results[test]&.status || :idle)
|
|
189
|
+
label = bounded_test_label([*test.groups, test.name], separator: " › ", suffix: " · line #{test.line}")
|
|
190
|
+
bounded_test_label(["#{mark} ", label])
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def bounded_test_label(parts, separator: "", suffix: nil)
|
|
194
|
+
label = +""
|
|
195
|
+
parts.each_with_index do |part, index|
|
|
196
|
+
if index.positive? && !append_test_label(label, separator)
|
|
197
|
+
return finish_test_label(label)
|
|
198
|
+
end
|
|
199
|
+
return finish_test_label(label) unless append_test_label(label, part)
|
|
200
|
+
end
|
|
201
|
+
return finish_test_label(label) if suffix && !append_test_label(label, suffix)
|
|
202
|
+
|
|
203
|
+
label.rstrip.freeze
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def append_test_label(label, value)
|
|
207
|
+
value.to_s.each_char do |character|
|
|
208
|
+
character = " " if character.match?(/\s/)
|
|
209
|
+
next if character == " " && (label.empty? || label.end_with?(" "))
|
|
210
|
+
return false if label.length >= TEST_LABEL_LIMIT
|
|
211
|
+
|
|
212
|
+
label << character
|
|
213
|
+
end
|
|
214
|
+
true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def finish_test_label(label)
|
|
218
|
+
label.rstrip!
|
|
219
|
+
label.slice!(TEST_LABEL_LIMIT - 1..) if label.length >= TEST_LABEL_LIMIT
|
|
220
|
+
label << "…"
|
|
221
|
+
label.freeze
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def select_test(test)
|
|
225
|
+
return false unless test.is_a?(TestRunner::Test)
|
|
226
|
+
|
|
227
|
+
result = @test_results[test]
|
|
228
|
+
if result&.status == :failure
|
|
229
|
+
begin
|
|
230
|
+
return select_test_location(result.path, result.line)
|
|
231
|
+
rescue Error, SystemCallError
|
|
232
|
+
# The file may have changed since the run; use the declaration below.
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
select_test_location(test.path, test.line)
|
|
236
|
+
rescue Error, SystemCallError => error
|
|
237
|
+
self.message = "Cannot open test: #{error.message}"
|
|
238
|
+
false
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def select_test_location(relative, line)
|
|
242
|
+
relative = TestRunner.normalize_path(relative)
|
|
243
|
+
raise Error, "Test target path is invalid" unless relative
|
|
244
|
+
|
|
245
|
+
candidate = File.join(@root, relative)
|
|
246
|
+
raise Error, "Test target is not a file" if File.lstat(candidate).symlink?
|
|
247
|
+
path = File.realpath(candidate)
|
|
248
|
+
prefix = @root.end_with?(File::SEPARATOR) ? @root : @root + File::SEPARATOR
|
|
249
|
+
raise Error, "Test target is outside the workspace" unless path.start_with?(prefix) && File.file?(path)
|
|
250
|
+
|
|
251
|
+
target = open(path)
|
|
252
|
+
row = line - 1
|
|
253
|
+
raise Error, "Test target line is no longer valid" unless row.between?(0, target.buffer.line_count - 1)
|
|
254
|
+
target.select(target.buffer.rope.line_start(row))
|
|
255
|
+
target.reveal_cursor
|
|
256
|
+
true
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def test_decorations(buffer, rows)
|
|
260
|
+
return [] unless buffer.path&.start_with?(@root + File::SEPARATOR)
|
|
261
|
+
|
|
262
|
+
relative = buffer.path.delete_prefix(@root + File::SEPARATOR).tr(File::SEPARATOR, "/")
|
|
263
|
+
visible = @tests_by_path.fetch(relative, []).select { |test| rows.cover?(test.line - 1) }
|
|
264
|
+
visible.group_by(&:line).map do |line, tests|
|
|
265
|
+
tests = tests.freeze
|
|
266
|
+
status = test_row_status(tests)
|
|
267
|
+
label = tests.length == 1 ? "#{TEST_MARKS.fetch(status)} Run #{test_label(tests.first.name)}" :
|
|
268
|
+
"#{TEST_MARKS.fetch(status)} Run #{tests.length} tests on line #{line}"
|
|
269
|
+
style = {color: TEST_COLORS.fetch(status), gutter_offset: 1, gutter_width: 4,
|
|
270
|
+
hit_offset: 0, hit_width: 6}.freeze
|
|
271
|
+
Decoration::Item.new(:gutter, nil, line - 1, label, style, 30, :test,
|
|
272
|
+
TestClick.new(self, tests).freeze)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def test_row_status(tests)
|
|
277
|
+
statuses = tests.filter_map { |test| @test_results[test]&.status }
|
|
278
|
+
return :running if statuses.include?(:running)
|
|
279
|
+
return :failure if statuses.include?(:failure)
|
|
280
|
+
return :idle if statuses.length < tests.length
|
|
281
|
+
return :skipped if statuses.all? { |status| status == :skipped }
|
|
282
|
+
|
|
283
|
+
:success
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def capture_test_output(output, data)
|
|
287
|
+
@test_executions[output.id]&.append(data)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def cancel_test_output(output)
|
|
291
|
+
@test_executions[output&.id]&.cancel
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def complete_test_output(output)
|
|
295
|
+
execution = @test_executions.delete(output.id)
|
|
296
|
+
return unless execution
|
|
297
|
+
|
|
298
|
+
@test_outputs.delete_if { |_test, entry| entry.equal?(output) }
|
|
299
|
+
status = output.terminal.status if output.terminal.respond_to?(:status)
|
|
300
|
+
result = normalize_test_result(execution, execution.finish(status))
|
|
301
|
+
if execution.generation == @test_generation
|
|
302
|
+
execution.tests.each do |test|
|
|
303
|
+
@test_results[test] = test_result_for(test, result) if @tests.include?(test)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
refresh_test_results
|
|
307
|
+
rescue StandardError => error
|
|
308
|
+
message = "#{execution.test.name} failed: #{error.message}".encode(Encoding::UTF_8,
|
|
309
|
+
invalid: :replace, undef: :replace).delete("\0").byteslice(0, 4_096).scrub("")
|
|
310
|
+
if execution.generation == @test_generation
|
|
311
|
+
execution.tests.each do |test|
|
|
312
|
+
next unless @tests.include?(test)
|
|
313
|
+
|
|
314
|
+
@test_results[test] = TestRunner::Result.new(:failure, test.path, test.line, message).freeze
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
refresh_test_results
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def forget_test_output(output)
|
|
321
|
+
execution = @test_executions.delete(output.id)
|
|
322
|
+
return unless execution
|
|
323
|
+
|
|
324
|
+
execution.cancel
|
|
325
|
+
@test_outputs.delete_if { |_test, entry| entry.equal?(output) }
|
|
326
|
+
if execution.generation == @test_generation
|
|
327
|
+
execution.tests.each do |test|
|
|
328
|
+
@test_results[test] = execution.finish(nil) if @tests.include?(test)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
refresh_test_results
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def refresh_test_results
|
|
335
|
+
@test_tree&.replace(test_nodes)
|
|
336
|
+
@decorations.invalidate(:test)
|
|
337
|
+
publish_test_diagnostics
|
|
338
|
+
@window&.request_frame
|
|
339
|
+
nil
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def normalize_test_result(execution, result)
|
|
343
|
+
return result unless result.status == :failure
|
|
344
|
+
|
|
345
|
+
path = File.join(@root, execution.test.path)
|
|
346
|
+
entry = File.lstat(path)
|
|
347
|
+
return result.with(line: execution.test.line).freeze if entry.symlink? || !entry.file? ||
|
|
348
|
+
entry.size > TestRunner::Discovery::MAX_FILE_BYTES
|
|
349
|
+
|
|
350
|
+
absolute = File.realpath(path)
|
|
351
|
+
prefix = @root.end_with?(File::SEPARATOR) ? @root : @root + File::SEPARATOR
|
|
352
|
+
return result.with(line: execution.test.line).freeze unless absolute.start_with?(prefix)
|
|
353
|
+
|
|
354
|
+
source = File.open(absolute, "rb") { |file| file.read(TestRunner::Discovery::MAX_FILE_BYTES + 1) }
|
|
355
|
+
return result.with(line: execution.test.line).freeze if source.bytesize > TestRunner::Discovery::MAX_FILE_BYTES
|
|
356
|
+
|
|
357
|
+
lines = [source.count("\n") + 1, 1].max
|
|
358
|
+
line = result.line.between?(1, lines) ? result.line : execution.test.line.clamp(1, lines)
|
|
359
|
+
result.with(line: line).freeze
|
|
360
|
+
rescue SystemCallError
|
|
361
|
+
result.with(line: execution.test.line).freeze
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def test_result_for(test, result)
|
|
365
|
+
return result unless result.status == :failure
|
|
366
|
+
|
|
367
|
+
message = "#{test.name} failed".byteslice(0, 4_096).scrub("")
|
|
368
|
+
result.with(message: message).freeze
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def publish_test_diagnostics
|
|
372
|
+
grouped = @test_results.each_with_object(Hash.new { |hash, uri| hash[uri] = [] }) do |(test, result), values|
|
|
373
|
+
next unless result.status == :failure
|
|
374
|
+
|
|
375
|
+
path = File.join(@root, result.path)
|
|
376
|
+
uri = Sadr::Protocol.uri(path)
|
|
377
|
+
line = result.line - 1
|
|
378
|
+
values[uri] << {"message" => result.message, "severity" => 1, "source" => "test",
|
|
379
|
+
"range" => {"start" => {"line" => line, "character" => 0},
|
|
380
|
+
"end" => {"line" => line, "character" => 0}}}
|
|
381
|
+
end
|
|
382
|
+
snapshot = grouped.transform_values { |values| values.first(Diagnostics::PUBLICATION_LIMIT).freeze }.freeze
|
|
383
|
+
changed = (@test_diagnostic_snapshot.keys | snapshot.keys).select do |uri|
|
|
384
|
+
@test_diagnostic_snapshot[uri] != snapshot[uri]
|
|
385
|
+
end
|
|
386
|
+
return false if changed.empty?
|
|
387
|
+
|
|
388
|
+
changed.each { |uri| @diagnostics.publish(:test, uri, snapshot.fetch(uri, []), notify: false) }
|
|
389
|
+
@test_diagnostic_snapshot = snapshot
|
|
390
|
+
diagnostics_changed(changed)
|
|
391
|
+
true
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
|
|
8
|
+
module Canopus
|
|
9
|
+
class Workspace::Trust
|
|
10
|
+
def initialize(root, state_path: nil)
|
|
11
|
+
@root = File.realpath(root)
|
|
12
|
+
@state_path = state_path || File.join(ENV["XDG_STATE_HOME"] || File.expand_path("~/.local/state"), "canopus", "trust.json")
|
|
13
|
+
@key = Digest::SHA256.hexdigest(@root)
|
|
14
|
+
@states = load_states
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def trusted? = @states[@key] == true
|
|
18
|
+
def status = trusted? ? :trusted : :untrusted
|
|
19
|
+
|
|
20
|
+
def trust!
|
|
21
|
+
update(true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def untrust!
|
|
25
|
+
update(false)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def update(value)
|
|
31
|
+
@states[@key] = value
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(@state_path))
|
|
33
|
+
Tempfile.create([".trust-", ".json"], File.dirname(@state_path), perm: 0o600) do |file|
|
|
34
|
+
file.write(JSON.generate(@states))
|
|
35
|
+
file.flush
|
|
36
|
+
file.fsync
|
|
37
|
+
file.close
|
|
38
|
+
File.chmod(0o600, file.path)
|
|
39
|
+
File.rename(file.path, @state_path)
|
|
40
|
+
end
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def load_states
|
|
45
|
+
value = JSON.parse(File.read(@state_path))
|
|
46
|
+
value.is_a?(Hash) ? value.select { |key, state| key.is_a?(String) && (state.is_a?(TrueClass) || state.is_a?(FalseClass)) } : {}
|
|
47
|
+
rescue Errno::ENOENT, Errno::EACCES, JSON::ParserError, TypeError
|
|
48
|
+
{}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module Workspace::TrustAware
|
|
53
|
+
def workspace_trust = @trust
|
|
54
|
+
|
|
55
|
+
def toggle_workspace_trust
|
|
56
|
+
if @trust.trusted?
|
|
57
|
+
@trust.untrust!
|
|
58
|
+
@message = "Workspace untrusted"
|
|
59
|
+
else
|
|
60
|
+
@trust.trust!
|
|
61
|
+
@message = "Workspace trusted"
|
|
62
|
+
end
|
|
63
|
+
@window&.request_frame
|
|
64
|
+
@trust.status
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def show_workspace_trust
|
|
68
|
+
@message = "Workspace #{@trust.status}"
|
|
69
|
+
@window&.request_frame
|
|
70
|
+
@trust.status
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|