Skip to content

Commit

Permalink
eval: Make dictionary watchers work with empty keys
Browse files Browse the repository at this point in the history
Looks like dict_notifications_spec test used to depend on some state which 
should not be preserved. Changed all `setup()` calls to `before_each()` and 
added necessary state in addition to changes required to test empty keys.

Note: unit tests for tv_dict_watcher* are still needed.
  • Loading branch information
ZyX-I committed Dec 8, 2016
1 parent 4c042fb commit 0d54f0d
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 31 deletions.
16 changes: 3 additions & 13 deletions src/nvim/eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ static char *e_missbrac = N_("E111: Missing ']'");
static char *e_listarg = N_("E686: Argument of %s must be a List");
static char *e_listdictarg = N_(
"E712: Argument of %s must be a List or Dictionary");
static char *e_emptykey = N_("E713: Cannot use empty key for Dictionary");
static char *e_listreq = N_("E714: List required");
static char *e_dictreq = N_("E715: Dictionary required");
static char *e_strreq = N_("E114: String required");
Expand Down Expand Up @@ -2104,8 +2103,9 @@ static const char_u *get_lval(
for (len = 0; ASCII_ISALNUM(key[len]) || key[len] == '_'; ++len)
;
if (len == 0) {
if (!quiet)
EMSG(_(e_emptykey));
if (!quiet) {
EMSG(_("E713: Cannot use empty key after ."));
}
return NULL;
}
p = key + len;
Expand Down Expand Up @@ -7078,11 +7078,6 @@ static void f_dictwatcheradd(typval_T *argvars, typval_T *rettv, FunPtr fptr)
}
const size_t key_len = STRLEN(argvars[1].vval.v_string);

if (key_len == 0) {
EMSG(_(e_emptykey));
return;
}

ufunc_T *func = find_ufunc(argvars[2].vval.v_string);
if (!func) {
// Invalid function name. Error already reported by `find_ufunc`.
Expand Down Expand Up @@ -7115,11 +7110,6 @@ static void f_dictwatcherdel(typval_T *argvars, typval_T *rettv, FunPtr fptr)
}
const size_t key_len = STRLEN(argvars[1].vval.v_string);

if (key_len == 0) {
EMSG(_(e_emptykey));
return;
}

ufunc_T *func = find_ufunc(argvars[2].vval.v_string);
if (!func) {
// Invalid function name. Error already reported by `find_ufunc`.
Expand Down
2 changes: 1 addition & 1 deletion src/nvim/eval/typval.c
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ static bool tv_dict_watcher_matches(DictWatcher *watcher, const char *const key)
// of the string means it should match everything up to the '*' instead of the
// whole string.
const size_t len = watcher->key_pattern_len;
if (watcher->key_pattern[len - 1] == '*') {
if (len && watcher->key_pattern[len - 1] == '*') {
return strncmp(key, watcher->key_pattern, len - 1) == 0;
} else {
return strcmp(key, watcher->key_pattern) == 0;
Expand Down
66 changes: 49 additions & 17 deletions test/functional/ex_cmds/dict_notifications_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ 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 nvim_command = helpers.command


describe('dictionary change notifications', function()
local channel

setup(function()
before_each(function()
clear()
channel = nvim('get_api_info')[1]
nvim('set_var', 'channel', channel)
Expand All @@ -16,19 +17,16 @@ describe('dictionary change notifications', function()
-- 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 gentests(dict_expr, dict_init)

local function update(opval, key)
if not key then
key = 'watched'
end
if opval == '' then
nvim('command', "unlet "..dict_expr..dict_expr_suffix..key)
nvim_command(('unlet %s[\'%s\']'):format(dict_expr, key))
else
nvim('command', "let "..dict_expr..dict_expr_suffix..key.." "..opval)
nvim_command(('let %s[\'%s\'] %s'):format(dict_expr, key, opval))
end
end

Expand All @@ -48,7 +46,7 @@ describe('dictionary change notifications', function()

describe('watcher', function()
if dict_init then
setup(function()
before_each(function()
source(dict_init)
end)
end
Expand Down Expand Up @@ -152,6 +150,32 @@ describe('dictionary change notifications', function()
]])
end)

it('is triggered for empty keys', function()
nvim_command([[
call dictwatcheradd(]]..dict_expr..[[, "", "g:Changed")
]])
update('= 1', '')
verify_value({new = 1}, '')
update('= 2', '')
verify_value({old = 1, new = 2}, '')
nvim_command([[
call dictwatcherdel(]]..dict_expr..[[, "", "g:Changed")
]])
end)

it('is triggered for empty keys when using catch-all *', function()
nvim_command([[
call dictwatcheradd(]]..dict_expr..[[, "*", "g:Changed")
]])
update('= 1', '')
verify_value({new = 1}, '')
update('= 2', '')
verify_value({old = 1, new = 2}, '')
nvim_command([[
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)
Expand Down Expand Up @@ -199,10 +223,10 @@ describe('dictionary change notifications', function()
gentests('b:')
gentests('w:')
gentests('t:')
gentests('g:dict_var', '.', 'let g:dict_var = {}')
gentests('g:dict_var', 'let g:dict_var = {}')

describe('multiple watchers on the same dict/key', function()
setup(function()
before_each(function()
source([[
function! g:Watcher1(dict, key, value)
call rpcnotify(g:channel, '1', a:key, a:value)
Expand All @@ -222,13 +246,27 @@ describe('dictionary change notifications', function()
end)

it('only removes watchers that fully match dict, key and callback', function()
nvim('command', 'let g:key = "value"')
eq({'notification', '1', {'key', {new = 'value'}}}, next_msg())
eq({'notification', '2', {'key', {new = 'value'}}}, next_msg())
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()
before_each(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
]])
end)

-- 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",
Expand All @@ -247,15 +285,9 @@ describe('dictionary change notifications', function()
exc_exec('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('fails to replace a watcher function', function()
source([[
call dictwatcheradd(g:, "key", "g:Watcher2")
function! g:ReplaceWatcher2()
function! g:Watcher2()
endfunction
Expand Down

0 comments on commit 0d54f0d

Please sign in to comment.