Home | History | Annotate | Line # | Download | only in unit-tests
check-expect.lua revision 1.16
      1 #!  /usr/bin/lua
      2 -- $NetBSD: check-expect.lua,v 1.16 2025/07/01 04:24:20 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     mk_line:gsub("^#%s+expect%-not:%s*(.*)", function(text)
    112       for exp_lineno, exp_line in ipairs(exp_lines) do
    113         if exp_line.text:find(text, 1, true) then
    114           print_error("error: %s:%d: %s:%d must not contain '%s'",
    115             mk_fname, mk_lineno, exp_fname, exp_lineno, text)
    116         end
    117       end
    118     end)
    119 
    120     mk_line:gsub("^#%s+expect%-not%-matches:%s*(.*)", function(pattern)
    121       for exp_lineno, exp_line in ipairs(exp_lines) do
    122         if exp_line.text:find(pattern) then
    123           print_error("error: %s:%d: %s:%d must not match '%s'",
    124             mk_fname, mk_lineno, exp_fname, exp_lineno, pattern)
    125         end
    126       end
    127     end)
    128 
    129     mk_line:gsub("^#%s+expect:%s*(.*)", function(text)
    130       local i = exp_it
    131       while i <= #exp_lines and text ~= exp_lines[i].text do
    132         i = i + 1
    133       end
    134       if i <= #exp_lines then
    135         detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
    136         exp_lines[i].text = ""
    137         exp_it = i + 1
    138       else
    139         print_error("error: %s:%d: '%s:%d+' must contain '%s'",
    140           mk_fname, mk_lineno, exp_fname, exp_it, text)
    141       end
    142     end)
    143 
    144     if mk_line:match("^#%s*expect%-reset$") then
    145       exp_it = 1
    146     end
    147 
    148     mk_line:gsub("^#%s+expect([+%-]%d+):%s*(.*)", function(offset, text)
    149       local msg_lineno = mk_lineno + tonumber(offset)
    150 
    151       local i = exp_it
    152       while i <= #exp_lines and text ~= exp_lines[i].text do
    153         i = i + 1
    154       end
    155 
    156       if i <= #exp_lines and exp_lines[i].lineno == msg_lineno then
    157         detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
    158         exp_lines[i].text = ""
    159         exp_it = i + 1
    160       elseif i <= #exp_lines then
    161         print_error("error: %s:%d: expect%+d must be expect%+d",
    162           mk_fname, mk_lineno, tonumber(offset),
    163           exp_lines[i].lineno - mk_lineno)
    164       else
    165         print_error("error: %s:%d: %s:%d+ must contain '%s'",
    166           mk_fname, mk_lineno, exp_fname, exp_it, text)
    167       end
    168     end)
    169   end
    170   detect_missing_expect_lines(exp_fname, exp_lines, exp_it, #exp_lines)
    171 end
    172 
    173 for _, fname in ipairs(arg) do
    174   check_mk(fname)
    175 end
    176 os.exit(not had_errors)
    177