aboutsummaryrefslogtreecommitdiff
path: root/test/assert.lua
blob: 8b26a7c327d28ed8fcb8424b33fd1b3955a5b4fb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
--- @class test.assert
local M = {}

local FORMAT_DEPTH = 100

--- @param value any
--- @return string
local function fmt(value)
  if type(value) == 'string' then
    return string.format('%q', value)
  end

  local ok, inspected = pcall(vim.inspect, value, { depth = FORMAT_DEPTH })
  if ok then
    return inspected
  end

  return tostring(value)
end

--- @param condition boolean
--- @param value any
--- @param context any
--- @param message string
local function assert_value(condition, value, context, message)
  if not condition then
    error((context ~= nil and tostring(context) .. ': ' or '') .. message, 0)
  end
  return value
end

--- @param expected any
--- @param actual any
--- @param comparator string
--- @return string
local function comparison_message(expected, actual, comparator)
  return ('Expected values to be %s.\nExpected:\n%s\nActual:\n%s'):format(
    comparator,
    fmt(expected),
    fmt(actual)
  )
end

--- @param expected any
--- @param actual any
--- @param context? any
--- @return any
function M.eq(expected, actual, context)
  return assert_value(
    vim.deep_equal(expected, actual),
    actual,
    context,
    comparison_message(expected, actual, 'equal')
  )
end

--- Checks the supplied fields recursively, ignoring other record fields.
--- Lists must have the same length and order; their records may have extra fields.
--- Does not check the non-existence of a field.
---
--- Examples:
--- ```lua
--- local result = { id = 1, opts = { enabled = false, priority = 10 } }
--- eq_partial({ opts = { enabled = false } }, result) -- Passes.
--- eq(nil, result.missing) -- Check that a field is absent.
---
--- eq_partial({ { id = 1 } }, { { id = 1, name = 'test' } }) -- Passes.
--- eq_partial({ { id = 1 } }, { { id = 1 }, { id = 2 } }) -- Fails: extra list item.
--- ```
---
--- @param expected any
--- @param actual any
--- @param context? any
--- @return any
function M.eq_partial(expected, actual, context)
  local seen = {} --- @type table<table, table<table, boolean>>
  local prefix = context ~= nil and tostring(context) .. ': ' or ''

  --- @param want any
  --- @param got any
  --- @param path string
  local function compare(want, got, path)
    if type(want) ~= 'table' or type(got) ~= 'table' or next(want) == nil then
      M.eq(want, got, prefix .. path)
      return
    end

    if vim.islist(want) then
      assert_value(vim.islist(got), got, prefix .. path, 'Expected a list.\nActual:\n' .. fmt(got))
      M.eq(#want, #got, prefix .. path .. ' (length)')
    end

    if seen[want] and seen[want][got] then
      return
    end
    seen[want] = seen[want] or {}
    seen[want][got] = true

    for key, value in pairs(want) do
      local field = type(key) == 'string' and key:match('^[%a_][%w_]*$') and '.' .. key
        or '[' .. fmt(key) .. ']'
      compare(value, got[key], path .. field)
    end
  end

  compare(expected, actual, 'actual')
  return actual
end

--- @param expected any
--- @param actual any
--- @param context? any
--- @return any
function M.neq(expected, actual, context)
  return assert_value(
    not vim.deep_equal(expected, actual),
    actual,
    context,
    ('Expected values to differ.\nValue:\n%s'):format(fmt(actual))
  )
end

return setmetatable(M, {
  --- @param condition any
  --- @param message? string
  --- @param level? integer
  __call = function(_, condition, message, level, ...)
    if condition then
      return condition, message, level, ...
    end

    error(message or 'assertion failed!', (type(level) == 'number' and level or 1) + 1)
  end,
})