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