Repository navigation
Expand file tree
/
Copy pathasync_spec.lua
More file actions
463 lines (389 loc) · 17.5 KB
/
Copy pathasync_spec.lua
File metadata and controls
463 lines (389 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
local async = require("pack.async")
local state = require("pack.state")
local persist = require("pack.persist")
describe("pack.async.parse_behind_count", function()
it("parses a well-formed count with trailing newline", function()
assert.equals(3, async.parse_behind_count("3\n"))
end)
it("parses zero", function()
assert.equals(0, async.parse_behind_count("0"))
end)
it("returns nil for empty output", function()
assert.is_nil(async.parse_behind_count(""))
end)
it("returns nil for garbage/error output", function()
assert.is_nil(async.parse_behind_count("fatal: no upstream configured for branch"))
end)
it("returns nil for non-string input", function()
assert.is_nil(async.parse_behind_count(nil))
end)
end)
describe("pack.async.check_outdated", function()
local tmp_path
before_each(function()
tmp_path = vim.fn.tempname() .. "-pack-disabled.json"
persist._set_path_for_testing(tmp_path)
end)
after_each(function()
if vim.fn.filereadable(tmp_path) == 1 then
vim.fn.delete(tmp_path)
end
persist._set_path_for_testing(nil)
end)
local function config_with(plugins)
return {
install_path = vim.fn.tempname() .. "-pack-install",
plugins = plugins,
}
end
it("no-ops when plugin is disabled", function()
state.init(config_with({ "user/foo.nvim" }))
local p = state.get_plugins()["foo.nvim"]
-- Mark as disabled
state.set_disabled("foo.nvim", true)
-- Call check_outdated directly with ineligible plugin
async.check_outdated(p)
-- After a short wait, verify nothing was spawned (no behind/checked_at updates)
vim.wait(100)
assert.is_nil(p.behind)
assert.is_nil(p.checked_at)
end)
it("invokes done exactly once on an ineligible plugin (busy tracking)", function()
state.init(config_with({ "user/foo.nvim" }))
local p = state.get_plugins()["foo.nvim"]
state.update_status("foo.nvim", "missing")
local calls = 0
async.check_outdated(p, function() calls = calls + 1 end)
vim.wait(100)
assert.equals(1, calls)
end)
it("no-ops when plugin status is not installed or loaded", function()
state.init(config_with({ "user/foo.nvim" }))
local p = state.get_plugins()["foo.nvim"]
-- Manually set status to 'missing' (ineligible)
state.update_status("foo.nvim", "missing")
-- Call check_outdated directly with ineligible plugin
async.check_outdated(p)
-- After a short wait, verify nothing was spawned (no behind/checked_at updates)
vim.wait(100)
assert.is_nil(p.behind)
assert.is_nil(p.checked_at)
end)
it("clears stale outdated detail when behind reports 0", function()
state.init(config_with({ "user/foo.nvim" }))
local p = state.get_plugins()["foo.nvim"]
-- Simulate leftover rich detail from a previous outdated check.
state.set_outdated_detail("foo.nvim", {
revision_before = "abc1234",
revision_after = "def5678",
upstream_branch = "main",
pending_commits = { "abc1234 │ some old commit" },
})
assert.is_not_nil(p.revision_before)
-- The plugin has since caught up (e.g. via a manual `git pull` outside
-- the dashboard); a re-check should clear the stale detail once
-- set_behind reports 0, not just leave it lingering.
state.set_behind("foo.nvim", 0)
state.set_outdated_detail("foo.nvim", {})
assert.equals(0, p.behind)
assert.is_nil(p.revision_before)
assert.is_nil(p.revision_after)
assert.is_nil(p.upstream_branch)
assert.is_nil(p.pending_commits)
end)
it("runs the full check_outdated chain against a real local git repo", function()
-- Build a real upstream repo with one commit.
local upstream_dir = vim.fn.tempname() .. "-pack-upstream"
vim.fn.mkdir(upstream_dir, "p")
local function run(cmd, cwd)
vim.fn.system(cmd)
assert.equals(0, vim.v.shell_error, table.concat(cmd, " ") .. " failed")
end
run({ "git", "init", "-q", upstream_dir })
run({ "git", "-C", upstream_dir, "config", "user.email", "pack-test@example.com" })
run({ "git", "-C", upstream_dir, "config", "user.name", "pack-test" })
vim.fn.writefile({ "hello" }, upstream_dir .. "/file.txt")
run({ "git", "-C", upstream_dir, "add", "file.txt" })
run({ "git", "-C", upstream_dir, "commit", "-q", "-m", "initial commit" })
-- Clone it, simulating the plugin's install dir.
local clone_dir = vim.fn.tempname() .. "-pack-clone"
run({ "git", "clone", "-q", upstream_dir, clone_dir })
-- Make a second, pending commit upstream that the clone doesn't have yet.
vim.fn.writefile({ "hello", "world" }, upstream_dir .. "/file.txt")
run({ "git", "-C", upstream_dir, "add", "file.txt" })
run({ "git", "-C", upstream_dir, "commit", "-q", "-m", "second commit" })
state.init(config_with({ "user/fixture.nvim" }))
local fixture = state.get_plugins()["fixture.nvim"]
fixture.dir = clone_dir
fixture.status = "installed"
fixture.disabled = false
async.check_outdated(fixture)
local ok = vim.wait(5000, function()
return fixture.behind ~= nil and fixture.pending_commits ~= nil
end, 20)
assert.is_true(ok, "check_outdated did not complete in time; log:\n" .. table.concat(fixture.log, "\n"))
assert.equals(1, fixture.behind)
assert.is_not_nil(fixture.revision_before)
assert.is_not_nil(fixture.revision_after)
assert.matches("^%x%x%x%x+$", fixture.revision_before)
assert.matches("^%x%x%x%x+$", fixture.revision_after)
assert.is_not.equals(fixture.revision_before, fixture.revision_after)
assert.is_table(fixture.pending_commits)
assert.equals(1, #fixture.pending_commits)
assert.matches("^%x%x%x%x+ │ second commit$", fixture.pending_commits[1])
vim.fn.delete(upstream_dir, "rf")
vim.fn.delete(clone_dir, "rf")
end)
end)
describe("pack.async.upstream_ref", function()
-- upstream_ref is async: it takes a callback and may spawn `git symbolic-ref`
-- rather than blocking the UI thread.
local function resolve(plugin, dir)
local called, ref = false, nil
async.upstream_ref(plugin, dir, function(r)
called, ref = true, r
end)
vim.wait(2000, function()
return called
end)
assert.is_true(called, "upstream_ref must invoke its callback")
return ref
end
it("tracks origin/<branch> for a branch-pinned plugin", function()
assert.equals("origin/develop", resolve({ branch = "develop" }, "/nope"))
end)
it("returns nil for a tag/commit/version pin (nothing to track)", function()
assert.is_nil(resolve({ tag = "v1.0.0" }, "/nope"))
assert.is_nil(resolve({ commit = "abcdef1" }, "/nope"))
assert.is_nil(resolve({ version = ">=1.0" }, "/nope"))
end)
it("resolves the remote default branch (origin/HEAD) for an unpinned plugin", function()
-- Real clone so origin/HEAD is set; mirrors native's detached-HEAD install.
local upstream = vim.fn.tempname() .. "-uref-up"
vim.fn.mkdir(upstream, "p")
local function run(cmd) vim.fn.system(cmd); assert.equals(0, vim.v.shell_error, table.concat(cmd, " ")) end
run({ "git", "init", "-q", "-b", "main", upstream })
run({ "git", "-C", upstream, "config", "user.email", "t@t" })
run({ "git", "-C", upstream, "config", "user.name", "t" })
vim.fn.writefile({ "x" }, upstream .. "/f.txt")
run({ "git", "-C", upstream, "add", "-A" })
run({ "git", "-C", upstream, "commit", "-q", "-m", "i" })
local clone = vim.fn.tempname() .. "-uref-clone"
run({ "git", "clone", "-q", upstream, clone })
assert.equals("origin/main", resolve({}, clone))
vim.fn.delete(upstream, "rf")
vim.fn.delete(clone, "rf")
end)
end)
describe("pack.async.parse_revision_pair", function()
it("splits two-line rev-parse output", function()
local before, after = async.parse_revision_pair("abc123\ndef456\n")
assert.equals("abc123", before)
assert.equals("def456", after)
end)
it("returns nils for empty output", function()
local before, after = async.parse_revision_pair("")
assert.is_nil(before)
assert.is_nil(after)
end)
it("returns nils for non-string input", function()
local before, after = async.parse_revision_pair(nil)
assert.is_nil(before)
assert.is_nil(after)
end)
end)
describe("pack.async.parse_upstream_branch_name", function()
it("strips a single remote-name prefix", function()
assert.equals("main", async.parse_upstream_branch_name("origin/main\n"))
end)
it("only strips the first segment for a branch name containing slashes", function()
assert.equals("feature/foo", async.parse_upstream_branch_name("origin/feature/foo"))
end)
it("returns the trimmed input when there is no remote prefix", function()
assert.equals("main", async.parse_upstream_branch_name("main"))
end)
it("returns nil for empty or non-string input", function()
assert.is_nil(async.parse_upstream_branch_name(""))
assert.is_nil(async.parse_upstream_branch_name(nil))
end)
end)
describe("pack.async.parse_pending_commits", function()
it("splits multi-line git log output into a list", function()
local commits = async.parse_pending_commits("abc123 │ fix: x\ndef456 │ feat: y")
assert.same({ "abc123 │ fix: x", "def456 │ feat: y" }, commits)
end)
it("returns an empty list for empty or non-string output", function()
assert.same({}, async.parse_pending_commits(""))
assert.same({}, async.parse_pending_commits(nil))
end)
end)
describe("pack.async.run_build_hook", function()
local function fixture_plugin(build)
return { name = "fixture.nvim", dir = vim.fn.tempname(), build = build, log = {} }
end
it("no-ops (still calls done) when there is no build hook", function()
local done = false
async.run_build_hook(fixture_plugin(nil), function() done = true end)
assert.is_true(done)
end)
it("runs a function build hook and calls done", function()
local ran = false
local done = false
async.run_build_hook(fixture_plugin(function() ran = true end), function() done = true end)
vim.wait(500, function() return done end, 10)
assert.is_true(ran)
assert.is_true(done)
end)
it("runs a string build hook via the shell in the plugin dir", function()
local p = fixture_plugin("echo hi > built.txt")
vim.fn.mkdir(p.dir, "p")
local done = false
async.run_build_hook(p, function() done = true end)
assert.is_true(vim.wait(3000, function() return done end, 20), "build did not finish")
assert.equals(1, vim.fn.filereadable(p.dir .. "/built.txt"))
vim.fn.delete(p.dir, "rf")
end)
it("runs a ':' string build hook as a Vim command (lazy.nvim style)", function()
_G.PACK_BUILD_CMD_RAN = nil
vim.api.nvim_create_user_command("PackBuildTestCmd", function()
_G.PACK_BUILD_CMD_RAN = true
end, {})
local done = false
async.run_build_hook(fixture_plugin(":PackBuildTestCmd"), function() done = true end)
assert.is_true(vim.wait(500, function() return done end, 10), "build did not finish")
assert.is_true(_G.PACK_BUILD_CMD_RAN, "':' build must run as a Vim command")
vim.api.nvim_del_user_command("PackBuildTestCmd")
_G.PACK_BUILD_CMD_RAN = nil
end)
it("sources plugin directory commands before executing ':' ex-command build hooks", function()
local dir = vim.fn.tempname() .. "-plugin-hook"
vim.fn.mkdir(dir .. "/plugin", "p")
vim.fn.writefile({ "vim.api.nvim_create_user_command('LazySourcedCmd', function() _G.PACK_SOURCED_RAN = true end, {})" }, dir .. "/plugin/test.lua")
_G.PACK_SOURCED_RAN = nil
local p = fixture_plugin(":LazySourcedCmd")
p.dir = dir
local done = false
async.run_build_hook(p, function() done = true end)
assert.is_true(vim.wait(500, function() return done end, 10), "build did not finish")
assert.is_true(_G.PACK_SOURCED_RAN, "':' build hook must source plugin/ files before executing command")
if pcall(vim.api.nvim_del_user_command, "LazySourcedCmd") then end
_G.PACK_SOURCED_RAN = nil
vim.fn.delete(dir, "rf")
end)
it("updates plugin status to error when a build hook fails", function()
local orig_notify = vim.notify
vim.notify = function() end
local p = fixture_plugin(function() error("build step error") end)
p.status = "installed"
local done = false
async.run_build_hook(p, function() done = true end)
assert.is_true(vim.wait(500, function() return done end, 10), "build did not finish")
assert.equals("error", p.status, "failing build hook must update status to error")
vim.notify = orig_notify
end)
it("does not clobber a status set mid-build back to the pre-build placeholder", function()
-- Mirrors a fresh install: PackChanged(install) fires (and calls run_build_hook)
-- BEFORE native's load_fn callback flips status away from "missing". If the
-- build hook's own load_fn/M.load race finishes first and sets "loaded",
-- run_build_hook must not stomp that back down to "missing" on completion.
local p = { name = "fixture.nvim", dir = vim.fn.tempname(), log = {} }
p.status = "missing"
p.build = function()
p.status = "loaded"
end
local done = false
async.run_build_hook(p, function() done = true end)
assert.is_true(vim.wait(500, function() return done end, 10), "build did not finish")
assert.equals("loaded", p.status, "run_build_hook must not revert status to the pre-build placeholder")
end)
it("keeps a loaded plugin loaded after a rebuild (e.g. :Pack build)", function()
-- Rebuilding an already-loaded plugin (no update flow to restore status) must
-- not downgrade it to "installed". The build hook runs while status shows
-- "building"; on completion it returns to the pre-build "loaded".
local p = { name = "fixture.nvim", dir = vim.fn.tempname(), log = {} }
p.status = "loaded"
p.build = function() end -- does not touch status
local done = false
async.run_build_hook(p, function() done = true end)
assert.is_true(vim.wait(500, function() return done end, 10), "build did not finish")
assert.equals("loaded", p.status, "rebuild of a loaded plugin must stay loaded, not drop to installed")
end)
it("runs a list of build hooks in sequence and calls done exactly once", function()
local order = {}
local p = fixture_plugin({
function() table.insert(order, "a") end,
function() table.insert(order, "b") end,
})
local done_count = 0
async.run_build_hook(p, function() done_count = done_count + 1 end)
assert.is_true(vim.wait(1000, function() return done_count > 0 end, 10), "build did not finish")
assert.same({ "a", "b" }, order)
assert.equals(1, done_count, "done must be called once for a list of hooks")
end)
it("streams shell build output and tracks step progress in real time", function()
local dir = vim.fn.tempname() .. "-build-stream"
vim.fn.mkdir(dir, "p")
local p = {
name = "stream-fixture.nvim",
dir = dir,
status = "installed",
log = {},
build = { "echo line1", "echo line2" },
}
local done = false
async.run_build_hook(p, function() done = true end)
assert.is_true(vim.wait(2000, function() return done end, 10), "streaming build did not finish")
local log_text = table.concat(p.log, "\n")
assert.is_true(log_text:match("line1") ~= nil, "log should contain stdout line1")
assert.is_true(log_text:match("line2") ~= nil, "log should contain stdout line2")
assert.is_nil(p.build_progress, "build_progress should be cleared after completion")
vim.fn.delete(dir, "rf")
end)
end)
describe("pack.async.setup_build_hooks", function()
it("runs a plugin's build hook on a PackChanged install event", function()
local persist = require("pack.persist")
local tmp_path = vim.fn.tempname() .. "-disabled.json"
persist._set_path_for_testing(tmp_path)
state.init({ install_path = vim.fn.tempname() .. "-install", plugins = { "user/foo.nvim" } })
local ran = false
state.get_plugins()["foo.nvim"].build = function() ran = true end
async.setup_build_hooks()
vim.api.nvim_exec_autocmds("PackChanged", {
pattern = "/fake/foo.nvim",
data = { kind = "install", spec = { name = "foo.nvim" }, path = "/fake/foo.nvim" },
})
assert.is_true(vim.wait(500, function() return ran end, 10), "build hook did not run on PackChanged")
if vim.fn.filereadable(tmp_path) == 1 then vim.fn.delete(tmp_path) end
persist._set_path_for_testing(nil)
end)
it("ignores PackChanged delete events", function()
state.init({ install_path = vim.fn.tempname() .. "-install", plugins = { "user/foo.nvim" } })
local ran = false
state.get_plugins()["foo.nvim"].build = function() ran = true end
async.setup_build_hooks()
vim.api.nvim_exec_autocmds("PackChanged", {
pattern = "/fake/foo.nvim",
data = { kind = "delete", spec = { name = "foo.nvim" }, path = "/fake/foo.nvim" },
})
vim.wait(200)
assert.is_false(ran)
end)
it("restores status and clears behind when a PackChanged update completes", function()
state.init({ install_path = vim.fn.tempname() .. "-install", plugins = { "user/foo.nvim" } })
local p = state.get_plugins()["foo.nvim"]
-- Simulate an in-flight update: update_plugins flips status to "updating"
-- and stashes the prior status.
p.status = "updating"
p.status_before_update = "loaded"
state.set_behind("foo.nvim", 3)
async.setup_build_hooks()
vim.api.nvim_exec_autocmds("PackChanged", {
pattern = "/fake/foo.nvim",
data = { kind = "update", spec = { name = "foo.nvim" }, path = "/fake/foo.nvim" },
})
assert.is_true(vim.wait(500, function() return p.status == "loaded" end, 10), "status was not restored")
assert.equals(0, p.behind)
assert.is_nil(p.status_before_update)
end)
end)