aboutsummaryrefslogtreecommitdiffstats
path: root/tests/indent/common.lua
blob: 6add58c7f6492572652a20c9952be1cf525f2b5c (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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
local M = {}

local assert = require('luassert')
local say = require('say')
local scan_dir = require('plenary.scandir').scan_dir
local Path = require('plenary.path')

M.XFAIL = 'xfail'

local function same_indent(state, arguments)
  local before = arguments[1]
  local after = arguments[2]

  local ok = true
  local errors = { before = {}, after = {} }
  for line = 1, #before do
    if #string.match(before[line], '^%s*') ~= #string.match(after[line], '^%s*') then
      if before[line] and after[line] then
        -- store the actual indentation length for each line
        errors.before[line] = #string.match(before[line], '^%s*')
        errors.after[line] = #string.match(after[line], '^%s*')
      end
      ok = false
    end
  end

  -- we will always use only a single argument, passing the other one in fmtargs
  arguments.fmtargs = { { errors = errors, other = after } }
  arguments.fmtargs[2] = { errors = errors, other = after }

  return ok
end

local function format_indent(arg, fmtargs)
  if not arg or not fmtargs then
    return
  end
  -- find minimal width if any line is longer
  local width = 40
  for _, line in ipairs(fmtargs.other) do
    width = #line > width and #line or width
  end

  width = width + 3
  local header_fmt = '%8s %2s%-' .. tostring(width + 1) .. 's %s'
  local fmt = '%8s %2s |%-' .. tostring(width) .. 's |%s'

  local output = { header_fmt:format('', '', 'Found:', 'Expected:') }

  for i, line in ipairs(arg) do
    if fmtargs.errors.before[i] then
      local indents = string.format('%d vs %d', fmtargs.errors.after[i], fmtargs.errors.before[i])
      table.insert(output, fmt:format(indents, '=>', fmtargs.other[i], line))
    else
      table.insert(output, fmt:format('', '', fmtargs.other[i], line))
    end
  end

  return table.concat(output, '\n')
end

say:set_namespace('en')
say:set('assertion.same_indent.positive', 'Incorrect indentation\n%s')
say:set('assertion.same_indent.negative', 'Incorrect indentation\n%s')
assert:register(
  'assertion',
  'same_indent',
  same_indent,
  'assertion.same_indent.positive',
  'assert.same_indent.negative'
)

-- Custom assertion better suited for indentation diffs
local function compare_indent(before, after, xfail)
  assert:add_formatter(format_indent)
  if xfail then
    -- io.stdout:write('Warning! Known failure of this test! Please help to fix it! ')
    assert.is_not.same_indent(before, after)
  else
    assert.is.same_indent(before, after)
  end
  assert:remove_formatter(format_indent)
end

local function set_buf_indent_opts(opts)
  local optnames =
    { 'tabstop', 'shiftwidth', 'softtabstop', 'expandtab', 'filetype', 'lispoptions' }
  for _, opt in ipairs(optnames) do
    if opts[opt] ~= nil then
      vim.bo[opt] = opts[opt]
    end
  end
end

function M.run_indent_test(file, runner, opts)
  assert.are.same(1, vim.fn.filereadable(file), string.format('File "%s" not readable', file))

  -- load reference file
  vim.cmd(string.format('edit %s', file))
  vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
  local before = vim.api.nvim_buf_get_lines(0, 0, -1, true)

  assert.are.same("v:lua.require'nvim-treesitter'.indentexpr()", vim.bo.indentexpr)
  set_buf_indent_opts(opts)

  -- perform the test
  runner()

  -- get file content after the test
  local after = vim.api.nvim_buf_get_lines(0, 0, -1, true)

  -- clear any changes to avoid 'No write since last change (add ! to override)'
  vim.cmd('edit!')

  return before, after
end

function M.indent_whole_file(file, opts, xfail)
  local before, after = M.run_indent_test(file, function()
    vim.cmd('silent normal gg=G')
  end, opts)

  compare_indent(before, after, xfail)
end

-- Open a file, use `normal o` to insert a new line and compare results
-- @param file path to the initial file
-- @param spec a table with keys:
--   on_line: line on which `normal o` is executed
--   text: text inserted in the new line
--   indent: expected indent before the inserted text (string or int)
-- @param opts buffer options passed to set_buf_indent_opts
function M.indent_new_line(file, spec, opts, xfail)
  local before, after = M.run_indent_test(file, function()
    -- move to the line and input the new one
    vim.cmd(string.format('normal! %dG', spec.on_line))
    vim.cmd(string.format('normal! o%s', spec.text))
  end, opts)

  local indent = type(spec.indent) == 'string' and spec.indent or string.rep(' ', spec.indent)
  table.insert(before, spec.on_line + 1, indent .. spec.text)

  compare_indent(before, after, xfail)

  before, after = M.run_indent_test(file, function()
    -- move to the line and input the new one
    vim.cmd(string.format('normal! %dG$', spec.on_line))
    vim.cmd(
      string.format(vim.api.nvim_replace_termcodes('normal! a<cr>%s', true, true, true), spec.text)
    )
  end, opts)

  indent = type(spec.indent) == 'string' and spec.indent or string.rep(' ', spec.indent)
  table.insert(before, spec.on_line + 1, indent .. spec.text)

  compare_indent(before, after, xfail)
end

local Runner = {}
Runner.__index = Runner

-- Helper to avoid boilerplate when defining tests
-- @param it  the "it" function that busted defines globally in spec files
-- @param base_dir  all other paths will be resolved relative to this directory
-- @param buf_opts  buffer options passed to set_buf_indent_opts
function Runner:new(it, base_dir, buf_opts)
  local runner = {}
  runner.it = it
  runner.base_dir = Path:new(base_dir)
  runner.buf_opts = buf_opts
  return setmetatable(runner, self)
end

function Runner:whole_file(dirs, opts)
  opts = opts or {}
  local expected_failures = opts.expected_failures or {}
  expected_failures = vim.tbl_map(function(f)
    return Path:new(f):make_relative(self.base_dir.filename)
  end, expected_failures)
  dirs = type(dirs) == 'table' and dirs or { dirs }
  dirs = vim.tbl_map(function(dir)
    dir = self.base_dir / Path:new(dir)
    assert.is.same(1, vim.fn.isdirectory(dir.filename))
    return dir.filename
  end, dirs)
  local files = vim.iter(vim.tbl_map(scan_dir, dirs)):flatten():totable()
  for _, file in ipairs(files) do
    local relpath = Path:new(file):make_relative(self.base_dir.filename)
    self.it(relpath, function()
      M.indent_whole_file(file, self.buf_opts, vim.tbl_contains(expected_failures, relpath))
    end)
  end
end

function Runner:new_line(file, spec, title, xfail)
  title = title and title or tostring(spec.on_line)
  self.it(string.format('%s[%s]', file, title), function()
    local path = self.base_dir / file
    M.indent_new_line(path.filename, spec, self.buf_opts, xfail)
  end)
end

M.Runner = Runner

return M