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