xref: /freebsd/contrib/bmake/unit-tests/check-expect.lua (revision a8c56be47166295d37600ff81fc1857db87b3a9b)
1#!  /usr/bin/lua
2-- $NetBSD: check-expect.lua,v 1.17 2025/07/01 05:03:18 rillig Exp $
3
4--[[
5
6usage: lua ./check-expect.lua *.mk
7
8Check that the various 'expect' comments in the .mk files produce the
9expected text in the corresponding .exp file.
10
11# expect: <line>
12        Each <line> must occur in the .exp file.
13        The order in the .mk file must be the same as in the .exp file.
14
15# expect[+-]offset: <message>
16        Each <message> must occur in the .exp file and refer back to the
17        source line in the .mk file.
18        Each such line in the .exp file must have a corresponding expect line
19        in the .mk file.
20        The order in the .mk file must be the same as in the .exp file.
21
22# expect-reset
23        Search the following "expect:" and "expect[+-]offset:" comments
24        from the top of the .exp file again.
25
26# expect-not: <substring>
27        The <substring> must not occur as part of any line in the .exp file.
28
29# expect-not-matches: <pattern>
30        The <pattern> (see https://lua.org/manual/5.4/manual.html#6.4.1)
31        must not occur as part of any line in the .exp file.
32]]
33
34
35local had_errors = false
36---@param fmt string
37local function print_error(fmt, ...)
38  print(fmt:format(...))
39  had_errors = true
40end
41
42
43---@return nil | string[]
44local function load_lines(fname)
45  local lines = {}
46
47  local f = io.open(fname, "r")
48  if f == nil then
49    return nil
50  end
51
52  for line in f:lines() do
53    table.insert(lines, line)
54  end
55  f:close()
56
57  return lines
58end
59
60
61--- @shape ExpLine
62--- @field filename string | nil
63--- @field lineno number | nil
64--- @field text string
65
66
67--- @param lines string[]
68--- @return ExpLine[]
69local function parse_exp(lines)
70  local exp_lines = {}
71  for _, line in ipairs(lines) do
72    local l_filename, l_lineno, l_text =
73      line:match('^make: ([^:]+%.mk):(%d+):%s+(.*)')
74    if not l_filename then
75      l_text = line
76    end
77    l_text = l_text:gsub("^%s+", ""):gsub("%s+$", "")
78    table.insert(exp_lines, {
79      filename = l_filename,
80      lineno = tonumber(l_lineno),
81      text = l_text,
82    })
83  end
84  return exp_lines
85end
86
87---@param exp_lines ExpLine[]
88local function detect_missing_expect_lines(exp_fname, exp_lines, s, e)
89  for i = s, e do
90    local exp_line = exp_lines[i]
91    if exp_line.filename then
92      print_error("error: %s:%d requires in %s:%d: # expect+1: %s",
93        exp_fname, i, exp_line.filename, exp_line.lineno, exp_line.text)
94    end
95  end
96end
97
98local function check_mk(mk_fname)
99  local exp_fname = mk_fname:gsub("%.mk$", ".exp")
100  local mk_lines = load_lines(mk_fname)
101  local exp_raw_lines = load_lines(exp_fname)
102  if exp_raw_lines == nil then
103    return
104  end
105  local exp_lines = parse_exp(exp_raw_lines)
106
107  local exp_it = 1
108
109  for mk_lineno, mk_line in ipairs(mk_lines) do
110
111    local function match(pattern, action)
112      local _, n = mk_line:gsub(pattern, action)
113      if n > 0 then
114        match = function() end
115      end
116    end
117
118    match("^#%s+expect%-not:%s*(.*)", function(text)
119      for exp_lineno, exp_line in ipairs(exp_lines) do
120        if exp_line.text:find(text, 1, true) then
121          print_error("error: %s:%d: %s:%d must not contain '%s'",
122            mk_fname, mk_lineno, exp_fname, exp_lineno, text)
123        end
124      end
125    end)
126
127    match("^#%s+expect%-not%-matches:%s*(.*)", function(pattern)
128      for exp_lineno, exp_line in ipairs(exp_lines) do
129        if exp_line.text:find(pattern) then
130          print_error("error: %s:%d: %s:%d must not match '%s'",
131            mk_fname, mk_lineno, exp_fname, exp_lineno, pattern)
132        end
133      end
134    end)
135
136    match("^#%s+expect:%s*(.*)", function(text)
137      local i = exp_it
138      while i <= #exp_lines and text ~= exp_lines[i].text do
139        i = i + 1
140      end
141      if i <= #exp_lines then
142        detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
143        exp_lines[i].text = ""
144        exp_it = i + 1
145      else
146        print_error("error: %s:%d: '%s:%d+' must contain '%s'",
147          mk_fname, mk_lineno, exp_fname, exp_it, text)
148      end
149    end)
150
151    match("^#%s+expect%-reset$", function()
152      exp_it = 1
153    end)
154
155    match("^#%s+expect([+%-]%d+):%s*(.*)", function(offset, text)
156      local msg_lineno = mk_lineno + tonumber(offset)
157
158      local i = exp_it
159      while i <= #exp_lines and text ~= exp_lines[i].text do
160        i = i + 1
161      end
162
163      if i <= #exp_lines and exp_lines[i].lineno == msg_lineno then
164        detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
165        exp_lines[i].text = ""
166        exp_it = i + 1
167      elseif i <= #exp_lines then
168        print_error("error: %s:%d: expect%+d must be expect%+d",
169          mk_fname, mk_lineno, tonumber(offset),
170          exp_lines[i].lineno - mk_lineno)
171      else
172        print_error("error: %s:%d: %s:%d+ must contain '%s'",
173          mk_fname, mk_lineno, exp_fname, exp_it, text)
174      end
175    end)
176
177    match("^#%s+expect[+%-:]", function()
178      print_error("error: %s:%d: invalid \"expect\" line: %s",
179        mk_fname, mk_lineno, mk_line)
180    end)
181
182  end
183  detect_missing_expect_lines(exp_fname, exp_lines, exp_it, #exp_lines)
184end
185
186for _, fname in ipairs(arg) do
187  check_mk(fname)
188end
189os.exit(not had_errors)
190