t_options.lua revision 1.7 1 -- $NetBSD: t_options.lua,v 1.7 2023/06/26 12:21:18 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>...
28 --
29 -- Test driver for indent that runs indent on several inputs, checks the
30 -- output and can run indent with different command line options on the same
31 -- input.
32 --
33 -- The test files contain the input to be formatted, the formatting options
34 -- and the output, all as close together as possible. The test files use the
35 -- following directives:
36 --
37 -- //indent input
38 -- Specifies the input to be formatted.
39 -- //indent run [options]
40 -- Runs indent on the input, using the given options.
41 -- //indent end
42 -- Finishes an '//indent input' or '//indent run' section.
43 -- //indent run-equals-input [options]
44 -- Runs indent on the input, expecting unmodified output.
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'.
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 die(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 local lines = n ~= 1 and "lines" or "line"
116 warn(lineno, ("expecting %d empty %s, got %d")
117 :format(n, lines, max_empty_lines))
118 end
119 end
120
121 local function check_unused_input()
122 if unused_input_lineno ~= 0 then
123 warn(unused_input_lineno, "input is not used")
124 end
125 end
126
127 local function run_indent(inp, args)
128 local indent = os.getenv("INDENT") or "indent"
129 local cmd = indent .. " " .. args .. " 2>t_options.err"
130
131 local indent_in = assert(io.popen(cmd, "w"))
132 indent_in:write(inp)
133 local ok, kind, info = indent_in:close()
134 if not ok then
135 print("// " .. kind .. " " .. info)
136 end
137 for line in io.lines("t_options.err") do
138 print("// " .. line)
139 end
140 end
141
142 local function handle_empty_section(line)
143 if line == "" then
144 curr_empty_lines = curr_empty_lines + 1
145 else
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 end
160
161 local function handle_indent_input()
162 if prev_empty_lines ~= 2 and seen_input_section then
163 warn(lineno, "input section needs 2 empty lines "
164 .. "above, not " .. prev_empty_lines)
165 end
166 check_empty_lines_block(2)
167 check_unused_input()
168 section = "input"
169 section_excl_comm = ""
170 section_incl_comm = ""
171 unused_input_lineno = lineno
172 seen_input_section = true
173 output_excl_comm = ""
174 output_incl_comm = ""
175 output_lineno = 0
176 end
177
178 local function handle_indent_run(args)
179 if section ~= "" then
180 warn(lineno, "unfinished section '" .. section .. "'")
181 end
182 check_empty_lines_block(1)
183 if prev_empty_lines ~= 1 then
184 warn(lineno, "run section needs 1 empty line above, "
185 .. "not " .. prev_empty_lines)
186 end
187 section = "run"
188 output_lineno = lineno
189 section_excl_comm = ""
190 section_incl_comm = ""
191
192 run_indent(input_excl_comm, args)
193 unused_input_lineno = 0
194 end
195
196 local function handle_indent_run_equals_input(args)
197 check_empty_lines_block(1)
198 run_indent(input_excl_comm, args)
199 expected_out:write(input_excl_comm)
200 unused_input_lineno = 0
201 max_empty_lines = 0
202 output_incl_comm = ""
203 output_excl_comm = ""
204 end
205
206 local function handle_indent_run_equals_prev_output(args)
207 if output_incl_comm == "" then
208 warn(lineno,
209 "no previous output; use run-equals-input instead")
210 end
211 check_empty_lines_block(1)
212 run_indent(input_excl_comm, args)
213 expected_out:write(output_excl_comm)
214 max_empty_lines = 0
215 end
216
217 local function handle_indent_end_input()
218 if section_incl_comm == input_incl_comm then
219 warn(lineno, "duplicate input; remove this section")
220 end
221
222 input_excl_comm = section_excl_comm
223 input_incl_comm = section_incl_comm
224 section = ""
225 max_empty_lines = 0
226 end
227
228 local function handle_indent_end_run()
229 if section_incl_comm == input_incl_comm then
230 warn(output_lineno,
231 "output == input; use run-equals-input")
232 end
233 if section_incl_comm == output_incl_comm then
234 warn(output_lineno,
235 "duplicate output; use run-equals-prev-output")
236 end
237
238 output_excl_comm = section_excl_comm
239 output_incl_comm = section_incl_comm
240 section = ""
241 max_empty_lines = 0
242 end
243
244 local function handle_indent_directive(line, command, args)
245 print(line)
246 expected_out:write(line .. "\n")
247
248 if command == "input" and args ~= "" then
249 warn(lineno, "'//indent input' does not take arguments")
250 elseif command == "input" then
251 handle_indent_input()
252 elseif command == "run" then
253 handle_indent_run(args)
254 elseif command == "run-equals-input" then
255 handle_indent_run_equals_input(args)
256 elseif command == "run-equals-prev-output" then
257 handle_indent_run_equals_prev_output(args)
258 elseif command == "end" and args ~= "" then
259 warn(lineno, "'//indent end' does not take arguments")
260 elseif command == "end" and section == "input" then
261 handle_indent_end_input()
262 elseif command == "end" and section == "run" then
263 handle_indent_end_run()
264 elseif command == "end" then
265 warn(lineno, "misplaced '//indent end'")
266 else
267 die(lineno, "invalid line '" .. line .. "'")
268 end
269
270 prev_empty_lines = 0
271 curr_empty_lines = 0
272 end
273
274 local function handle_line(line)
275 if section == "" then
276 handle_empty_section(line)
277 end
278
279 -- Hide comments starting with dollar from indent; they are used for
280 -- marking bugs and adding other remarks directly in the input or
281 -- output sections.
282 if line:match("^%s*/[*]%s*[$].*[*]/$")
283 or line:match("^%s*//%s*[$]") then
284 if section ~= "" then
285 section_incl_comm = section_incl_comm .. line .. "\n"
286 end
287 return
288 end
289
290 local cmd, args = line:match("^//indent%s+([^%s]+)%s*(.*)$")
291 if cmd then
292 handle_indent_directive(line, cmd, args)
293 return
294 end
295
296 if section == "input" or section == "run" then
297 section_excl_comm = section_excl_comm .. line .. "\n"
298 section_incl_comm = section_incl_comm .. line .. "\n"
299 end
300
301 if section == "run" then
302 expected_out:write(line .. "\n")
303 end
304
305 if section == ""
306 and line ~= ""
307 and line:sub(1, 1) ~= "/"
308 and line:sub(1, 2) ~= " *" then
309 warn(lineno, "non-comment line outside 'input' or 'run' "
310 .. "section")
311 end
312 end
313
314 local function handle_file(fname)
315 init_file(fname)
316 local f = assert(io.open(fname))
317 for line in f:lines() do
318 lineno = lineno + 1
319 handle_line(line)
320 end
321 f:close()
322 end
323
324 local function main()
325 for _, arg in ipairs(arg) do
326 handle_file(arg)
327 end
328 if section ~= "" then
329 die(lineno, "still in section '" .. section .. "'")
330 end
331 check_unused_input()
332 expected_out:close()
333 os.exit(not warned)
334 end
335
336 main()
337