mirror of
https://github.com/neovim/neovim.git
synced 2024-12-26 14:11:15 -07:00
674db4b01f
This fixes a use-after-free noticed by ASAN which would occur when a dictwatcher was still active on a dictionary when the dictionary was freed. fun! MakeWatch() let d = {'foo': 'bar'} call dictwatcheradd(d, 'foo', function('...')) endfun Patch-by: oni-link Closes #5930
277 lines
8.7 KiB
Lua
277 lines
8.7 KiB
Lua
local helpers = require('test.functional.helpers')(after_each)
|
|
local clear, nvim, source = helpers.clear, helpers.nvim, helpers.source
|
|
local eq, next_msg = helpers.eq, helpers.next_message
|
|
local exc_exec = helpers.exc_exec
|
|
local command = helpers.command
|
|
local eval = helpers.eval
|
|
|
|
|
|
describe('dictionary change notifications', function()
|
|
local channel
|
|
|
|
setup(function()
|
|
clear()
|
|
channel = nvim('get_api_info')[1]
|
|
nvim('set_var', 'channel', channel)
|
|
end)
|
|
|
|
-- the same set of tests are applied to top-level dictionaries(g:, b:, w: and
|
|
-- t:) and a dictionary variable, so we generate them in the following
|
|
-- function.
|
|
local function gentests(dict_expr, dict_expr_suffix, dict_init)
|
|
if not dict_expr_suffix then
|
|
dict_expr_suffix = ''
|
|
end
|
|
|
|
local function update(opval, key)
|
|
if not key then
|
|
key = 'watched'
|
|
end
|
|
if opval == '' then
|
|
nvim('command', "unlet "..dict_expr..dict_expr_suffix..key)
|
|
else
|
|
nvim('command', "let "..dict_expr..dict_expr_suffix..key.." "..opval)
|
|
end
|
|
end
|
|
|
|
local function verify_echo()
|
|
-- helper to verify that no notifications are sent after certain change
|
|
-- to a dict
|
|
nvim('command', "call rpcnotify(g:channel, 'echo')")
|
|
eq({'notification', 'echo', {}}, next_msg())
|
|
end
|
|
|
|
local function verify_value(vals, key)
|
|
if not key then
|
|
key = 'watched'
|
|
end
|
|
eq({'notification', 'values', {key, vals}}, next_msg())
|
|
end
|
|
|
|
describe('watcher', function()
|
|
if dict_init then
|
|
setup(function()
|
|
source(dict_init)
|
|
end)
|
|
end
|
|
|
|
before_each(function()
|
|
source([[
|
|
function! g:Changed(dict, key, value)
|
|
if a:dict != ]]..dict_expr..[[ |
|
|
throw 'invalid dict'
|
|
endif
|
|
call rpcnotify(g:channel, 'values', a:key, a:value)
|
|
endfunction
|
|
call dictwatcheradd(]]..dict_expr..[[, "watched", "g:Changed")
|
|
call dictwatcheradd(]]..dict_expr..[[, "watched2", "g:Changed")
|
|
]])
|
|
end)
|
|
|
|
after_each(function()
|
|
source([[
|
|
call dictwatcherdel(]]..dict_expr..[[, "watched", "g:Changed")
|
|
call dictwatcherdel(]]..dict_expr..[[, "watched2", "g:Changed")
|
|
]])
|
|
update('= "test"')
|
|
update('= "test2"', 'watched2')
|
|
update('', 'watched2')
|
|
update('')
|
|
verify_echo()
|
|
end)
|
|
|
|
it('is not triggered when unwatched keys are updated', function()
|
|
update('= "noop"', 'unwatched')
|
|
update('.= "noop2"', 'unwatched')
|
|
update('', 'unwatched')
|
|
verify_echo()
|
|
end)
|
|
|
|
it('is triggered by remove()', function()
|
|
update('= "test"')
|
|
verify_value({new = 'test'})
|
|
nvim('command', 'call remove('..dict_expr..', "watched")')
|
|
verify_value({old = 'test'})
|
|
end)
|
|
|
|
it('is triggered by extend()', function()
|
|
update('= "xtend"')
|
|
verify_value({new = 'xtend'})
|
|
nvim('command', [[
|
|
call extend(]]..dict_expr..[[, {'watched': 'xtend2', 'watched2': 5, 'watched3': 'a'})
|
|
]])
|
|
verify_value({old = 'xtend', new = 'xtend2'})
|
|
verify_value({new = 5}, 'watched2')
|
|
update('')
|
|
verify_value({old = 'xtend2'})
|
|
update('', 'watched2')
|
|
verify_value({old = 5}, 'watched2')
|
|
update('', 'watched3')
|
|
verify_echo()
|
|
end)
|
|
|
|
it('is triggered with key patterns', function()
|
|
source([[
|
|
call dictwatcheradd(]]..dict_expr..[[, "wat*", "g:Changed")
|
|
]])
|
|
update('= 1')
|
|
verify_value({new = 1})
|
|
verify_value({new = 1})
|
|
update('= 3', 'watched2')
|
|
verify_value({new = 3}, 'watched2')
|
|
verify_value({new = 3}, 'watched2')
|
|
verify_echo()
|
|
source([[
|
|
call dictwatcherdel(]]..dict_expr..[[, "wat*", "g:Changed")
|
|
]])
|
|
-- watch every key pattern
|
|
source([[
|
|
call dictwatcheradd(]]..dict_expr..[[, "*", "g:Changed")
|
|
]])
|
|
update('= 3', 'another_key')
|
|
update('= 4', 'another_key')
|
|
update('', 'another_key')
|
|
update('= 2')
|
|
verify_value({new = 3}, 'another_key')
|
|
verify_value({old = 3, new = 4}, 'another_key')
|
|
verify_value({old = 4}, 'another_key')
|
|
verify_value({old = 1, new = 2})
|
|
verify_value({old = 1, new = 2})
|
|
verify_echo()
|
|
source([[
|
|
call dictwatcherdel(]]..dict_expr..[[, "*", "g:Changed")
|
|
]])
|
|
end)
|
|
|
|
-- test a sequence of updates of different types to ensure proper memory
|
|
-- management(with ASAN)
|
|
local function test_updates(tests)
|
|
it('test change sequence', function()
|
|
local input, output
|
|
for i = 1, #tests do
|
|
input, output = unpack(tests[i])
|
|
update(input)
|
|
verify_value(output)
|
|
end
|
|
end)
|
|
end
|
|
|
|
test_updates({
|
|
{'= 3', {new = 3}},
|
|
{'= 6', {old = 3, new = 6}},
|
|
{'+= 3', {old = 6, new = 9}},
|
|
{'', {old = 9}}
|
|
})
|
|
|
|
test_updates({
|
|
{'= "str"', {new = 'str'}},
|
|
{'= "str2"', {old = 'str', new = 'str2'}},
|
|
{'.= "2str"', {old = 'str2', new = 'str22str'}},
|
|
{'', {old = 'str22str'}}
|
|
})
|
|
|
|
test_updates({
|
|
{'= [1, 2]', {new = {1, 2}}},
|
|
{'= [1, 2, 3]', {old = {1, 2}, new = {1, 2, 3}}},
|
|
-- the += will update the list in place, so old and new are the same
|
|
{'+= [4, 5]', {old = {1, 2, 3, 4, 5}, new = {1, 2, 3, 4, 5}}},
|
|
{'', {old = {1, 2, 3, 4 ,5}}}
|
|
})
|
|
|
|
test_updates({
|
|
{'= {"k": "v"}', {new = {k = 'v'}}},
|
|
{'= {"k1": 2}', {old = {k = 'v'}, new = {k1 = 2}}},
|
|
{'', {old = {k1 = 2}}},
|
|
})
|
|
end)
|
|
end
|
|
|
|
gentests('g:')
|
|
gentests('b:')
|
|
gentests('w:')
|
|
gentests('t:')
|
|
gentests('g:dict_var', '.', 'let g:dict_var = {}')
|
|
|
|
describe('multiple watchers on the same dict/key', function()
|
|
setup(function()
|
|
source([[
|
|
function! g:Watcher1(dict, key, value)
|
|
call rpcnotify(g:channel, '1', a:key, a:value)
|
|
endfunction
|
|
function! g:Watcher2(dict, key, value)
|
|
call rpcnotify(g:channel, '2', a:key, a:value)
|
|
endfunction
|
|
call dictwatcheradd(g:, "key", "g:Watcher1")
|
|
call dictwatcheradd(g:, "key", "g:Watcher2")
|
|
]])
|
|
end)
|
|
|
|
it('invokes all callbacks when the key is changed', function()
|
|
nvim('command', 'let g:key = "value"')
|
|
eq({'notification', '1', {'key', {new = 'value'}}}, next_msg())
|
|
eq({'notification', '2', {'key', {new = 'value'}}}, next_msg())
|
|
end)
|
|
|
|
it('only removes watchers that fully match dict, key and callback', function()
|
|
nvim('command', 'call dictwatcherdel(g:, "key", "g:Watcher1")')
|
|
nvim('command', 'let g:key = "v2"')
|
|
eq({'notification', '2', {'key', {old = 'value', new = 'v2'}}}, next_msg())
|
|
end)
|
|
end)
|
|
|
|
describe('errors', function()
|
|
-- WARNING: This suite depends on the above tests
|
|
it('fails to remove if no watcher with matching callback is found', function()
|
|
eq("Vim(call):Couldn't find a watcher matching key and callback",
|
|
exc_exec('call dictwatcherdel(g:, "key", "g:Watcher1")'))
|
|
end)
|
|
|
|
it('fails to remove if no watcher with matching key is found', function()
|
|
eq("Vim(call):Couldn't find a watcher matching key and callback",
|
|
exc_exec('call dictwatcherdel(g:, "invalid_key", "g:Watcher2")'))
|
|
end)
|
|
|
|
it("does not fail to add/remove if the callback doesn't exist", function()
|
|
command('call dictwatcheradd(g:, "key", "g:InvalidCb")')
|
|
command('call dictwatcherdel(g:, "key", "g:InvalidCb")')
|
|
end)
|
|
|
|
it('fails with empty keys', function()
|
|
eq("Vim(call):E713: Cannot use empty key for Dictionary",
|
|
exc_exec('call dictwatcheradd(g:, "", "g:Watcher1")'))
|
|
eq("Vim(call):E713: Cannot use empty key for Dictionary",
|
|
exc_exec('call dictwatcherdel(g:, "", "g:Watcher1")'))
|
|
end)
|
|
|
|
it('does not fail to replace a watcher function', function()
|
|
source([[
|
|
function! g:ReplaceWatcher2()
|
|
function! g:Watcher2(dict, key, value)
|
|
call rpcnotify(g:channel, '2b', a:key, a:value)
|
|
endfunction
|
|
endfunction
|
|
]])
|
|
command('call g:ReplaceWatcher2()')
|
|
command('let g:key = "value"')
|
|
eq({'notification', '2b', {'key', {old = 'v2', new = 'value'}}}, next_msg())
|
|
|
|
end)
|
|
|
|
it('does not crash when freeing a watched dictionary', function()
|
|
source([[
|
|
function! Watcher(dict, key, value)
|
|
echo a:key string(a:value)
|
|
endfunction
|
|
|
|
function! MakeWatch()
|
|
let d = {'foo': 'bar'}
|
|
call dictwatcheradd(d, 'foo', function('Watcher'))
|
|
endfunction
|
|
]])
|
|
|
|
command('call MakeWatch()')
|
|
eq(2, eval('1+1')) -- Still alive?
|
|
end)
|
|
end)
|
|
end)
|