t_options.lua revision 1.8 1 -- $NetBSD: t_options.lua,v 1.8 2024/12/12 05:33:47 rillig Exp $
2 --
3 -- Copyright (c) 2023 The NetBSD Foundation, Inc.
4 -- All rights reserved.
5 --
6 -- Redistribution and use in source and binary forms, with or without
7 -- modification, are permitted provided that the following conditions
8 -- are met:
9 -- 1. Redistributions of source code must retain the above copyright
10 -- notice, this list of conditions and the following disclaimer.
11 -- 2. Redistributions in binary form must reproduce the above copyright
12 -- notice, this list of conditions and the following disclaimer in the
13 -- documentation and/or other materials provided with the distribution.
14 --
15 -- THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
16 -- ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
17 -- TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18 -- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
19 -- BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20 -- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21 -- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22 -- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23 -- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24 -- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 -- POSSIBILITY OF SUCH DAMAGE.
26
27 -- usage: [INDENT=...] lua t_options.lua <file.c>...
28 --
29 -- Run indent on several inputs with different command line options, verifying
30 -- that the actual output equals the expected output.
31 --
32 -- The test files contain the input to be formatted, the formatting options
33 -- and the output, all as close together as possible. The test files use the
34 -- following directives:
35 --
36 -- //indent input
37 -- Specifies the input to be formatted.
38 -- //indent run [options]
39 -- Runs indent on the input, using the given options.
40 -- //indent end
41 -- Finishes an '//indent input' or '//indent run' section.
42 -- //indent run-equals-input [options]
43 -- Runs indent on the input, expecting that the output is the
44 -- same as the input.
45 -- //indent run-equals-prev-output [options]
46 -- Runs indent on the input, expecting the same output as from
47 -- the previous run.
48 --
49 -- All text outside input..end or run..end directives is not passed to indent.
50 --
51 -- Inside the input..end or run..end sections, comments that start with '$'
52 -- are filtered out, they can be used for remarks near the affected code.
53 --
54 -- The actual output from running indent is written to stdout, the expected
55 -- test output is written to 'expected.out', ready to be compared using diff.
56
57 local warned = false
58
59 local filename = ""
60 local lineno = 0
61
62 local prev_empty_lines = 0 -- the finished "block" of empty lines
63 local curr_empty_lines = 0 -- the ongoing "block" of empty lines
64 local max_empty_lines = 0 -- between sections
65 local seen_input_section = false-- the first input section is not checked
66
67 local section = "" -- "", "input" or "run"
68 local section_excl_comm = "" -- without dollar comments
69 local section_incl_comm = "" -- with dollar comments
70
71 local input_excl_comm = "" -- the input text for indent
72 local input_incl_comm = "" -- used for duplicate checks
73 local unused_input_lineno = 0
74
75 local output_excl_comm = "" -- expected output
76 local output_incl_comm = "" -- used for duplicate checks
77 local output_lineno = 0
78
79 local expected_out = assert(io.open("expected.out", "w"))
80
81 local function err(ln, msg)
82 io.stderr:write(("%s:%d: error: %s\n"):format(filename, ln, msg))
83 os.exit(false)
84 end
85
86 local function warn(ln, msg)
87 io.stderr:write(("%s:%d: warning: %s\n"):format(filename, ln, msg))
88 warned = true
89 end
90
91 local function init_file(fname)
92 filename = fname
93 lineno = 0
94
95 prev_empty_lines = 0
96 curr_empty_lines = 0
97 max_empty_lines = 0
98 seen_input_section = false
99
100 section = ""
101 section_excl_comm = ""
102 section_incl_comm = ""
103
104 input_excl_comm = ""
105 input_incl_comm = ""
106 unused_input_lineno = 0
107
108 output_excl_comm = ""
109 output_incl_comm = ""
110 output_lineno = 0
111 end
112
113 local function check_empty_lines_block(n)
114 if max_empty_lines ~= n and seen_input_section then
115 warn(lineno, ("expecting %d empty %s, got %d")
116 :format(n, n ~= 1 and "lines" or "line", max_empty_lines))
117 end
118 end
119
120 local function check_unused_input()
121 if unused_input_lineno ~= 0 then
122 warn(unused_input_lineno, "input is not used")
123 end
124 end
125
126 local function run_indent(inp, args)
127 local indent = os.getenv("INDENT") or "indent"
128 local cmd = indent .. " " .. args .. " 2>t_options.err"
129
130 local indent_in = assert(io.popen(cmd, "w"))
131 indent_in:write(inp)
132 local ok, kind, info = indent_in:close()
133 if not ok then
134 print("// " .. kind .. " " .. info)
135 end
136 for line in io.lines("t_options.err") do
137 print("// " .. line)
138 end
139 end
140
141 local function handle_line_outside_section(line)
142 if line == "" then
143 curr_empty_lines = curr_empty_lines + 1
144 return
145 end
146 if curr_empty_lines > max_empty_lines then
147 max_empty_lines = curr_empty_lines
148 end
149 if curr_empty_lines > 0 then
150 if prev_empty_lines > 1 then
151 warn(lineno - curr_empty_lines - 1,
152 prev_empty_lines .. " empty lines a few "
153 .. "lines above, should be only 1")
154 end
155 prev_empty_lines = curr_empty_lines
156 end
157 curr_empty_lines = 0
158 end
159
160 local function handle_indent_input()
161 if prev_empty_lines ~= 2 and seen_input_section then
162 warn(lineno, "input section needs 2 empty lines "
163 .. "above, not " .. prev_empty_lines)
164 end
165 check_empty_lines_block(2)
166 check_unused_input()
167 section = "input"
168 section_excl_comm = ""
169 section_incl_comm = ""
170 unused_input_lineno = lineno
171 seen_input_section = true
172 output_excl_comm = ""
173 output_incl_comm = ""
174 output_lineno = 0
175 end
176
177 local function handle_indent_run(args)
178 if section ~= "" then
179 warn(lineno, "unfinished section '" .. section .. "'")
180 end
181 check_empty_lines_block(1)
182 if prev_empty_lines ~= 1 then
183 warn(lineno, "run section needs 1 empty line above, "
184 .. "not " .. prev_empty_lines)
185 end
186 section = "run"
187 output_lineno = lineno
188 section_excl_comm = ""
189 section_incl_comm = ""
190
191 run_indent(input_excl_comm, args)
192 unused_input_lineno = 0
193 end
194
195 local function handle_indent_run_equals_input(args)
196 check_empty_lines_block(1)
197 run_indent(input_excl_comm, args)
198 expected_out:write(input_excl_comm)
199 unused_input_lineno = 0
200 max_empty_lines = 0
201 output_incl_comm = ""
202 output_excl_comm = ""
203 end
204
205 local function handle_indent_run_equals_prev_output(args)
206 if output_incl_comm == "" then
207 warn(lineno,
208 "no previous output; use run-equals-input instead")
209 end
210 check_empty_lines_block(1)
211 run_indent(input_excl_comm, args)
212 expected_out:write(output_excl_comm)
213 max_empty_lines = 0
214 end
215
216 local function handle_indent_end_input()
217 if section_incl_comm == input_incl_comm then
218 warn(lineno, "duplicate input; remove this section")
219 end
220
221 input_excl_comm = section_excl_comm
222 input_incl_comm = section_incl_comm
223 section = ""
224 max_empty_lines = 0
225 end
226
227 local function handle_indent_end_run()
228 if section_incl_comm == input_incl_comm then
229 warn(output_lineno,
230 "output == input; use run-equals-input")
231 end
232 if section_incl_comm == output_incl_comm then
233 warn(output_lineno,
234 "duplicate output; use run-equals-prev-output")
235 end
236
237 output_excl_comm = section_excl_comm
238 output_incl_comm = section_incl_comm
239 section = ""
240 max_empty_lines = 0
241 end
242
243 local function handle_indent_directive(line, command, args)
244 print(line)
245 expected_out:write(line .. "\n")
246
247 if command == "input" and args ~= "" then
248 warn(lineno, "'//indent input' does not take arguments")
249 elseif command == "input" then
250 handle_indent_input()
251 elseif command == "run" then
252 handle_indent_run(args)
253 elseif command == "run-equals-input" then
254 handle_indent_run_equals_input(args)
255 elseif command == "run-equals-prev-output" then
256 handle_indent_run_equals_prev_output(args)
257 elseif command == "end" and args ~= "" then
258 warn(lineno, "'//indent end' does not take arguments")
259 elseif command == "end" and section == "input" then
260 handle_indent_end_input()
261 elseif command == "end" and section == "run" then
262 handle_indent_end_run()
263 elseif command == "end" then
264 warn(lineno, "misplaced '//indent end'")
265 else
266 err(lineno, "invalid line '" .. line .. "'")
267 end
268
269 prev_empty_lines = 0
270 curr_empty_lines = 0
271 end
272
273 local function handle_line(line)
274 if section == "" then
275 handle_line_outside_section(line)
276 end
277
278 -- Hide comments starting with dollar from indent; they are used for
279 -- marking bugs and adding other remarks directly in the input or
280 -- output sections.
281 if line:match("^%s*/[*]%s*[$].*[*]/$")
282 or line:match("^%s*//%s*[$]") then
283 if section ~= "" then
284 section_incl_comm = section_incl_comm .. line .. "\n"
285 end
286 return
287 end
288
289 local cmd, args = line:match("^//indent%s+([^%s]+)%s*(.*)$")
290 if cmd then
291 handle_indent_directive(line, cmd, args)
292 return
293 end
294
295 if section == "input" or section == "run" then
296 section_excl_comm = section_excl_comm .. line .. "\n"
297 section_incl_comm = section_incl_comm .. line .. "\n"
298 end
299
300 if section == "run" then
301 expected_out:write(line .. "\n")
302 end
303
304 if section == ""
305 and line ~= ""
306 and line:sub(1, 1) ~= "/"
307 and line:sub(1, 2) ~= " *" then
308 warn(lineno, "non-comment line outside 'input' or 'run' "
309 .. "section")
310 end
311 end
312
313 local function handle_file(fname)
314 init_file(fname)
315 local f = assert(io.open(fname))
316 for line in f:lines() do
317 lineno = lineno + 1
318 handle_line(line)
319 end
320 f:close()
321 end
322
323 local function main()
324 for _, arg in ipairs(arg) do
325 handle_file(arg)
326 end
327 if section ~= "" then
328 err(lineno, "still in section '" .. section .. "'")
329 end
330 check_unused_input()
331 expected_out:close()
332 os.exit(not warned)
333 end
334
335 main()
336