Skip to content

Commit d957fd9

Browse files
authored
fix: handle errors in callback passed to context_manager.with (#372)
* Handle errors in callback passed to `context_manager.with` This more closely emulates the behavior of the Python `with` statement, which according to comments this is modeled off of. With this change the context will always be closed, even if an error occurs. This means that, for example, you can open a file without having to worry about an error leaking the file handle. Or if you created a context for creating temporary files and deleting them when done, you wouldn't need to worry about failing to delete the temp file. This change also re-enables disabled `context_manager` tests. Fixes #340 * Fix stylua errors
1 parent 1da13ad commit d957fd9

File tree

2 files changed

+178
-54
lines changed

2 files changed

+178
-54
lines changed

lua/plenary/context_manager.lua

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,26 @@ function context_manager.with(obj, callable)
1313
local ok, context = coroutine.resume(obj)
1414
assert(ok, "Should have yielded in coroutine.")
1515

16-
local result = callable(context)
16+
local succeeded, result = pcall(callable, context)
1717

1818
local done, _ = coroutine.resume(obj)
1919
assert(done, "Should be done")
2020

2121
local no_other = not coroutine.resume(obj)
2222
assert(no_other, "Should not yield anymore, otherwise that would make things complicated")
2323

24+
assert(succeeded, result)
2425
return result
2526
else
2627
assert(obj.enter)
2728
assert(obj.exit)
2829

2930
-- TODO: Callable can be string for vimL function or a lua callable
3031
local context = obj:enter()
31-
local result = callable(context)
32+
local succeeded, result = pcall(callable, context)
3233
obj:exit()
3334

35+
assert(succeeded, result)
3436
return result
3537
end
3638
end
Lines changed: 174 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,203 @@
1-
--[[
2-
local lu = require("luaunit")
3-
4-
local context_manager = require("plenary.context_manager")
5-
local debug_utils = require("plenary.debug_utils")
6-
local Path = require("plenary.path")
1+
local context_manager = require "plenary.context_manager"
2+
local debug_utils = require "plenary.debug_utils"
3+
local Path = require "plenary.path"
74

85
local with = context_manager.with
96
local open = context_manager.open
107

11-
local README_STR_PATH = vim.fn.fnamemodify(debug_utils.sourced_filepath(), ":h:h:h:h") .. "/README.md"
8+
local README_STR_PATH = vim.fn.fnamemodify(debug_utils.sourced_filepath(), ":h:h:h") .. "/README.md"
129
local README_FIRST_LINE = "# plenary.nvim"
1310

14-
TestContextManager = {}
11+
describe("context_manager", function()
12+
it("works with objects", function()
13+
local obj_manager = {
14+
enter = function(self)
15+
self.result = 10
16+
return self.result
17+
end,
1518

16-
function TestContextManager:testWorksWithObj()
17-
local obj_manager = {
18-
enter = function(self)
19-
self.result = 10
20-
return self.result
21-
end,
19+
exit = function() end,
20+
}
2221

23-
exit = function()
24-
end,
25-
}
22+
local result = with(obj_manager, function(obj)
23+
return obj
24+
end)
2625

27-
local result = with(obj_manager, function(obj)
28-
return obj
26+
assert.are.same(10, result)
27+
assert.are.same(obj_manager.result, result)
2928
end)
3029

30+
it("works with coroutine", function()
31+
local co = function()
32+
coroutine.yield(10)
33+
end
3134

32-
lu.assertEquals(10, result)
33-
lu.assertEquals(obj_manager.result, result)
34-
end
35+
local result = with(co, function(obj)
36+
return obj
37+
end)
38+
39+
assert.are.same(10, result)
40+
end)
3541

42+
it("does not work with coroutine with extra yields", function()
43+
local co = function()
44+
coroutine.yield(10)
3645

37-
function TestContextManager:testWorksWithCoroutine()
38-
local co = function()
39-
coroutine.yield(10)
40-
end
46+
-- Can't yield twice. That'd be bad and wouldn't make any sense.
47+
coroutine.yield(10)
48+
end
4149

42-
local result = with(co, function(obj)
43-
return obj
50+
assert.has.error_match(function()
51+
with(co, function(obj)
52+
return obj
53+
end)
54+
end, "Should not yield anymore, otherwise that would make things complicated")
4455
end)
4556

46-
lu.assertEquals(10, result)
47-
end
57+
it("reads from files with open", function()
58+
local result = with(open(README_STR_PATH), function(reader)
59+
return reader:read()
60+
end)
4861

49-
function TestContextManager:testDoesNotWorkWithCoroutineWithExtraYields()
50-
local co = function()
51-
coroutine.yield(10)
62+
assert.are.same(result, README_FIRST_LINE)
63+
end)
5264

53-
-- Can't yield twice. That'd be bad and wouldn't make any sense.
54-
coroutine.yield(10)
55-
end
65+
it("reads from Paths with open", function()
66+
local p = Path:new(README_STR_PATH)
5667

57-
lu.assertError(function()
58-
with(co, function(obj)
59-
return obj
68+
local result = with(open(p), function(reader)
69+
return reader:read()
6070
end)
71+
72+
assert.are.same(result, README_FIRST_LINE)
6173
end)
62-
end
6374

64-
function TestContextManager:testOpenWorks()
65-
local result = with(open(README_STR_PATH), function(reader)
66-
return reader:read()
75+
it("calls exit on error with objects", function()
76+
local entered = false
77+
local exited = false
78+
local obj_manager = {
79+
enter = function(self)
80+
entered = true
81+
end,
82+
83+
exit = function(self)
84+
exited = true
85+
end,
86+
}
87+
88+
assert.has.error_match(function()
89+
with(obj_manager, function(obj)
90+
assert(false, "failed in callback")
91+
end)
92+
end, "failed in callback")
93+
94+
assert.is["true"](entered)
95+
assert.is["true"](exited)
6796
end)
6897

69-
lu.assertEquals(result, README_FIRST_LINE)
70-
end
98+
it("calls exit on error with coroutines", function()
99+
local entered = false
100+
local exited = false
101+
local co = function()
102+
entered = true
103+
coroutine.yield(nil)
104+
105+
exited = true
106+
end
71107

72-
function TestContextManager:testOpenWorksWithPath()
73-
local p = Path:new(README_STR_PATH)
108+
assert.has.error_match(function()
109+
with(co, function(obj)
110+
assert(false, "failed in callback")
111+
end)
112+
end, "failed in callback")
74113

75-
local result = with(open(p), function(reader)
76-
return reader:read()
114+
assert.is["true"](entered)
115+
assert.is["true"](exited)
77116
end)
78117

79-
lu.assertEquals(result, README_FIRST_LINE)
80-
end
81-
--]]
118+
it("fails from enter error with objects", function()
119+
local exited = false
120+
local obj_manager = {
121+
enter = function(self)
122+
assert(false, "failed in enter")
123+
end,
124+
125+
exit = function(self)
126+
exited = true
127+
end,
128+
}
129+
130+
local ran_callback = false
131+
assert.has.error_match(function()
132+
with(obj_manager, function(obj)
133+
ran_callback = true
134+
end)
135+
end, "failed in enter")
136+
137+
assert.is["false"](ran_callback)
138+
assert.is["false"](exited)
139+
end)
140+
141+
it("fails from enter error with coroutines", function()
142+
local exited = false
143+
local co = function()
144+
assert(false, "failed in enter")
145+
coroutine.yield(nil)
146+
147+
exited = true
148+
end
149+
150+
local ran_callback = false
151+
assert.has.error_match(function()
152+
with(co, function(obj)
153+
ran_callback = true
154+
end)
155+
end, "Should have yielded in coroutine.")
156+
157+
assert.is["false"](ran_callback)
158+
assert.is["false"](exited)
159+
end)
160+
161+
it("fails from exit error with objects", function()
162+
local entered = false
163+
local obj_manager = {
164+
enter = function(self)
165+
entered = true
166+
end,
167+
168+
exit = function(self)
169+
assert(false, "failed in exit")
170+
end,
171+
}
172+
173+
local ran_callback = false
174+
assert.has.error_match(function()
175+
with(obj_manager, function(obj)
176+
ran_callback = true
177+
end)
178+
end, "failed in exit")
179+
180+
assert.is["true"](entered)
181+
assert.is["true"](ran_callback)
182+
end)
183+
184+
it("fails from exit error with coroutines", function()
185+
local entered = false
186+
local co = function()
187+
entered = true
188+
coroutine.yield(nil)
189+
190+
assert(false, "failed in exit")
191+
end
192+
193+
local ran_callback = false
194+
assert.has.error_match(function()
195+
with(co, function(obj)
196+
ran_callback = true
197+
end)
198+
end, "Should be done")
199+
200+
assert.is["true"](entered)
201+
assert.is["true"](ran_callback)
202+
end)
203+
end)

0 commit comments

Comments
 (0)