xref: /netbsd-src/tests/usr.bin/indent/t_options.lua (revision 4170684f22077e3779c5c14826430de0dec964b2)
1-- $NetBSD: t_options.lua,v 1.4 2023/05/22 06:35:56 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
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 die(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		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
119end
120
121local function check_unused_input()
122	if unused_input_lineno ~= 0 then
123		warn(unused_input_lineno, "input is not used")
124	end
125end
126
127local function run_indent(inp, args)
128	local indent = os.getenv("INDENT") or "indent"
129	local cmd = indent .. " " .. args .. " 2>&1"
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
137end
138
139local function handle_empty_section(line)
140	if line == "" then
141		curr_empty_lines = curr_empty_lines + 1
142	else
143		if curr_empty_lines > max_empty_lines then
144			max_empty_lines = curr_empty_lines
145		end
146		if curr_empty_lines > 0 then
147			if prev_empty_lines > 1 then
148				warn(lineno - curr_empty_lines - 1,
149				    prev_empty_lines .. " empty lines a few "
150				    .. "lines above, should be only 1")
151			end
152			prev_empty_lines = curr_empty_lines
153		end
154		curr_empty_lines = 0
155	end
156end
157
158local function handle_indent_input()
159	if prev_empty_lines ~= 2 and seen_input_section then
160		warn(lineno, "input section needs 2 empty lines "
161		    .. "above, not " .. prev_empty_lines)
162	end
163	check_empty_lines_block(2)
164	check_unused_input()
165	section = "input"
166	section_excl_comm = ""
167	section_incl_comm = ""
168	unused_input_lineno = lineno
169	seen_input_section = true
170	output_excl_comm = ""
171	output_incl_comm = ""
172	output_lineno = 0
173end
174
175local function handle_indent_run(args)
176	if section ~= "" then
177		warn(lineno, "unfinished section '" .. section .. "'")
178	end
179	check_empty_lines_block(1)
180	if prev_empty_lines ~= 1 then
181		warn(lineno, "run section needs 1 empty line above, "
182		    .. "not " .. prev_empty_lines)
183	end
184	section = "run"
185	output_lineno = lineno
186	section_excl_comm = ""
187	section_incl_comm = ""
188
189	run_indent(input_excl_comm, args)
190	unused_input_lineno = 0
191end
192
193local function handle_indent_run_equals_input(args)
194	check_empty_lines_block(1)
195	run_indent(input_excl_comm, args)
196	expected_out:write(input_excl_comm)
197	unused_input_lineno = 0
198	max_empty_lines = 0
199end
200
201local function handle_indent_run_equals_prev_output(args)
202	check_empty_lines_block(1)
203	run_indent(input_excl_comm, args)
204	expected_out:write(output_excl_comm)
205	max_empty_lines = 0
206end
207
208local function handle_indent_end_input()
209	if section_incl_comm == input_incl_comm then
210		warn(lineno, "duplicate input; remove this section")
211	end
212
213	input_excl_comm = section_excl_comm
214	input_incl_comm = section_incl_comm
215	section = ""
216	max_empty_lines = 0
217end
218
219local function handle_indent_end_run()
220	if section_incl_comm == input_incl_comm then
221		warn(output_lineno,
222		    "output == input; use run-equals-input")
223	end
224	if section_incl_comm == output_incl_comm then
225		warn(output_lineno,
226		    "duplicate output; use run-equals-prev-output")
227	end
228
229	output_excl_comm = section_excl_comm
230	output_incl_comm = section_incl_comm
231	section = ""
232	max_empty_lines = 0
233end
234
235local function handle_indent_directive(line, command, args)
236	print(line)
237	expected_out:write(line .. "\n")
238
239	if command == "input" and args ~= "" then
240		warn(lineno, "'//indent input' does not take arguments")
241	elseif command == "input" then
242		handle_indent_input()
243	elseif command == "run" then
244		handle_indent_run(args)
245	elseif command == "run-equals-input" then
246		handle_indent_run_equals_input(args)
247	elseif command == "run-equals-prev-output" then
248		handle_indent_run_equals_prev_output(args)
249	elseif command == "end" and args ~= "" then
250		warn(lineno, "'//indent end' does not take arguments")
251	elseif command == "end" and section == "input" then
252		handle_indent_end_input()
253	elseif command == "end" and section == "run" then
254		handle_indent_end_run()
255	elseif command == "end" then
256		warn(lineno, "misplaced '//indent end'")
257	else
258		die(lineno, "invalid line '" .. line .. "'")
259	end
260
261	prev_empty_lines = 0
262	curr_empty_lines = 0
263end
264
265local function handle_line(line)
266	if section == "" then
267		handle_empty_section(line)
268	end
269
270	-- Hide comments starting with dollar from indent; they are used for
271	-- marking bugs and adding other remarks directly in the input or
272	-- output sections.
273	if line:match("^%s*/[*]%s*[$].*[*]/$")
274	    or line:match("^%s*//%s*[$]") then
275		if section ~= "" then
276			section_incl_comm = section_incl_comm .. line .. "\n"
277		end
278		return
279	end
280
281	local cmd, args = line:match("^//indent%s+([^%s]+)%s*(.*)$")
282	if cmd then
283		handle_indent_directive(line, cmd, args)
284		return
285	end
286
287	if section == "input" or section == "run" then
288		section_excl_comm = section_excl_comm .. line .. "\n"
289		section_incl_comm = section_incl_comm .. line .. "\n"
290	end
291
292	if section == "run" then
293		expected_out:write(line .. "\n")
294	end
295
296	if section == ""
297	    and line ~= ""
298	    and line:sub(1, 1) ~= "/"
299	    and line:sub(1, 2) ~= " *" then
300		warn(lineno, "non-comment line outside 'input' or 'run' "
301		    .. "section")
302	end
303end
304
305local function handle_file(fname)
306	init_file(fname)
307	local f = assert(io.open(fname))
308	for line in f:lines() do
309		lineno = lineno + 1
310		handle_line(line)
311	end
312	f:close()
313end
314
315local function main()
316	for _, arg in ipairs(arg) do
317		handle_file(arg)
318	end
319	if section ~= "" then
320		die(lineno, "still in section '" .. section .. "'")
321	end
322	check_unused_input()
323	expected_out:close()
324	os.exit(not warned)
325end
326
327main()
328