diff options
Diffstat (limited to 'v_windows/v/old/cmd/tools/vtest-parser.v')
-rw-r--r-- | v_windows/v/old/cmd/tools/vtest-parser.v | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/v_windows/v/old/cmd/tools/vtest-parser.v b/v_windows/v/old/cmd/tools/vtest-parser.v new file mode 100644 index 0000000..e1e829a --- /dev/null +++ b/v_windows/v/old/cmd/tools/vtest-parser.v @@ -0,0 +1,289 @@ +import os +import flag +import term +import time +import v.parser +import v.ast +import v.pref + +const ( + vexe = pref.vexe_path() + vroot = os.dir(vexe) + support_color = term.can_show_color_on_stderr() && term.can_show_color_on_stdout() + ecode_timeout = 101 + ecode_memout = 102 + ecode_details = map{ + -1: 'worker executable not found' + 101: 'too slow' + 102: 'too memory hungry' + } +) + +struct Context { +mut: + is_help bool + is_worker bool + is_verbose bool + is_silent bool // do not print any status/progress during processing, just failures. + is_linear bool // print linear progress log, without trying to do term cursor up + \r msg. Easier to use in a CI job + timeout_ms int + myself string // path to this executable, so the supervisor can launch worker processes + all_paths []string // all files given to the supervisor process + path string // the current path, given to a worker process + cut_index int // the cut position in the source from context.path + max_index int // the maximum index (equivalent to the file content length) + // parser context in the worker processes: + table ast.Table + scope ast.Scope + pref &pref.Preferences + period_ms int // print periodic progress + stop_print bool // stop printing the periodic progress +} + +fn main() { + mut context := process_cli_args() + if context.is_worker { + pid := os.getpid() + context.log('> worker ${pid:5} starts parsing at cut_index: ${context.cut_index:5} | $context.path') + // A worker's process job is to try to parse a single given file in context.path. + // It can crash/panic freely. + context.table = ast.new_table() + context.scope = &ast.Scope{ + parent: 0 + } + context.pref = &pref.Preferences{ + output_mode: .silent + } + mut source := os.read_file(context.path) ? + source = source[..context.cut_index] + + go fn (ms int) { + time.sleep(ms * time.millisecond) + exit(ecode_timeout) + }(context.timeout_ms) + _ := parser.parse_text(source, context.path, context.table, .skip_comments, context.pref) + context.log('> worker ${pid:5} finished parsing $context.path') + exit(0) + } else { + // The process supervisor should NOT crash/panic, unlike the workers. + // It's job, is to: + // 1) start workers + // 2) accumulate results + // 3) produce a summary at the end + context.expand_all_paths() + mut fails := 0 + mut panics := 0 + sw := time.new_stopwatch() + for path in context.all_paths { + filesw := time.new_stopwatch() + context.start_printing() + new_fails, new_panics := context.process_whole_file_in_worker(path) + fails += new_fails + panics += new_panics + context.stop_printing() + context.info('File: ${path:-30} | new_fails: ${new_fails:5} | new_panics: ${new_panics:5} | Elapsed time: ${filesw.elapsed().milliseconds()}ms') + } + non_panics := fails - panics + context.info('Total files processed: ${context.all_paths.len:5} | Errors found: ${fails:5} | Panics: ${panics:5} | Non panics: ${non_panics:5} | Elapsed time: ${sw.elapsed().milliseconds()}ms') + if fails > 0 { + exit(1) + } + exit(0) + } +} + +fn process_cli_args() &Context { + mut context := &Context{ + pref: pref.new_preferences() + } + context.myself = os.executable() + mut fp := flag.new_flag_parser(os.args_after('test-parser')) + fp.application(os.file_name(context.myself)) + fp.version('0.0.1') + fp.description('Test the V parser, by parsing each .v file in each PATH,\n' + + 'as if it was typed character by character by the user.\n' + + 'A PATH can be either a folder, or a specific .v file.\n' + + 'NB: you *have to quote* the PATH, if it contains spaces/punctuation.') + fp.arguments_description('PATH1 PATH2 ...') + fp.skip_executable() + context.is_help = fp.bool('help', `h`, false, 'Show help/usage screen.') + context.is_verbose = fp.bool('verbose', `v`, false, 'Be more verbose.') + context.is_silent = fp.bool('silent', `S`, false, 'Do not print progress at all.') + context.is_linear = fp.bool('linear', `L`, false, 'Print linear progress log. Suitable for CI.') + context.period_ms = fp.int('progress_ms', `s`, 500, 'print a status report periodically, the period is given in milliseconds.') + context.is_worker = fp.bool('worker', `w`, false, 'worker specific flag - is this a worker process, that can crash/panic.') + context.cut_index = fp.int('cut_index', `c`, 1, 'worker specific flag - cut index in the source file, everything before that will be parsed, the rest - ignored.') + context.timeout_ms = fp.int('timeout_ms', `t`, 250, 'worker specific flag - timeout in ms; a worker taking longer, will self terminate.') + context.path = fp.string('path', `p`, '', 'worker specific flag - path to the current source file, which will be parsed.') + // + if context.is_help { + println(fp.usage()) + exit(0) + } + context.all_paths = fp.finalize() or { + context.error(err.msg) + exit(1) + } + if !context.is_worker && context.all_paths.len == 0 { + println(fp.usage()) + exit(0) + } + return context +} + +// //////////////// +fn bold(msg string) string { + if !support_color { + return msg + } + return term.bold(msg) +} + +fn red(msg string) string { + if !support_color { + return msg + } + return term.red(msg) +} + +fn yellow(msg string) string { + if !support_color { + return msg + } + return term.yellow(msg) +} + +fn (mut context Context) info(msg string) { + println(msg) +} + +fn (mut context Context) log(msg string) { + if context.is_verbose { + label := yellow('info') + ts := time.now().format_ss_micro() + eprintln('$label: $ts | $msg') + } +} + +fn (mut context Context) error(msg string) { + label := red('error') + eprintln('$label: $msg') +} + +fn (mut context Context) expand_all_paths() { + context.log('> context.all_paths before: $context.all_paths') + mut files := []string{} + for path in context.all_paths { + if os.is_dir(path) { + files << os.walk_ext(path, '.v') + files << os.walk_ext(path, '.vsh') + continue + } + if !path.ends_with('.v') && !path.ends_with('.vv') && !path.ends_with('.vsh') { + context.error('`v test-parser` can only be used on .v/.vv/.vsh files.\nOffending file: "$path".') + continue + } + if !os.exists(path) { + context.error('"$path" does not exist.') + continue + } + files << path + } + context.all_paths = files + context.log('> context.all_paths after: $context.all_paths') +} + +fn (mut context Context) process_whole_file_in_worker(path string) (int, int) { + context.path = path // needed for the progress bar + context.log('> context.process_whole_file_in_worker path: $path') + if !(os.is_file(path) && os.is_readable(path)) { + context.error('$path is not readable') + return 1, 0 + } + source := os.read_file(path) or { '' } + if source == '' { + // an empty file is a valid .v file + return 0, 0 + } + len := source.len - 1 + mut fails := 0 + mut panics := 0 + context.max_index = len + for i in 0 .. len { + verbosity := if context.is_verbose { '-v' } else { '' } + context.cut_index = i // needed for the progress bar + cmd := '"$context.myself" $verbosity --worker --timeout_ms ${context.timeout_ms:5} --cut_index ${i:5} --path "$path" ' + context.log(cmd) + mut res := os.execute(cmd) + context.log('worker exit_code: $res.exit_code | worker output:\n$res.output') + if res.exit_code != 0 { + fails++ + mut is_panic := false + if res.output.contains('V panic:') { + is_panic = true + panics++ + } + part := source[..i] + line := part.count('\n') + 1 + last_line := part.all_after_last('\n') + col := last_line.len + err := if is_panic { + red('parser failure: panic') + } else { + red('parser failure: crash, ${ecode_details[res.exit_code]}') + } + path_to_line := bold('$path:$line:$col:') + err_line := last_line.trim_left('\t') + println('$path_to_line $err') + println('\t$line | $err_line') + println('') + eprintln(res.output) + } + } + return fails, panics +} + +fn (mut context Context) start_printing() { + context.stop_print = false + if !context.is_linear && !context.is_silent { + println('\n') + } + go context.print_periodic_status() +} + +fn (mut context Context) stop_printing() { + context.stop_print = true + time.sleep(time.millisecond * context.period_ms / 5) +} + +fn (mut context Context) print_status() { + if context.is_silent { + return + } + if (context.cut_index == 1) && (context.max_index == 0) { + return + } + msg := '> ${context.path:-30} | index: ${context.cut_index:5}/${context.max_index - 1:5}' + if context.is_linear { + eprintln(msg) + return + } + term.cursor_up(1) + eprint('\r $msg\n') +} + +fn (mut context Context) print_periodic_status() { + context.print_status() + mut printed_at_least_once := false + for !context.stop_print { + context.print_status() + for i := 0; i < 10 && !context.stop_print; i++ { + time.sleep(time.millisecond * context.period_ms / 10) + if context.cut_index > 50 && !printed_at_least_once { + context.print_status() + printed_at_least_once = true + } + } + } + context.print_status() +} |