Home | History | Annotate | Line # | Download | only in unit-tests
      1 #!  /usr/bin/lua
      2 -- $NetBSD: check-expect.lua,v 1.17 2025/07/01 05:03:18 rillig Exp $
      3 
      4 --[[
      5 
      6 usage: lua ./check-expect.lua *.mk
      7 
      8 Check that the various 'expect' comments in the .mk files produce the
      9 expected text in the corresponding .exp file.
     10 
     11 # expect: <line>
     12         Each <line> must occur in the .exp file.
     13         The order in the .mk file must be the same as in the .exp file.
     14 
     15 # expect[+-]offset: <message>
     16         Each <message> must occur in the .exp file and refer back to the
     17         source line in the .mk file.
     18         Each such line in the .exp file must have a corresponding expect line
     19         in the .mk file.
     20         The order in the .mk file must be the same as in the .exp file.
     21 
     22 # expect-reset
     23         Search the following "expect:" and "expect[+-]offset:" comments
     24         from the top of the .exp file again.
     25 
     26 # expect-not: <substring>
     27         The <substring> must not occur as part of any line in the .exp file.
     28 
     29 # expect-not-matches: <pattern>
     30         The <pattern> (see https://lua.org/manual/5.4/manual.html#6.4.1)
     31         must not occur as part of any line in the .exp file.
     32 ]]
     33 
     34 
     35 local had_errors = false
     36 ---@param fmt string
     37 local function print_error(fmt, ...)
     38   print(fmt:format(...))
     39   had_errors = true
     40 end
     41 
     42 
     43 ---@return nil | string[]
     44 local function load_lines(fname)
     45   local lines = {}
     46 
     47   local f = io.open(fname, "r")
     48   if f == nil then
     49     return nil
     50   end
     51 
     52   for line in f:lines() do
     53     table.insert(lines, line)
     54   end
     55   f:close()
     56 
     57   return lines
     58 end
     59 
     60 
     61 --- @shape ExpLine
     62 --- @field filename string | nil
     63 --- @field lineno number | nil
     64 --- @field text string
     65 
     66 
     67 --- @param lines string[]
     68 --- @return ExpLine[]
     69 local function parse_exp(lines)
     70   local exp_lines = {}
     71   for _, line in ipairs(lines) do
     72     local l_filename, l_lineno, l_text =
     73       line:match('^make: ([^:]+%.mk):(%d+):%s+(.*)')
     74     if not l_filename then
     75       l_text = line
     76     end
     77     l_text = l_text:gsub("^%s+", ""):gsub("%s+$", "")
     78     table.insert(exp_lines, {
     79       filename = l_filename,
     80       lineno = tonumber(l_lineno),
     81       text = l_text,
     82     })
     83   end
     84   return exp_lines
     85 end
     86 
     87 ---@param exp_lines ExpLine[]
     88 local function detect_missing_expect_lines(exp_fname, exp_lines, s, e)
     89   for i = s, e do
     90     local exp_line = exp_lines[i]
     91     if exp_line.filename then
     92       print_error("error: %s:%d requires in %s:%d: # expect+1: %s",
     93         exp_fname, i, exp_line.filename, exp_line.lineno, exp_line.text)
     94     end
     95   end
     96 end
     97 
     98 local 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_raw_lines = load_lines(exp_fname)
    102   if exp_raw_lines == nil then
    103     return
    104   end
    105   local exp_lines = parse_exp(exp_raw_lines)
    106 
    107   local exp_it = 1
    108 
    109   for mk_lineno, mk_line in ipairs(mk_lines) do
    110 
    111     local function match(pattern, action)
    112       local _, n = mk_line:gsub(pattern, action)
    113       if n > 0 then
    114         match = function() end
    115       end
    116     end
    117 
    118     match("^#%s+expect%-not:%s*(.*)", function(text)
    119       for exp_lineno, exp_line in ipairs(exp_lines) do
    120         if exp_line.text:find(text, 1, true) then
    121           print_error("error: %s:%d: %s:%d must not contain '%s'",
    122             mk_fname, mk_lineno, exp_fname, exp_lineno, text)
    123         end
    124       end
    125     end)
    126 
    127     match("^#%s+expect%-not%-matches:%s*(.*)", function(pattern)
    128       for exp_lineno, exp_line in ipairs(exp_lines) do
    129         if exp_line.text:find(pattern) then
    130           print_error("error: %s:%d: %s:%d must not match '%s'",
    131             mk_fname, mk_lineno, exp_fname, exp_lineno, pattern)
    132         end
    133       end
    134     end)
    135 
    136     match("^#%s+expect:%s*(.*)", function(text)
    137       local i = exp_it
    138       while i <= #exp_lines and text ~= exp_lines[i].text do
    139         i = i + 1
    140       end
    141       if i <= #exp_lines then
    142         detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
    143         exp_lines[i].text = ""
    144         exp_it = i + 1
    145       else
    146         print_error("error: %s:%d: '%s:%d+' must contain '%s'",
    147           mk_fname, mk_lineno, exp_fname, exp_it, text)
    148       end
    149     end)
    150 
    151     match("^#%s+expect%-reset$", function()
    152       exp_it = 1
    153     end)
    154 
    155     match("^#%s+expect([+%-]%d+):%s*(.*)", function(offset, text)
    156       local msg_lineno = mk_lineno + tonumber(offset)
    157 
    158       local i = exp_it
    159       while i <= #exp_lines and text ~= exp_lines[i].text do
    160         i = i + 1
    161       end
    162 
    163       if i <= #exp_lines and exp_lines[i].lineno == msg_lineno then
    164         detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
    165         exp_lines[i].text = ""
    166         exp_it = i + 1
    167       elseif i <= #exp_lines then
    168         print_error("error: %s:%d: expect%+d must be expect%+d",
    169           mk_fname, mk_lineno, tonumber(offset),
    170           exp_lines[i].lineno - mk_lineno)
    171       else
    172         print_error("error: %s:%d: %s:%d+ must contain '%s'",
    173           mk_fname, mk_lineno, exp_fname, exp_it, text)
    174       end
    175     end)
    176 
    177     match("^#%s+expect[+%-:]", function()
    178       print_error("error: %s:%d: invalid \"expect\" line: %s",
    179         mk_fname, mk_lineno, mk_line)
    180     end)
    181 
    182   end
    183   detect_missing_expect_lines(exp_fname, exp_lines, exp_it, #exp_lines)
    184 end
    185 
    186 for _, fname in ipairs(arg) do
    187   check_mk(fname)
    188 end
    189 os.exit(not had_errors)
    190