Home | History | Annotate | Line # | Download | only in indent
      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