1#! /usr/bin/lua 2-- $NetBSD: check-expect.lua,v 1.13 2025/04/13 09:29:32 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 All of these lines must occur in the .exp file, in the same order as 13 in the .mk file. 14 15# expect-reset 16 Search the following 'expect:' comments from the top of the .exp 17 file again. 18 19# expect[+-]offset: <message> 20 Each message must occur in the .exp file and refer back to the 21 source line in the .mk file. 22 23# expect-not: <substring> 24 The substring must not occur as part of any line of the .exp file. 25 26# expect-not-matches: <pattern> 27 The pattern (see https://lua.org/manual/5.4/manual.html#6.4.1) 28 must not occur as part of any line of the .exp file. 29]] 30 31 32local had_errors = false 33---@param fmt string 34function print_error(fmt, ...) 35 print(fmt:format(...)) 36 had_errors = true 37end 38 39 40---@return nil | string[] 41local function load_lines(fname) 42 local lines = {} 43 44 local f = io.open(fname, "r") 45 if f == nil then return nil end 46 47 for line in f:lines() do 48 table.insert(lines, line) 49 end 50 f:close() 51 52 return lines 53end 54 55 56---@param exp_lines string[] 57local function collect_lineno_diagnostics(exp_lines) 58 ---@type table<string, string[]> 59 local by_location = {} 60 61 for _, line in ipairs(exp_lines) do 62 ---@type string | nil, string, string 63 local l_fname, l_lineno, l_msg = 64 line:match('^make: ([^:]+):(%d+): (.*)') 65 if l_fname ~= nil then 66 local location = ("%s:%d"):format(l_fname, l_lineno) 67 if by_location[location] == nil then 68 by_location[location] = {} 69 end 70 table.insert(by_location[location], l_msg) 71 end 72 end 73 74 return by_location 75end 76 77 78local function missing(by_location) 79 ---@type {filename: string, lineno: number, location: string, message: string}[] 80 local missing_expectations = {} 81 82 for location, messages in pairs(by_location) do 83 for _, message in ipairs(messages) do 84 if message ~= "" and location:find(".mk:") then 85 local filename, lineno = location:match("^(%S+):(%d+)$") 86 table.insert(missing_expectations, { 87 filename = filename, 88 lineno = tonumber(lineno), 89 location = location, 90 message = message 91 }) 92 end 93 end 94 end 95 table.sort(missing_expectations, function(a, b) 96 if a.filename ~= b.filename then 97 return a.filename < b.filename 98 end 99 return a.lineno < b.lineno 100 end) 101 return missing_expectations 102end 103 104 105local function check_mk(mk_fname) 106 local exp_fname = mk_fname:gsub("%.mk$", ".exp") 107 local mk_lines = load_lines(mk_fname) 108 local exp_lines = load_lines(exp_fname) 109 if exp_lines == nil then return end 110 local by_location = collect_lineno_diagnostics(exp_lines) 111 local prev_expect_line = 0 112 113 for mk_lineno, mk_line in ipairs(mk_lines) do 114 115 for text in mk_line:gmatch("#%s*expect%-not:%s*(.*)") do 116 local i = 1 117 while i <= #exp_lines and not exp_lines[i]:find(text, 1, true) do 118 i = i + 1 119 end 120 if i <= #exp_lines then 121 print_error("error: %s:%d: %s must not contain '%s'", 122 mk_fname, mk_lineno, exp_fname, text) 123 end 124 end 125 126 for text in mk_line:gmatch("#%s*expect%-not%-matches:%s*(.*)") do 127 local i = 1 128 while i <= #exp_lines and not exp_lines[i]:find(text) do 129 i = i + 1 130 end 131 if i <= #exp_lines then 132 print_error("error: %s:%d: %s must not match '%s'", 133 mk_fname, mk_lineno, exp_fname, text) 134 end 135 end 136 137 for text in mk_line:gmatch("#%s*expect:%s*(.*)") do 138 local i = prev_expect_line 139 -- As of 2022-04-15, some lines in the .exp files contain trailing 140 -- whitespace. If possible, this should be avoided by rewriting the 141 -- debug logging. When done, the trailing gsub can be removed. 142 -- See deptgt-phony.exp lines 14 and 15. 143 while i < #exp_lines and text ~= exp_lines[i + 1]:gsub("^%s*", ""):gsub("%s*$", "") do 144 i = i + 1 145 end 146 if i < #exp_lines then 147 prev_expect_line = i + 1 148 else 149 print_error("error: %s:%d: '%s:%d+' must contain '%s'", 150 mk_fname, mk_lineno, exp_fname, prev_expect_line + 1, text) 151 end 152 end 153 if mk_line:match("^#%s*expect%-reset$") then 154 prev_expect_line = 0 155 end 156 157 ---@param text string 158 for offset, text in mk_line:gmatch("#%s*expect([+%-]%d+):%s*(.*)") do 159 local location = ("%s:%d"):format(mk_fname, mk_lineno + tonumber(offset)) 160 161 local found = false 162 if by_location[location] ~= nil then 163 for i, message in ipairs(by_location[location]) do 164 if message == text then 165 by_location[location][i] = "" 166 found = true 167 break 168 elseif message ~= "" then 169 print_error("error: %s:%d: out-of-order '%s'", 170 mk_fname, mk_lineno, message) 171 end 172 end 173 end 174 175 if not found then 176 print_error("error: %s:%d: %s must contain '%s'", 177 mk_fname, mk_lineno, exp_fname, text) 178 end 179 end 180 end 181 182 for _, m in ipairs(missing(by_location)) do 183 print_error("missing: %s: # expect+1: %s", m.location, m.message) 184 end 185end 186 187for _, fname in ipairs(arg) do 188 check_mk(fname) 189end 190os.exit(not had_errors) 191