check-expect.lua revision 1.14 1 1.14 rillig #! /usr/bin/lua
2 1.14 rillig -- $NetBSD: check-expect.lua,v 1.14 2025/02/27 06:48:29 rillig Exp $
3 1.1 rillig
4 1.1 rillig --[[
5 1.1 rillig
6 1.9 rillig usage: lua ./check-expect.lua [-u] *.c
7 1.1 rillig
8 1.1 rillig Check that the /* expect+-n: ... */ comments in the .c source files match the
9 1.2 rillig actual messages found in the corresponding .exp files. The .exp files are
10 1.1 rillig expected in the current working directory.
11 1.1 rillig
12 1.1 rillig The .exp files are generated on the fly during the ATF tests, see
13 1.1 rillig t_integration.sh. During development, they can be generated using
14 1.1 rillig lint1/accept.sh.
15 1.1 rillig ]]
16 1.1 rillig
17 1.1 rillig
18 1.1 rillig local function test(func)
19 1.1 rillig func()
20 1.1 rillig end
21 1.1 rillig
22 1.1 rillig local function assert_equals(got, expected)
23 1.1 rillig if got ~= expected then
24 1.1 rillig assert(false, string.format("got %q, expected %q", got, expected))
25 1.1 rillig end
26 1.1 rillig end
27 1.1 rillig
28 1.1 rillig
29 1.1 rillig local had_errors = false
30 1.1 rillig ---@param fmt string
31 1.1 rillig function print_error(fmt, ...)
32 1.1 rillig print(fmt:format(...))
33 1.1 rillig had_errors = true
34 1.1 rillig end
35 1.1 rillig
36 1.1 rillig
37 1.1 rillig local function load_lines(fname)
38 1.1 rillig local lines = {}
39 1.1 rillig
40 1.7 rillig local f, err, errno = io.open(fname, "r")
41 1.7 rillig if f == nil then return nil, err, errno end
42 1.1 rillig
43 1.1 rillig for line in f:lines() do
44 1.1 rillig table.insert(lines, line)
45 1.1 rillig end
46 1.1 rillig f:close()
47 1.1 rillig
48 1.1 rillig return lines
49 1.1 rillig end
50 1.1 rillig
51 1.1 rillig
52 1.6 rillig local function save_lines(fname, lines)
53 1.6 rillig local f = io.open(fname, "w")
54 1.6 rillig for _, line in ipairs(lines) do
55 1.6 rillig f:write(line .. "\n")
56 1.6 rillig end
57 1.6 rillig f:close()
58 1.6 rillig end
59 1.6 rillig
60 1.6 rillig
61 1.1 rillig -- Load the 'expect:' comments from a C source file.
62 1.1 rillig --
63 1.1 rillig -- example return values:
64 1.1 rillig -- {
65 1.8 rillig -- ["file.c(18)"] = {"syntax error 'a' [249]", "syntax error 'b' [249]"},
66 1.1 rillig -- ["file.c(23)"] = {"not a constant expression [123]"},
67 1.1 rillig -- },
68 1.1 rillig -- { "file.c(18)", "file.c(23)" }
69 1.1 rillig local function load_c(fname)
70 1.4 rillig local basename = fname:match("([^/]+)$")
71 1.1 rillig local lines = load_lines(fname)
72 1.1 rillig if lines == nil then return nil, nil end
73 1.1 rillig
74 1.1 rillig local pp_fname = fname
75 1.1 rillig local pp_lineno = 0
76 1.1 rillig local comment_locations = {}
77 1.1 rillig local comments_by_location = {}
78 1.1 rillig
79 1.1 rillig local function add_expectation(offset, message)
80 1.1 rillig local location = ("%s(%d)"):format(pp_fname, pp_lineno + offset)
81 1.1 rillig if comments_by_location[location] == nil then
82 1.1 rillig table.insert(comment_locations, location)
83 1.1 rillig comments_by_location[location] = {}
84 1.1 rillig end
85 1.1 rillig local trimmed_msg = message:match("^%s*(.-)%s*$")
86 1.1 rillig table.insert(comments_by_location[location], trimmed_msg)
87 1.1 rillig end
88 1.1 rillig
89 1.1 rillig for phys_lineno, line in ipairs(lines) do
90 1.1 rillig
91 1.1 rillig for offset, comment in line:gmatch("/%* expect([+%-]%d+): (.-) %*/") do
92 1.1 rillig add_expectation(tonumber(offset), comment)
93 1.1 rillig end
94 1.1 rillig
95 1.1 rillig pp_lineno = pp_lineno + 1
96 1.1 rillig
97 1.1 rillig local ppl_lineno, ppl_fname = line:match("^#%s*(%d+)%s+\"([^\"]+)\"")
98 1.1 rillig if ppl_lineno ~= nil then
99 1.3 rillig if ppl_fname == basename and tonumber(ppl_lineno) ~= phys_lineno + 1 then
100 1.1 rillig print_error("error: %s:%d: preprocessor line number must be %d",
101 1.1 rillig fname, phys_lineno, phys_lineno + 1)
102 1.1 rillig end
103 1.5 rillig if ppl_fname:match("%.c$") and ppl_fname ~= basename then
104 1.5 rillig print_error("error: %s:%d: preprocessor filename must be '%s'",
105 1.5 rillig fname, phys_lineno, basename)
106 1.5 rillig end
107 1.1 rillig pp_fname = ppl_fname
108 1.1 rillig pp_lineno = ppl_lineno
109 1.1 rillig end
110 1.1 rillig end
111 1.1 rillig
112 1.1 rillig return comment_locations, comments_by_location
113 1.1 rillig end
114 1.1 rillig
115 1.1 rillig
116 1.1 rillig -- Load the expected raw lint output from a .exp file.
117 1.1 rillig --
118 1.1 rillig -- example return value: {
119 1.1 rillig -- {
120 1.5 rillig -- exp_lineno = 18,
121 1.1 rillig -- location = "file.c(18)",
122 1.1 rillig -- message = "not a constant expression [123]",
123 1.1 rillig -- }
124 1.1 rillig -- }
125 1.1 rillig local function load_exp(exp_fname)
126 1.1 rillig
127 1.1 rillig local lines = load_lines(exp_fname)
128 1.2 rillig if lines == nil then
129 1.2 rillig print_error("check-expect.lua: error: file " .. exp_fname .. " not found")
130 1.2 rillig return
131 1.2 rillig end
132 1.1 rillig
133 1.1 rillig local messages = {}
134 1.1 rillig for exp_lineno, line in ipairs(lines) do
135 1.8 rillig for location, message in line:gmatch("(.+%(%d+%)): (.+)$") do
136 1.1 rillig table.insert(messages, {
137 1.1 rillig exp_lineno = exp_lineno,
138 1.1 rillig location = location,
139 1.1 rillig message = message
140 1.1 rillig })
141 1.1 rillig end
142 1.1 rillig end
143 1.1 rillig
144 1.1 rillig return messages
145 1.1 rillig end
146 1.1 rillig
147 1.1 rillig
148 1.1 rillig local function matches(comment, pattern)
149 1.1 rillig if comment == "" then return false end
150 1.1 rillig
151 1.1 rillig local any_prefix = pattern:sub(1, 3) == "..."
152 1.1 rillig if any_prefix then pattern = pattern:sub(4) end
153 1.1 rillig local any_suffix = pattern:sub(-3) == "..."
154 1.1 rillig if any_suffix then pattern = pattern:sub(1, -4) end
155 1.1 rillig
156 1.1 rillig if any_prefix and any_suffix then
157 1.1 rillig return comment:find(pattern, 1, true) ~= nil
158 1.1 rillig elseif any_prefix then
159 1.1 rillig return pattern ~= "" and comment:sub(-#pattern) == pattern
160 1.1 rillig elseif any_suffix then
161 1.1 rillig return comment:sub(1, #pattern) == pattern
162 1.1 rillig else
163 1.1 rillig return comment == pattern
164 1.1 rillig end
165 1.1 rillig end
166 1.1 rillig
167 1.1 rillig test(function()
168 1.1 rillig assert_equals(matches("a", "a"), true)
169 1.1 rillig assert_equals(matches("a", "b"), false)
170 1.1 rillig assert_equals(matches("a", "aaa"), false)
171 1.1 rillig
172 1.1 rillig assert_equals(matches("abc", "a..."), true)
173 1.1 rillig assert_equals(matches("abc", "c..."), false)
174 1.1 rillig
175 1.1 rillig assert_equals(matches("abc", "...c"), true)
176 1.1 rillig assert_equals(matches("abc", "...a"), false)
177 1.1 rillig
178 1.1 rillig assert_equals(matches("abc123xyz", "...a..."), true)
179 1.1 rillig assert_equals(matches("abc123xyz", "...b..."), true)
180 1.1 rillig assert_equals(matches("abc123xyz", "...c..."), true)
181 1.1 rillig assert_equals(matches("abc123xyz", "...1..."), true)
182 1.1 rillig assert_equals(matches("abc123xyz", "...2..."), true)
183 1.1 rillig assert_equals(matches("abc123xyz", "...3..."), true)
184 1.1 rillig assert_equals(matches("abc123xyz", "...x..."), true)
185 1.1 rillig assert_equals(matches("abc123xyz", "...y..."), true)
186 1.1 rillig assert_equals(matches("abc123xyz", "...z..."), true)
187 1.1 rillig assert_equals(matches("pattern", "...pattern..."), true)
188 1.10 rillig assert_equals(matches("pattern", "... pattern ..."), false)
189 1.1 rillig end)
190 1.1 rillig
191 1.1 rillig
192 1.10 rillig -- Inserts the '/* expect */' lines to the .c file, so that the .c file
193 1.10 rillig -- matches the .exp file.
194 1.10 rillig --
195 1.10 rillig -- TODO: Fix crashes in tests with '# line file' preprocessing directives.
196 1.6 rillig local function insert_missing(missing)
197 1.6 rillig for fname, items in pairs(missing) do
198 1.10 rillig for i, item in ipairs(items) do
199 1.10 rillig item.stable_sort_rank = i
200 1.10 rillig end
201 1.10 rillig local function less(a, b)
202 1.10 rillig if a.lineno ~= b.lineno then
203 1.10 rillig return a.lineno > b.lineno
204 1.10 rillig end
205 1.10 rillig return a.stable_sort_rank > b.stable_sort_rank
206 1.10 rillig end
207 1.10 rillig table.sort(items, less)
208 1.7 rillig local lines = assert(load_lines(fname))
209 1.10 rillig local seen = {}
210 1.6 rillig for _, item in ipairs(items) do
211 1.6 rillig local lineno, message = item.lineno, item.message
212 1.6 rillig local indent = (lines[lineno] or ""):match("^([ \t]*)")
213 1.10 rillig local offset = 1 + (seen[lineno] or 0)
214 1.10 rillig local line = ("%s/* expect+%d: %s */"):format(indent, offset, message)
215 1.6 rillig table.insert(lines, lineno, line)
216 1.10 rillig seen[lineno] = (seen[lineno] or 0) + 1
217 1.6 rillig end
218 1.6 rillig save_lines(fname, lines)
219 1.6 rillig end
220 1.6 rillig end
221 1.6 rillig
222 1.6 rillig
223 1.6 rillig local function check_test(c_fname, update)
224 1.1 rillig local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
225 1.1 rillig
226 1.1 rillig local c_comment_locations, c_comments_by_location = load_c(c_fname)
227 1.1 rillig if c_comment_locations == nil then return end
228 1.1 rillig
229 1.1 rillig local exp_messages = load_exp(exp_fname) or {}
230 1.6 rillig local missing = {}
231 1.1 rillig
232 1.1 rillig for _, exp_message in ipairs(exp_messages) do
233 1.1 rillig local c_comments = c_comments_by_location[exp_message.location] or {}
234 1.1 rillig local expected_message =
235 1.1 rillig exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
236 1.1 rillig
237 1.1 rillig local found = false
238 1.1 rillig for i, c_comment in ipairs(c_comments) do
239 1.12 rillig if c_comment ~= "" then
240 1.12 rillig if matches(expected_message, c_comment) then
241 1.12 rillig c_comments[i] = ""
242 1.12 rillig found = true
243 1.12 rillig end
244 1.1 rillig break
245 1.1 rillig end
246 1.1 rillig end
247 1.1 rillig
248 1.1 rillig if not found then
249 1.1 rillig print_error("error: %s: missing /* expect+1: %s */",
250 1.1 rillig exp_message.location, expected_message)
251 1.6 rillig
252 1.6 rillig if update then
253 1.6 rillig local fname = exp_message.location:match("^([^(]+)")
254 1.6 rillig local lineno = tonumber(exp_message.location:match("%((%d+)%)$"))
255 1.6 rillig if not missing[fname] then missing[fname] = {} end
256 1.6 rillig table.insert(missing[fname], {
257 1.6 rillig lineno = lineno,
258 1.6 rillig message = expected_message,
259 1.6 rillig })
260 1.6 rillig end
261 1.1 rillig end
262 1.1 rillig end
263 1.1 rillig
264 1.1 rillig for _, c_comment_location in ipairs(c_comment_locations) do
265 1.1 rillig for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
266 1.1 rillig if c_comment ~= "" then
267 1.1 rillig print_error(
268 1.1 rillig "error: %s: declared message \"%s\" is not in the actual output",
269 1.1 rillig c_comment_location, c_comment)
270 1.1 rillig end
271 1.1 rillig end
272 1.1 rillig end
273 1.6 rillig
274 1.6 rillig if missing then
275 1.6 rillig insert_missing(missing)
276 1.6 rillig end
277 1.1 rillig end
278 1.1 rillig
279 1.1 rillig
280 1.1 rillig local function main(args)
281 1.13 rillig local update = false
282 1.13 rillig for _, arg in ipairs(args) do
283 1.13 rillig if arg == "-u" then
284 1.13 rillig update = true
285 1.13 rillig end
286 1.6 rillig end
287 1.6 rillig
288 1.1 rillig for _, name in ipairs(args) do
289 1.13 rillig if name ~= "-u" then
290 1.13 rillig check_test(name, update)
291 1.13 rillig end
292 1.1 rillig end
293 1.1 rillig end
294 1.1 rillig
295 1.1 rillig
296 1.1 rillig main(arg)
297 1.1 rillig os.exit(not had_errors)
298