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