API: return non-generic VimL errors

- Return VimL errors instead of generic errors for:
  - nvim_call_function
  - nvim_call_dict_function
- Fix tests which were silently broken before this change.

This violates #6150 where we agreed not to translate API errors.  But
that can be fixed later.
This commit is contained in:
Justin M. Keyes
2018-05-07 03:24:01 +02:00
parent 33bfea31b0
commit c9f3174075
10 changed files with 105 additions and 51 deletions

View File

@@ -46,8 +46,7 @@
/// Executes an ex-command. /// Executes an ex-command.
/// ///
/// On parse error: forwards the Vim error; does not update v:errmsg. /// On execution error: fails with VimL error, does not update v:errmsg.
/// On runtime error: forwards the Vim error; does not update v:errmsg.
/// ///
/// @param command Ex-command string /// @param command Ex-command string
/// @param[out] err Error details (Vim error), if any /// @param[out] err Error details (Vim error), if any
@@ -103,7 +102,8 @@ Dictionary nvim_get_hl_by_id(Integer hl_id, Boolean rgb, Error *err)
} }
/// Passes input keys to Nvim. /// Passes input keys to Nvim.
/// On VimL error: Does not fail, but updates v:errmsg. ///
/// On execution error: does not fail, but updates v:errmsg.
/// ///
/// @param keys to be typed /// @param keys to be typed
/// @param mode mapping options /// @param mode mapping options
@@ -169,7 +169,8 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_csi)
} }
/// Passes keys to Nvim as raw user-input. /// Passes keys to Nvim as raw user-input.
/// On VimL error: Does not fail, but updates v:errmsg. ///
/// On execution error: does not fail, but updates v:errmsg.
/// ///
/// Unlike `nvim_feedkeys`, this uses a lower-level input buffer and the call /// Unlike `nvim_feedkeys`, this uses a lower-level input buffer and the call
/// is not deferred. This is the most reliable way to send real user input. /// is not deferred. This is the most reliable way to send real user input.
@@ -213,8 +214,7 @@ String nvim_replace_termcodes(String str, Boolean from_part, Boolean do_lt,
/// Executes an ex-command and returns its (non-error) output. /// Executes an ex-command and returns its (non-error) output.
/// Shell |:!| output is not captured. /// Shell |:!| output is not captured.
/// ///
/// On parse error: forwards the Vim error; does not update v:errmsg. /// On execution error: fails with VimL error, does not update v:errmsg.
/// On runtime error: forwards the Vim error; does not update v:errmsg.
/// ///
/// @param command Ex-command string /// @param command Ex-command string
/// @param[out] err Error details (Vim error), if any /// @param[out] err Error details (Vim error), if any
@@ -259,7 +259,8 @@ theend:
/// Evaluates a VimL expression (:help expression). /// Evaluates a VimL expression (:help expression).
/// Dictionaries and Lists are recursively expanded. /// Dictionaries and Lists are recursively expanded.
/// On VimL error: Returns a generic error; v:errmsg is not updated. ///
/// On execution error: fails with generic error; v:errmsg is not updated.
/// ///
/// @param expr VimL expression string /// @param expr VimL expression string
/// @param[out] err Error details, if any /// @param[out] err Error details, if any
@@ -331,18 +332,23 @@ static Object _call_function(String fn, Array args, dict_T *self, Error *err)
} }
try_start(); try_start();
// Call the function msg_first_ignored_err = NULL;
typval_T rettv; typval_T rettv;
int dummy; int dummy;
int r = call_func((char_u *)fn.data, (int)fn.size, &rettv, (int)args.size, int r = call_func((char_u *)fn.data, (int)fn.size, &rettv, (int)args.size,
vim_args, NULL, curwin->w_cursor.lnum, vim_args, NULL, curwin->w_cursor.lnum,
curwin->w_cursor.lnum, &dummy, true, NULL, self); curwin->w_cursor.lnum, &dummy, true, NULL, self);
if (r == FAIL) { // call_func() retval is deceptive; must also check did_emsg et al.
api_set_error(err, kErrorTypeException, "Error calling function"); if (msg_first_ignored_err
&& (r == FAIL || (did_emsg && force_abort && !current_exception))) {
api_set_error(err, kErrorTypeException, msg_first_ignored_err);
} }
if (!try_end(err)) { if (!try_end(err)) {
rv = vim_to_object(&rettv); rv = vim_to_object(&rettv);
} }
xfree(msg_first_ignored_err);
msg_first_ignored_err = NULL;
tv_clear(&rettv); tv_clear(&rettv);
free_vim_args: free_vim_args:
@@ -355,7 +361,7 @@ free_vim_args:
/// Calls a VimL function with the given arguments. /// Calls a VimL function with the given arguments.
/// ///
/// On VimL error: Returns a generic error; v:errmsg is not updated. /// On execution error: fails with VimL error, does not update v:errmsg.
/// ///
/// @param fn Function to call /// @param fn Function to call
/// @param args Function arguments packed in an Array /// @param args Function arguments packed in an Array
@@ -369,6 +375,8 @@ Object nvim_call_function(String fn, Array args, Error *err)
/// Calls a VimL |Dictionary-function| with the given arguments. /// Calls a VimL |Dictionary-function| with the given arguments.
/// ///
/// On execution error: fails with VimL error, does not update v:errmsg.
///
/// @param dict Dictionary, or String evaluating to a VimL |self| dict /// @param dict Dictionary, or String evaluating to a VimL |self| dict
/// @param fn Name of the function defined on the VimL dict /// @param fn Name of the function defined on the VimL dict
/// @param args Function arguments packed in an Array /// @param args Function arguments packed in an Array

View File

@@ -6241,20 +6241,21 @@ bool set_ref_in_func(char_u *name, ufunc_T *fp_in, int copyID)
/// invoked function uses them. It is called like this: /// invoked function uses them. It is called like this:
/// new_argcount = argv_func(current_argcount, argv, called_func_argcount) /// new_argcount = argv_func(current_argcount, argv, called_func_argcount)
/// ///
/// Return FAIL when the function can't be called, OK otherwise. /// @return FAIL if function cannot be called, else OK (even if an error
/// Also returns OK when an error was encountered while executing the function. /// occurred while executing the function! Use `msg_first_ignored_err`
/// to get the error)
int int
call_func( call_func(
const char_u *funcname, // name of the function const char_u *funcname, // name of the function
int len, // length of "name" int len, // length of "name"
typval_T *rettv, // return value goes here typval_T *rettv, // [out] value goes here
int argcount_in, // number of "argvars" int argcount_in, // number of "argvars"
typval_T *argvars_in, // vars for arguments, must have "argcount" typval_T *argvars_in, // vars for arguments, must have "argcount"
// PLUS ONE elements! // PLUS ONE elements!
ArgvFunc argv_func, // function to fill in argvars ArgvFunc argv_func, // function to fill in argvars
linenr_T firstline, // first line of range linenr_T firstline, // first line of range
linenr_T lastline, // last line of range linenr_T lastline, // last line of range
int *doesrange, // return: function handled range int *doesrange, // [out] function handled range
bool evaluate, bool evaluate,
partial_T *partial, // optional, can be NULL partial_T *partial, // optional, can be NULL
dict_T *selfdict_in // Dictionary for "self" dict_T *selfdict_in // Dictionary for "self"
@@ -6428,22 +6429,26 @@ call_func(
return ret; return ret;
} }
/* /// Give an error message with a function name. Handle <SNR> things.
* Give an error message with a function name. Handle <SNR> things. ///
* "ermsg" is to be passed without translation, use N_() instead of _(). /// @param ermsg must be passed without translation (use N_() instead of _()).
*/ /// @param name function name
static void emsg_funcname(char *ermsg, char_u *name) static void emsg_funcname(char *ermsg, char_u *name)
{ {
char_u *p; char_u *p;
if (*name == K_SPECIAL) if (*name == K_SPECIAL) {
p = concat_str((char_u *)"<SNR>", name + 3); p = concat_str((char_u *)"<SNR>", name + 3);
else } else {
p = name; p = name;
}
EMSG2(_(ermsg), p); EMSG2(_(ermsg), p);
if (p != name)
if (p != name) {
xfree(p); xfree(p);
} }
}
/* /*
* Return TRUE for a non-zero Number and a non-empty String. * Return TRUE for a non-zero Number and a non-empty String.

View File

@@ -67,6 +67,7 @@ static char_u *confirm_msg_tail; /* tail of confirm_msg */
MessageHistoryEntry *first_msg_hist = NULL; MessageHistoryEntry *first_msg_hist = NULL;
MessageHistoryEntry *last_msg_hist = NULL; MessageHistoryEntry *last_msg_hist = NULL;
char *msg_first_ignored_err = NULL;
static int msg_hist_len = 0; static int msg_hist_len = 0;
static FILE *verbose_fd = NULL; static FILE *verbose_fd = NULL;
@@ -504,6 +505,9 @@ int emsg(const char_u *s_)
if (cause_errthrow((char_u *)s, severe, &ignore) == true) { if (cause_errthrow((char_u *)s, severe, &ignore) == true) {
if (!ignore) { if (!ignore) {
did_emsg = true; did_emsg = true;
if (msg_first_ignored_err == NULL) {
msg_first_ignored_err = xstrdup(s);
}
} }
return true; return true;
} }

View File

@@ -85,6 +85,10 @@ extern MessageHistoryEntry *first_msg_hist;
/// Last message /// Last message
extern MessageHistoryEntry *last_msg_hist; extern MessageHistoryEntry *last_msg_hist;
/// Abort-causing non-exception error ignored by emsg(), needed by callers
/// (RPC API) of call_func() to get error details when messages are disabled.
extern char *msg_first_ignored_err;
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "message.h.generated.h" # include "message.h.generated.h"
#endif #endif

View File

@@ -250,7 +250,7 @@ describe('server -> client', function()
end) end)
after_each(function() after_each(function()
funcs.jobstop(jobid) pcall(funcs.jobstop, jobid)
end) end)
if helpers.pending_win32(pending) then return end if helpers.pending_win32(pending) then return end

View File

@@ -5,6 +5,7 @@ local global_helpers = require('test.helpers')
local NIL = helpers.NIL local NIL = helpers.NIL
local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq
local command = helpers.command local command = helpers.command
local eval = helpers.eval
local funcs = helpers.funcs local funcs = helpers.funcs
local iswin = helpers.iswin local iswin = helpers.iswin
local meth_pcall = helpers.meth_pcall local meth_pcall = helpers.meth_pcall
@@ -40,20 +41,20 @@ describe('api', function()
os.remove(fname) os.remove(fname)
end) end)
it("parse error: fails (specific error), does NOT update v:errmsg", function() it('VimL validation error: fails with specific error', function()
-- Most API methods return generic errors (or no error) if a VimL
-- expression fails; nvim_command returns the VimL error details.
local status, rv = pcall(nvim, "command", "bogus_command") local status, rv = pcall(nvim, "command", "bogus_command")
eq(false, status) -- nvim_command() failed. eq(false, status) -- nvim_command() failed.
eq("E492:", string.match(rv, "E%d*:")) -- VimL error was returned. eq("E492:", string.match(rv, "E%d*:")) -- VimL error was returned.
eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. eq('', nvim('eval', 'v:errmsg')) -- v:errmsg was not updated.
eq('', eval('v:exception'))
end) end)
it("runtime error: fails (specific error)", function() it('VimL execution error: fails with specific error', function()
local status, rv = pcall(nvim, "command_output", "buffer 23487") local status, rv = pcall(nvim, "command_output", "buffer 23487")
eq(false, status) -- nvim_command() failed. eq(false, status) -- nvim_command() failed.
eq("E86: Buffer 23487 does not exist", string.match(rv, "E%d*:.*")) eq("E86: Buffer 23487 does not exist", string.match(rv, "E%d*:.*"))
eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. eq('', eval('v:errmsg')) -- v:errmsg was not updated.
eq('', eval('v:exception'))
end) end)
end) end)
@@ -109,21 +110,21 @@ describe('api', function()
eq(':!echo foo\r\n\nfoo'..win_lf..'\n', nvim('command_output', [[!echo foo]])) eq(':!echo foo\r\n\nfoo'..win_lf..'\n', nvim('command_output', [[!echo foo]]))
end) end)
it("parse error: fails (specific error), does NOT update v:errmsg", function() it('VimL validation error: fails with specific error', function()
local status, rv = pcall(nvim, "command_output", "bogus commannnd") local status, rv = pcall(nvim, "command_output", "bogus commannnd")
eq(false, status) -- nvim_command_output() failed. eq(false, status) -- nvim_command_output() failed.
eq("E492: Not an editor command: bogus commannnd", eq("E492: Not an editor command: bogus commannnd",
string.match(rv, "E%d*:.*")) string.match(rv, "E%d*:.*"))
eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. eq('', eval('v:errmsg')) -- v:errmsg was not updated.
-- Verify NO hit-enter prompt. -- Verify NO hit-enter prompt.
eq({mode='n', blocking=false}, nvim("get_mode")) eq({mode='n', blocking=false}, nvim("get_mode"))
end) end)
it("runtime error: fails (specific error)", function() it('VimL execution error: fails with specific error', function()
local status, rv = pcall(nvim, "command_output", "buffer 42") local status, rv = pcall(nvim, "command_output", "buffer 42")
eq(false, status) -- nvim_command_output() failed. eq(false, status) -- nvim_command_output() failed.
eq("E86: Buffer 42 does not exist", string.match(rv, "E%d*:.*")) eq("E86: Buffer 42 does not exist", string.match(rv, "E%d*:.*"))
eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. eq('', eval('v:errmsg')) -- v:errmsg was not updated.
-- Verify NO hit-enter prompt. -- Verify NO hit-enter prompt.
eq({mode='n', blocking=false}, nvim("get_mode")) eq({mode='n', blocking=false}, nvim("get_mode"))
end) end)
@@ -149,7 +150,7 @@ describe('api', function()
local status, rv = pcall(nvim, "eval", "bogus expression") local status, rv = pcall(nvim, "eval", "bogus expression")
eq(false, status) -- nvim_eval() failed. eq(false, status) -- nvim_eval() failed.
ok(nil ~= string.find(rv, "Failed to evaluate expression")) ok(nil ~= string.find(rv, "Failed to evaluate expression"))
eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. eq('', eval('v:errmsg')) -- v:errmsg was not updated.
end) end)
end) end)
@@ -160,12 +161,39 @@ describe('api', function()
eq(17, nvim('call_function', 'eval', {17})) eq(17, nvim('call_function', 'eval', {17}))
eq('foo', nvim('call_function', 'simplify', {'this/./is//redundant/../../../foo'})) eq('foo', nvim('call_function', 'simplify', {'this/./is//redundant/../../../foo'}))
end) end)
it("VimL error: fails (generic error), does NOT update v:errmsg", function()
local status, rv = pcall(nvim, "call_function", "bogus function", {"arg1"}) it("VimL validation error: returns specific error, does NOT update v:errmsg", function()
eq(false, status) -- nvim_call_function() failed. expect_err('E117: Unknown function: bogus function', request,
ok(nil ~= string.find(rv, "Error calling function")) 'nvim_call_function', 'bogus function', {'arg1'})
eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. expect_err('E119: Not enough arguments for function: atan', request,
'nvim_call_function', 'atan', {})
eq('', eval('v:exception'))
eq('', eval('v:errmsg')) -- v:errmsg was not updated.
end) end)
it("VimL error: returns error details, does NOT update v:errmsg", function()
expect_err('E808: Number or Float required', request,
'nvim_call_function', 'atan', {'foo'})
expect_err('Invalid channel stream "xxx"', request,
'nvim_call_function', 'chanclose', {999, 'xxx'})
expect_err('E900: Invalid channel id', request,
'nvim_call_function', 'chansend', {999, 'foo'})
eq('', eval('v:exception'))
eq('', eval('v:errmsg')) -- v:errmsg was not updated.
end)
it("VimL exception: returns exception details, does NOT update v:errmsg", function()
source([[
function! Foo() abort
throw 'wtf'
endfunction
]])
expect_err('wtf', request,
'nvim_call_function', 'Foo', {})
eq('', eval('v:exception'))
eq('', eval('v:errmsg')) -- v:errmsg was not updated.
end)
it('validates args', function() it('validates args', function()
local too_many_args = { 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x' } local too_many_args = { 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x' }
source([[ source([[
@@ -216,7 +244,7 @@ describe('api', function()
'nvim_call_dict_function', 'g:d', 'baz', {1,2}) 'nvim_call_dict_function', 'g:d', 'baz', {1,2})
expect_err('Not a function: meep', request, expect_err('Not a function: meep', request,
'nvim_call_dict_function', 'g:d', 'meep', {1,2}) 'nvim_call_dict_function', 'g:d', 'meep', {1,2})
expect_err('Error calling function', request, expect_err('E117: Unknown function: f', request,
'nvim_call_dict_function', { f = '' }, 'f', {1,2}) 'nvim_call_dict_function', { f = '' }, 'f', {1,2})
expect_err('Not a function: f', request, expect_err('Not a function: f', request,
'nvim_call_dict_function', "{ 'f': '' }", 'f', {1,2}) 'nvim_call_dict_function', "{ 'f': '' }", 'f', {1,2})

View File

@@ -6,6 +6,7 @@ local clear = helpers.clear
local funcs = helpers.funcs local funcs = helpers.funcs
local command = helpers.command local command = helpers.command
local exc_exec = helpers.exc_exec local exc_exec = helpers.exc_exec
local expect_err = helpers.expect_err
before_each(clear) before_each(clear)
@@ -40,9 +41,11 @@ describe('setmatches()', function()
end) end)
it('fails with -1 if highlight group is not defined', function() it('fails with -1 if highlight group is not defined', function()
eq(-1, funcs.setmatches({{group=1, pattern=2, id=3, priority=4}})) expect_err('E28: No such highlight group name: 1', funcs.setmatches,
{{group=1, pattern=2, id=3, priority=4}})
eq({}, funcs.getmatches()) eq({}, funcs.getmatches())
eq(-1, funcs.setmatches({{group=1, pos1={2}, pos2={6}, id=3, priority=4, conceal=5}})) expect_err('E28: No such highlight group name: 1', funcs.setmatches,
{{group=1, pos1={2}, pos2={6}, id=3, priority=4, conceal=5}})
eq({}, funcs.getmatches()) eq({}, funcs.getmatches())
end) end)
end) end)

View File

@@ -5,6 +5,7 @@ local clear, funcs, meths = helpers.clear, helpers.funcs, helpers.meths
local iswin = helpers.iswin local iswin = helpers.iswin
local ok = helpers.ok local ok = helpers.ok
local matches = helpers.matches local matches = helpers.matches
local expect_err = helpers.expect_err
local function clear_serverlist() local function clear_serverlist()
for _, server in pairs(funcs.serverlist()) do for _, server in pairs(funcs.serverlist()) do
@@ -89,19 +90,20 @@ describe('server', function()
s = funcs.serverstart(v4) s = funcs.serverstart(v4)
if #s > 0 then if #s > 0 then
table.insert(expected, v4) table.insert(expected, v4)
funcs.serverstart(v4) -- exists already; ignore pcall(funcs.serverstart, v4) -- exists already; ignore
end end
local v6 = '::1:12345' local v6 = '::1:12345'
s = funcs.serverstart(v6) s = funcs.serverstart(v6)
if #s > 0 then if #s > 0 then
table.insert(expected, v6) table.insert(expected, v6)
funcs.serverstart(v6) -- exists already; ignore pcall(funcs.serverstart, v6) -- exists already; ignore
end end
eq(expected, funcs.serverlist()) eq(expected, funcs.serverlist())
clear_serverlist() clear_serverlist()
funcs.serverstart('127.0.0.1:65536') -- invalid port expect_err('Failed to start server: invalid argument',
funcs.serverstart, '127.0.0.1:65536') -- invalid port
eq({}, funcs.serverlist()) eq({}, funcs.serverlist())
end) end)

View File

@@ -15,7 +15,7 @@ describe('Test getting and setting file permissions', function()
it('file permissions', function() it('file permissions', function()
eq('', call('getfperm', tempfile)) eq('', call('getfperm', tempfile))
eq(0, call('setfperm', tempfile, 'r------')) eq(0, call('setfperm', tempfile, 'r--------'))
call('writefile', {'one'}, tempfile) call('writefile', {'one'}, tempfile)
eq(9, call('len', call('getfperm', tempfile))) eq(9, call('len', call('getfperm', tempfile)))

View File

@@ -51,12 +51,12 @@ describe('luaeval()', function()
end) end)
describe('recursive lua values', function() describe('recursive lua values', function()
it('are successfully transformed', function() it('are successfully transformed', function()
funcs.luaeval('rawset(_G, "d", {})') command('lua rawset(_G, "d", {})')
funcs.luaeval('rawset(d, "d", d)') command('lua rawset(d, "d", d)')
eq('\n{\'d\': {...@0}}', funcs.execute('echo luaeval("d")')) eq('\n{\'d\': {...@0}}', funcs.execute('echo luaeval("d")'))
funcs.luaeval('rawset(_G, "l", {})') command('lua rawset(_G, "l", {})')
funcs.luaeval('table.insert(l, l)') command('lua table.insert(l, l)')
eq('\n[[...@0]]', funcs.execute('echo luaeval("l")')) eq('\n[[...@0]]', funcs.execute('echo luaeval("l")'))
end) end)
end) end)