neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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)