xref: /netbsd-src/tests/usr.bin/indent/t_options.lua (revision b51d224522ceee6cecacc497eaa3a0fa7294cdd7)
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
57local warned = false
58
59local filename = ""
60local lineno = 0
61
62local prev_empty_lines = 0	-- the finished "block" of empty lines
63local curr_empty_lines = 0	-- the ongoing "block" of empty lines
64local max_empty_lines = 0	-- between sections
65local seen_input_section = false-- the first input section is not checked
66
67local section = ""		-- "", "input" or "run"
68local section_excl_comm = ""	-- without dollar comments
69local section_incl_comm = ""	-- with dollar comments
70
71local input_excl_comm = ""	-- the input text for indent
72local input_incl_comm = ""	-- used for duplicate checks
73local unused_input_lineno = 0
74
75local output_excl_comm = ""	-- expected output
76local output_incl_comm = ""	-- used for duplicate checks
77local output_lineno = 0
78
79local expected_out = assert(io.open("expected.out", "w"))
80
81local function err(ln, msg)
82	io.stderr:write(("%s:%d: error: %s\n"):format(filename, ln, msg))
83	os.exit(false)
84end
85
86local function warn(ln, msg)
87	io.stderr:write(("%s:%d: warning: %s\n"):format(filename, ln, msg))
88	warned = true
89end
90
91local 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
111end
112
113local 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
118end
119
120local function check_unused_input()
121	if unused_input_lineno ~= 0 then
122		warn(unused_input_lineno, "input is not used")
123	end
124end
125
126local 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
139end
140
141local 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
158end
159
160local 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
175end
176
177local 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
193end
194
195local 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 = ""
203end
204
205local 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
214end
215
216local 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
225end
226
227local 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
241end
242
243local 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
271end
272
273local 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
311end
312
313local 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()
321end
322
323local 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)
333end
334
335main()
336