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