check-expect.lua revision 1.5 1 1.1 rillig #! /usr/bin/lua
2 1.5 rillig -- $NetBSD: check-expect.lua,v 1.5 2023/07/06 07:33:36 rillig Exp $
3 1.1 rillig
4 1.1 rillig --[[
5 1.1 rillig
6 1.1 rillig usage: lua ./check-expect.lua *.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.1 rillig local f = io.open(fname, "r")
41 1.1 rillig if f == nil then return nil 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.1 rillig -- Load the 'expect:' comments from a C source file.
53 1.1 rillig --
54 1.1 rillig -- example return values:
55 1.1 rillig -- {
56 1.1 rillig -- ["file.c(18)"] = {"invalid argument 'a'", "invalid argument 'b'"},
57 1.1 rillig -- ["file.c(23)"] = {"not a constant expression [123]"},
58 1.1 rillig -- },
59 1.1 rillig -- { "file.c(18)", "file.c(23)" }
60 1.1 rillig local function load_c(fname)
61 1.4 rillig local basename = fname:match("([^/]+)$")
62 1.1 rillig local lines = load_lines(fname)
63 1.1 rillig if lines == nil then return nil, nil end
64 1.1 rillig
65 1.1 rillig local pp_fname = fname
66 1.1 rillig local pp_lineno = 0
67 1.1 rillig local comment_locations = {}
68 1.1 rillig local comments_by_location = {}
69 1.1 rillig
70 1.1 rillig local function add_expectation(offset, message)
71 1.1 rillig local location = ("%s(%d)"):format(pp_fname, pp_lineno + offset)
72 1.1 rillig if comments_by_location[location] == nil then
73 1.1 rillig table.insert(comment_locations, location)
74 1.1 rillig comments_by_location[location] = {}
75 1.1 rillig end
76 1.1 rillig local trimmed_msg = message:match("^%s*(.-)%s*$")
77 1.1 rillig table.insert(comments_by_location[location], trimmed_msg)
78 1.1 rillig end
79 1.1 rillig
80 1.1 rillig for phys_lineno, line in ipairs(lines) do
81 1.1 rillig
82 1.1 rillig for offset, comment in line:gmatch("/%* expect([+%-]%d+): (.-) %*/") do
83 1.1 rillig add_expectation(tonumber(offset), comment)
84 1.1 rillig end
85 1.1 rillig
86 1.1 rillig pp_lineno = pp_lineno + 1
87 1.1 rillig
88 1.1 rillig local ppl_lineno, ppl_fname = line:match("^#%s*(%d+)%s+\"([^\"]+)\"")
89 1.1 rillig if ppl_lineno ~= nil then
90 1.3 rillig if ppl_fname == basename and tonumber(ppl_lineno) ~= phys_lineno + 1 then
91 1.1 rillig print_error("error: %s:%d: preprocessor line number must be %d",
92 1.1 rillig fname, phys_lineno, phys_lineno + 1)
93 1.1 rillig end
94 1.5 rillig if ppl_fname:match("%.c$") and ppl_fname ~= basename then
95 1.5 rillig print_error("error: %s:%d: preprocessor filename must be '%s'",
96 1.5 rillig fname, phys_lineno, basename)
97 1.5 rillig end
98 1.1 rillig pp_fname = ppl_fname
99 1.1 rillig pp_lineno = ppl_lineno
100 1.1 rillig end
101 1.1 rillig end
102 1.1 rillig
103 1.1 rillig return comment_locations, comments_by_location
104 1.1 rillig end
105 1.1 rillig
106 1.1 rillig
107 1.1 rillig -- Load the expected raw lint output from a .exp file.
108 1.1 rillig --
109 1.1 rillig -- example return value: {
110 1.1 rillig -- {
111 1.5 rillig -- exp_lineno = 18,
112 1.1 rillig -- location = "file.c(18)",
113 1.1 rillig -- message = "not a constant expression [123]",
114 1.1 rillig -- }
115 1.1 rillig -- }
116 1.1 rillig local function load_exp(exp_fname)
117 1.1 rillig
118 1.1 rillig local lines = load_lines(exp_fname)
119 1.2 rillig if lines == nil then
120 1.2 rillig print_error("check-expect.lua: error: file " .. exp_fname .. " not found")
121 1.2 rillig return
122 1.2 rillig end
123 1.1 rillig
124 1.1 rillig local messages = {}
125 1.1 rillig for exp_lineno, line in ipairs(lines) do
126 1.1 rillig for location, message in line:gmatch("(%S+%(%d+%)): (.+)$") do
127 1.1 rillig table.insert(messages, {
128 1.1 rillig exp_lineno = exp_lineno,
129 1.1 rillig location = location,
130 1.1 rillig message = message
131 1.1 rillig })
132 1.1 rillig end
133 1.1 rillig end
134 1.1 rillig
135 1.1 rillig return messages
136 1.1 rillig end
137 1.1 rillig
138 1.1 rillig
139 1.1 rillig ---@param comment string
140 1.1 rillig ---@param pattern string
141 1.1 rillig ---@return boolean
142 1.1 rillig local function matches(comment, pattern)
143 1.1 rillig if comment == "" then return false end
144 1.1 rillig
145 1.1 rillig local any_prefix = pattern:sub(1, 3) == "..."
146 1.1 rillig if any_prefix then pattern = pattern:sub(4) end
147 1.1 rillig local any_suffix = pattern:sub(-3) == "..."
148 1.1 rillig if any_suffix then pattern = pattern:sub(1, -4) end
149 1.1 rillig
150 1.1 rillig if any_prefix and any_suffix then
151 1.1 rillig return comment:find(pattern, 1, true) ~= nil
152 1.1 rillig elseif any_prefix then
153 1.1 rillig return pattern ~= "" and comment:sub(-#pattern) == pattern
154 1.1 rillig elseif any_suffix then
155 1.1 rillig return comment:sub(1, #pattern) == pattern
156 1.1 rillig else
157 1.1 rillig return comment == pattern
158 1.1 rillig end
159 1.1 rillig end
160 1.1 rillig
161 1.1 rillig test(function()
162 1.1 rillig assert_equals(matches("a", "a"), true)
163 1.1 rillig assert_equals(matches("a", "b"), false)
164 1.1 rillig assert_equals(matches("a", "aaa"), false)
165 1.1 rillig
166 1.1 rillig assert_equals(matches("abc", "a..."), true)
167 1.1 rillig assert_equals(matches("abc", "c..."), false)
168 1.1 rillig
169 1.1 rillig assert_equals(matches("abc", "...c"), true)
170 1.1 rillig assert_equals(matches("abc", "...a"), false)
171 1.1 rillig
172 1.1 rillig assert_equals(matches("abc123xyz", "...a..."), true)
173 1.1 rillig assert_equals(matches("abc123xyz", "...b..."), true)
174 1.1 rillig assert_equals(matches("abc123xyz", "...c..."), true)
175 1.1 rillig assert_equals(matches("abc123xyz", "...1..."), true)
176 1.1 rillig assert_equals(matches("abc123xyz", "...2..."), true)
177 1.1 rillig assert_equals(matches("abc123xyz", "...3..."), true)
178 1.1 rillig assert_equals(matches("abc123xyz", "...x..."), true)
179 1.1 rillig assert_equals(matches("abc123xyz", "...y..."), true)
180 1.1 rillig assert_equals(matches("abc123xyz", "...z..."), true)
181 1.1 rillig assert_equals(matches("pattern", "...pattern..."), true)
182 1.1 rillig end)
183 1.1 rillig
184 1.1 rillig
185 1.1 rillig local function check_test(c_fname)
186 1.1 rillig local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
187 1.1 rillig
188 1.1 rillig local c_comment_locations, c_comments_by_location = load_c(c_fname)
189 1.1 rillig if c_comment_locations == nil then return end
190 1.1 rillig
191 1.1 rillig local exp_messages = load_exp(exp_fname) or {}
192 1.1 rillig
193 1.1 rillig for _, exp_message in ipairs(exp_messages) do
194 1.1 rillig local c_comments = c_comments_by_location[exp_message.location] or {}
195 1.1 rillig local expected_message =
196 1.1 rillig exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
197 1.1 rillig
198 1.1 rillig local found = false
199 1.1 rillig for i, c_comment in ipairs(c_comments) do
200 1.1 rillig if c_comment ~= "" and matches(expected_message, c_comment) then
201 1.1 rillig c_comments[i] = ""
202 1.1 rillig found = true
203 1.1 rillig break
204 1.1 rillig end
205 1.1 rillig end
206 1.1 rillig
207 1.1 rillig if not found then
208 1.1 rillig print_error("error: %s: missing /* expect+1: %s */",
209 1.1 rillig exp_message.location, expected_message)
210 1.1 rillig end
211 1.1 rillig end
212 1.1 rillig
213 1.1 rillig for _, c_comment_location in ipairs(c_comment_locations) do
214 1.1 rillig for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
215 1.1 rillig if c_comment ~= "" then
216 1.1 rillig print_error(
217 1.1 rillig "error: %s: declared message \"%s\" is not in the actual output",
218 1.1 rillig c_comment_location, c_comment)
219 1.1 rillig end
220 1.1 rillig end
221 1.1 rillig end
222 1.1 rillig end
223 1.1 rillig
224 1.1 rillig
225 1.1 rillig local function main(args)
226 1.1 rillig for _, name in ipairs(args) do
227 1.1 rillig check_test(name)
228 1.1 rillig end
229 1.1 rillig end
230 1.1 rillig
231 1.1 rillig
232 1.1 rillig main(arg)
233 1.1 rillig os.exit(not had_errors)
234