check-expect.lua revision 1.17 1 #! /usr/bin/lua
2 -- $NetBSD: check-expect.lua,v 1.17 2025/07/01 05:03:18 rillig Exp $
3
4 --[[
5
6 usage: lua ./check-expect.lua *.mk
7
8 Check that the various 'expect' comments in the .mk files produce the
9 expected 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
35 local had_errors = false
36 ---@param fmt string
37 local function print_error(fmt, ...)
38 print(fmt:format(...))
39 had_errors = true
40 end
41
42
43 ---@return nil | string[]
44 local 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
58 end
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[]
69 local 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
85 end
86
87 ---@param exp_lines ExpLine[]
88 local 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
96 end
97
98 local 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)
184 end
185
186 for _, fname in ipairs(arg) do
187 check_mk(fname)
188 end
189 os.exit(not had_errors)
190