version_spec.lua (9531B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 4 local clear, fn, eq = n.clear, n.fn, t.eq 5 local api = n.api 6 7 local function read_mpack_file(fname) 8 local fd = io.open(fname, 'rb') 9 if fd == nil then 10 return nil 11 end 12 13 local data = fd:read('*a') 14 fd:close() 15 local unpack = vim.mpack.Unpacker() 16 return unpack(data) 17 end 18 19 describe("api_info()['version']", function() 20 before_each(clear) 21 22 it('returns API level', function() 23 local version = fn.api_info()['version'] 24 local current = version['api_level'] 25 local compat = version['api_compatible'] 26 eq('number', type(current)) 27 eq('number', type(compat)) 28 assert(current >= compat) 29 end) 30 31 it('returns Nvim version', function() 32 local version = fn.api_info()['version'] 33 local major = version['major'] 34 local minor = version['minor'] 35 local patch = version['patch'] 36 local prerelease = version['prerelease'] 37 local build = version['build'] 38 eq('number', type(major)) 39 eq('number', type(minor)) 40 eq('number', type(patch)) 41 eq('boolean', type(prerelease)) 42 eq(1, fn.has('nvim-' .. major .. '.' .. minor .. '.' .. patch)) 43 eq(0, fn.has('nvim-' .. major .. '.' .. minor .. '.' .. (patch + 1))) 44 eq(0, fn.has('nvim-' .. major .. '.' .. (minor + 1) .. '.' .. patch)) 45 eq(0, fn.has('nvim-' .. (major + 1) .. '.' .. minor .. '.' .. patch)) 46 assert(build == vim.NIL or type(build) == 'string') 47 end) 48 end) 49 50 describe('api metadata', function() 51 local function name_table(entries) 52 local by_name = {} 53 for _, e in ipairs(entries) do 54 by_name[e.name] = e 55 end 56 return by_name 57 end 58 59 --- Remove or patch metadata that is not essential to backwards-compatibility. 60 --- @param f gen_api_dispatch.Function.Exported 61 local function normalize_func_metadata(f) 62 -- Dictionary was renamed to Dict. That doesn't break back-compat because clients don't actually 63 -- use the `return_type` field (evidence: "ArrayOf(…)" didn't break clients). 64 f.return_type = f.return_type:gsub('Dictionary', 'Dict') 65 f.return_type = f.return_type:gsub('^ArrayOf%(.*', 'Array') 66 67 f.deprecated_since = nil 68 for idx, _ in ipairs(f.parameters) do 69 -- Dictionary was renamed to Dict. Doesn't break back-compat because clients don't actually 70 -- use the `parameters` field of API metadata (evidence: "ArrayOf(…)" didn't break clients). 71 f.parameters[idx][1] = f.parameters[idx][1]:gsub('Dictionary', 'Dict') 72 f.parameters[idx][1] = f.parameters[idx][1]:gsub('ArrayOf%(.*', 'Array') 73 74 f.parameters[idx][2] = '' -- Remove parameter name. 75 end 76 77 if string.sub(f.name, 1, 4) ~= 'nvim' then 78 f.method = nil 79 end 80 return f 81 end 82 83 --- Checks that the current signature of a function is backwards-compatible with the previous 84 --- version, per ":help api-contract". 85 --- @param old_fn gen_api_dispatch.Function.Exported 86 --- @param new_fn gen_api_dispatch.Function.Exported 87 local function assert_func_backcompat(old_fn, new_fn) 88 old_fn = normalize_func_metadata(old_fn) 89 new_fn = normalize_func_metadata(new_fn) 90 if old_fn.return_type == 'void' then 91 old_fn.return_type = new_fn.return_type 92 end 93 eq(old_fn, new_fn) 94 end 95 96 local function check_ui_event_compatible(old_e, new_e) 97 -- check types of existing params are the same 98 -- adding parameters is ok, but removing params is not (gives nil error) 99 eq(old_e.since, new_e.since, old_e.name) 100 for i, p in ipairs(old_e.parameters) do 101 eq(new_e.parameters[i][1], p[1], old_e.name) 102 end 103 end 104 105 --- Level 0 represents methods from 0.1.5 and earlier, when 'since' was not 106 --- yet defined, and metadata was not filtered of internal keys like 'async'. 107 --- 108 --- @param metadata { functions: gen_api_dispatch.Function[] } 109 local function clean_level_0(metadata) 110 for _, f in ipairs(metadata.functions) do 111 f.can_fail = nil 112 f.async = nil -- XXX: renamed to "fast". 113 f.receives_channel_id = nil 114 f.since = 0 115 end 116 end 117 118 local api_info --[[@type table]] 119 local compat --[[@type integer]] 120 local stable --[[@type integer]] 121 local api_level --[[@type integer]] 122 local old_api = {} ---@type { functions: gen_api_dispatch.Function[] }[] 123 setup(function() 124 clear() -- Ensure a session before requesting api_info. 125 --[[@type { functions: gen_api_dispatch.Function[], version: {api_compatible: integer, api_level: integer, api_prerelease: boolean} }]] 126 api_info = api.nvim_get_api_info()[2] 127 compat = api_info.version.api_compatible 128 api_level = api_info.version.api_level 129 stable = api_info.version.api_prerelease and api_level - 1 or api_level 130 131 for level = compat, stable do 132 local path = ('test/functional/fixtures/api_level_' .. tostring(level) .. '.mpack') 133 old_api[level] = read_mpack_file(path) 134 if old_api[level] == nil then 135 local errstr = 'missing metadata fixture for stable level ' .. level .. '. ' 136 if level == api_level and not api_info.version.api_prerelease then 137 errstr = ( 138 errstr 139 .. 'If NVIM_API_CURRENT was bumped, ' 140 .. "don't forget to set NVIM_API_PRERELEASE to true." 141 ) 142 end 143 error(errstr) 144 end 145 146 if level == 0 then 147 clean_level_0(old_api[level]) 148 end 149 end 150 -- No Nvim session will be used in the following tests. 151 n.check_close() 152 end) 153 154 it('functions are compatible with old metadata or have new level', function() 155 local funcs_new = name_table(api_info.functions) 156 local funcs_compat = {} 157 for level = compat, stable do 158 for _, f in ipairs(old_api[level].functions) do 159 if funcs_new[f.name] == nil then 160 if f.since >= compat then 161 local msg = 162 'function "%s" was removed but exists in level %s which Nvim claims to be compatible with' 163 error((msg):format(f.name, f.since)) 164 end 165 else 166 assert_func_backcompat(f --[[@as any]], funcs_new[f.name]) 167 end 168 end 169 funcs_compat[level] = name_table(old_api[level].functions) 170 end 171 172 for _, f in ipairs(api_info.functions) do 173 if f.since <= stable then 174 local f_old = funcs_compat[f.since][f.name] 175 if f_old == nil then 176 if string.sub(f.name, 1, 4) == 'nvim' then 177 local errstr = ('function "%s" has too low `since` value. For new functions set it to "%s".'):format( 178 f.name, 179 (stable + 1) 180 ) 181 if not api_info.version.api_prerelease then 182 errstr = ( 183 errstr 184 .. ' Also bump NVIM_API_CURRENT and set ' 185 .. 'NVIM_API_PRERELEASE to true in CMakeLists.txt.' 186 ) 187 end 188 error(errstr) 189 else 190 error("function name '" .. f.name .. "' doesn't begin with 'nvim_'") 191 end 192 end 193 elseif f.since > api_level then 194 if api_info.version.api_prerelease then 195 error('New function ' .. f.name .. ' should use since value ' .. api_level) 196 else 197 error( 198 'function ' 199 .. f.name 200 .. ' has since value > api_level. ' 201 .. 'Bump NVIM_API_CURRENT and set ' 202 .. 'NVIM_API_PRERELEASE to true in CMakeLists.txt.' 203 ) 204 end 205 end 206 end 207 end) 208 209 it('UI events are compatible with old metadata or have new level', function() 210 local ui_events_new = name_table(api_info.ui_events) 211 local ui_events_compat = {} 212 213 -- UI events were formalized in level 3 214 for level = 3, stable do 215 for _, e in ipairs(old_api[level].ui_events) do 216 local new_e = ui_events_new[e.name] 217 if new_e ~= nil then 218 check_ui_event_compatible(e, new_e) 219 end 220 end 221 ui_events_compat[level] = name_table(old_api[level].ui_events) 222 end 223 224 for _, e in ipairs(api_info.ui_events) do 225 if e.since <= stable then 226 local e_old = ui_events_compat[e.since][e.name] 227 if e_old == nil then 228 local errstr = ( 229 'UI event ' 230 .. e.name 231 .. ' has too low since value. ' 232 .. 'For new events set it to ' 233 .. (stable + 1) 234 .. '.' 235 ) 236 if not api_info.version.api_prerelease then 237 errstr = ( 238 errstr 239 .. ' Also bump NVIM_API_CURRENT and set ' 240 .. 'NVIM_API_PRERELEASE to true in CMakeLists.txt.' 241 ) 242 end 243 error(errstr) 244 end 245 elseif e.since > api_level then 246 if api_info.version.api_prerelease then 247 error('New UI event ' .. e.name .. ' should use since value ' .. api_level) 248 else 249 error( 250 'UI event ' 251 .. e.name 252 .. ' has since value > api_level. ' 253 .. 'Bump NVIM_API_CURRENT and set ' 254 .. 'NVIM_API_PRERELEASE to true in CMakeLists.txt.' 255 ) 256 end 257 end 258 end 259 end) 260 261 it('ui_options are preserved from older levels', function() 262 local available_options = {} 263 for _, option in ipairs(api_info.ui_options) do 264 available_options[option] = true 265 end 266 -- UI options were versioned from level 4 267 for level = 4, stable do 268 for _, option in ipairs(old_api[level].ui_options) do 269 if not available_options[option] then 270 error('UI option ' .. option .. ' from stable metadata is missing') 271 end 272 end 273 end 274 end) 275 end)