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