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