Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,221 @@
|
||||
// amalgamate multiple C source files into a single
|
||||
// C source file. See https://sqlite.org/amalgamation.html
|
||||
// for a description of file amalgamation.
|
||||
//
|
||||
// If an input file is not specified, source is read
|
||||
// from stdin.
|
||||
//
|
||||
// If an output file is not specified, source is output
|
||||
// to stdout.
|
||||
|
||||
module main
|
||||
|
||||
import flag
|
||||
import os
|
||||
import regex
|
||||
|
||||
const app_name = 'amalgamate'
|
||||
const app_version = '0.0.1'
|
||||
|
||||
// pre-compile the include statement regex
|
||||
const re = regex.regex_opt(r'^\s*#\s*include\s*"([^"]+)"')!
|
||||
|
||||
struct Config {
|
||||
mut:
|
||||
input_files []string
|
||||
output_file string
|
||||
search_dirs []string
|
||||
blacklist []string
|
||||
}
|
||||
|
||||
struct Context {
|
||||
config Config
|
||||
mut:
|
||||
processed_files []string
|
||||
}
|
||||
|
||||
fn parse_arguments() Config {
|
||||
mut cfg := Config{}
|
||||
|
||||
mut parser := flag.new_flag_parser(os.args)
|
||||
parser.skip_executable()
|
||||
parser.application(app_name)
|
||||
parser.version(app_version)
|
||||
|
||||
parser.arguments_description('[file ...]')
|
||||
|
||||
parser.description('combine multiple .c and .h files into one.')
|
||||
parser.description('')
|
||||
parser.description('Combine input, coming from either stdin or input files, into one')
|
||||
parser.description('large file. Include statements are processed and the contents')
|
||||
parser.description('copied in place. Only #include "file.h" statements cause their')
|
||||
parser.description('contents to be copied, not #include <file.h> statements. If no')
|
||||
parser.description('input files are specified, read from stdin.')
|
||||
|
||||
parser.footer('\nAn example showing multiple blacklisted files and multiple search')
|
||||
parser.footer('directories.')
|
||||
parser.footer('')
|
||||
parser.footer(' amalgamate -o output_file.c -b ignore_me.h \\')
|
||||
parser.footer(' -b ignore_me_2.h -b other/ignore_me.h \\')
|
||||
parser.footer(' -s relative/search/dir -s /absolute/search/dir \\')
|
||||
parser.footer(' file1.c file2.c')
|
||||
parser.footer('')
|
||||
|
||||
cfg.output_file = parser.string('output', `o`, '', 'output file. If not specified,\n' +
|
||||
flag.space + 'defaults to stdout.\n', val_desc: '<filename>')
|
||||
|
||||
cfg.blacklist = parser.string_multi('blacklist', `b`,
|
||||
'blacklist a file name. This prevents\n' + flag.space +
|
||||
'the named file from being included.\n' + flag.space +
|
||||
'This can be specified more that once.\n', val_desc: '<include_file>')
|
||||
|
||||
cfg.search_dirs = parser.string_multi('search_path', `s`,
|
||||
'add a directory to the search path.\n' + flag.space +
|
||||
'An include file is searched for in\n' + flag.space +
|
||||
'the current working directory and\n' + flag.space +
|
||||
'if not found, the directories in this\n' + flag.space +
|
||||
'list are searched, in order, until the\n' + flag.space +
|
||||
'file is found or the search list is\n' + flag.space +
|
||||
'exhausted. This can be specified\n' + flag.space + 'more that once.\n',
|
||||
val_desc: '<search_dir>'
|
||||
)
|
||||
|
||||
cfg.input_files = parser.finalize() or {
|
||||
// this only reports the first unrecognized argument
|
||||
eprintln('${err}\n')
|
||||
eprintln('${parser.usage()}\n')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
fn main() {
|
||||
cfg := parse_arguments()
|
||||
|
||||
mut ctx := Context{
|
||||
config: cfg
|
||||
}
|
||||
|
||||
ctx.amalgamate() or {
|
||||
eprintln('error: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Context) amalgamate() ! {
|
||||
mut source := ''
|
||||
|
||||
if c.config.input_files.len == 0 {
|
||||
// source += '/* ########## stdin */\n'
|
||||
// if there are no input files, read from stdin
|
||||
local_dir := os.getwd()
|
||||
source += c.handle_includes(local_dir, os.get_raw_lines_joined())!
|
||||
// source += '/* ########## stdin end */\n'
|
||||
} else {
|
||||
// read each input file, in order, and
|
||||
// handle all of its includes.
|
||||
for file in c.config.input_files {
|
||||
if file in c.config.blacklist {
|
||||
// skip blacklisted files
|
||||
continue
|
||||
}
|
||||
|
||||
found_file := c.find_file(file)!
|
||||
|
||||
if found_file in c.processed_files {
|
||||
// skip over files already read
|
||||
continue
|
||||
}
|
||||
|
||||
// source += '/* ########## ${file} */\n'
|
||||
c.processed_files << found_file
|
||||
local_dir := os.dir(found_file)
|
||||
file_source_code := os.read_file(found_file)!
|
||||
source += c.handle_includes(local_dir, file_source_code)!
|
||||
// source += '/* ########## ${file} end */\n'
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.output_file == '' {
|
||||
print(source)
|
||||
} else {
|
||||
os.write_file(c.config.output_file, source)!
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fn (c Context) find_file(file string) !string {
|
||||
mut full_path := os.real_path(file)
|
||||
|
||||
if os.is_file(full_path) {
|
||||
return full_path
|
||||
}
|
||||
|
||||
for dir in c.config.search_dirs {
|
||||
full_path = os.real_path(os.join_path_single(dir, file))
|
||||
|
||||
if os.is_file(full_path) {
|
||||
return full_path
|
||||
}
|
||||
}
|
||||
|
||||
return error('file "${file}" not found')
|
||||
}
|
||||
|
||||
// handle_includes looks for lines that start with #include
|
||||
// and inserts the lines from the named include file.
|
||||
//
|
||||
// The pattern matches file names for local header files,
|
||||
// not system header files as are denoted by < and >.
|
||||
fn (mut c Context) handle_includes(local_dir string, input_source string) !string {
|
||||
source_lines := input_source.split_into_lines()
|
||||
mut output_lines := []string{}
|
||||
|
||||
for line in source_lines {
|
||||
start, _ := re.match_string(line)
|
||||
|
||||
if start >= 0 {
|
||||
file := line[re.groups[0]..re.groups[1]]
|
||||
mut found_file := ''
|
||||
|
||||
if file in c.config.blacklist {
|
||||
// leave blacklisted files alone
|
||||
if file in c.processed_files {
|
||||
// we don't want a second include
|
||||
output_lines << '\n'
|
||||
} else {
|
||||
output_lines << line
|
||||
c.processed_files << file
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !os.is_abs_path(file) {
|
||||
found_file = c.find_file(os.join_path_single(local_dir, file)) or {
|
||||
// keep looking
|
||||
''
|
||||
}
|
||||
}
|
||||
|
||||
if found_file == '' {
|
||||
found_file = c.find_file(file)!
|
||||
}
|
||||
|
||||
if found_file in c.processed_files {
|
||||
// skip over files already read
|
||||
continue
|
||||
}
|
||||
c.processed_files << found_file
|
||||
file_source_code := os.read_file(found_file)!
|
||||
// output_lines << '/* ########## ${file} begin */\n'
|
||||
output_lines << c.handle_includes(os.dir(found_file), file_source_code)!
|
||||
// output_lines << '/* ########## ${file} end */\n'
|
||||
} else {
|
||||
output_lines << line
|
||||
}
|
||||
}
|
||||
|
||||
return output_lines.join_lines() + '\n'
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import benchmark
|
||||
|
||||
fn main() {
|
||||
max_iterations := arguments()[1] or { '1_000_000' }.int()
|
||||
assert max_iterations > 0
|
||||
mut m := {
|
||||
123: 456
|
||||
789: 321
|
||||
}
|
||||
mut volatile sum := u64(0)
|
||||
mut b := benchmark.start()
|
||||
for i in 0 .. max_iterations {
|
||||
m.clear()
|
||||
m[i] = i * 2
|
||||
sum += u64(m.len)
|
||||
}
|
||||
assert m.len == 1
|
||||
b.measure('m.clear(), iterations: ${max_iterations}, sum: ${sum}')
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp
|
||||
|
||||
import os
|
||||
|
||||
const time_fmt = '"CPU: %Us\tReal: %es\tElapsed: %E\tRAM: %MKB\t%C"'
|
||||
const flags = os.getenv('FLAGS')
|
||||
|
||||
unbuffer_stdout()
|
||||
|
||||
start := os.args[1] or { '1_000_000' }.int()
|
||||
end := os.args[2] or { '10_000_000' }.int()
|
||||
step := os.args[3] or { '500_000' }.int()
|
||||
|
||||
os.chdir(os.dir(@VEXE))!
|
||||
vcmd := 'v ${flags} cmd/tools/bench/map_clear.v'
|
||||
|
||||
println('>> start: ${start} | end: ${end} | step: ${step} | workdir: "${os.getwd()}" | flags: "${flags}" | vcmd: "${vcmd}"')
|
||||
assert os.system(vcmd) == 0
|
||||
|
||||
println('running...')
|
||||
for i := start; i <= end; i += step {
|
||||
os.system('/usr/bin/time -f ${time_fmt} cmd/tools/bench/map_clear ${i}') == 0
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
module main
|
||||
|
||||
import hash.fnv1a
|
||||
import hash as wyhash
|
||||
import rand
|
||||
import benchmark
|
||||
|
||||
fn main() {
|
||||
rand.seed([u32(42), 0])
|
||||
sample_size := 10000000
|
||||
min_str_len := 20
|
||||
max_str_len := 40
|
||||
println('Generating ${sample_size} strings between ${min_str_len} - ${max_str_len} chars long...')
|
||||
mut checksum := u64(0)
|
||||
mut start_pos := 0
|
||||
mut bgenerating := benchmark.start()
|
||||
mut bytepile := []u8{}
|
||||
for _ in 0 .. sample_size * max_str_len {
|
||||
bytepile << u8(rand.int_in_range(40, 125) or { 40 })
|
||||
}
|
||||
mut str_lens := []int{}
|
||||
for _ in 0 .. sample_size {
|
||||
str_lens << rand.int_in_range(min_str_len, max_str_len) or { min_str_len }
|
||||
}
|
||||
bgenerating.measure('generating strings')
|
||||
println('Hashing each of the generated strings...')
|
||||
|
||||
mut bhashing_1 := benchmark.start()
|
||||
start_pos = 0
|
||||
checksum = 0
|
||||
for len in str_lens {
|
||||
end_pos := start_pos + len
|
||||
checksum ^= wyhash.wyhash_c(unsafe { &u8(bytepile.data) + start_pos }, u64(len), 1)
|
||||
start_pos = end_pos
|
||||
}
|
||||
bhashing_1.measure('wyhash.wyhash_c | checksum: ${checksum:22}')
|
||||
mut bhashing_2 := benchmark.start()
|
||||
start_pos = 0
|
||||
checksum = 0
|
||||
for len in str_lens {
|
||||
end_pos := start_pos + len
|
||||
checksum ^= wyhash.sum64(bytepile[start_pos..end_pos], 1)
|
||||
start_pos = end_pos
|
||||
}
|
||||
bhashing_2.measure('wyhash.sum64 | checksum: ${checksum:22}')
|
||||
mut bhashing_3 := benchmark.start()
|
||||
start_pos = 0
|
||||
checksum = 0
|
||||
for len in str_lens {
|
||||
end_pos := start_pos + len
|
||||
checksum ^= fnv1a.sum64(bytepile[start_pos..end_pos])
|
||||
start_pos = end_pos
|
||||
}
|
||||
bhashing_3.measure('fnv1a.sum64 | checksum: ${checksum:22}')
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
module main
|
||||
|
||||
import v.builder.cbuilder
|
||||
|
||||
// TODO: change bootstrapping to use the C code generated from
|
||||
// `VEXE=v cmd/tools/builders/c_builder -os cross -o c.c cmd/tools/builders/c_builder.v`
|
||||
// See also `cmd/v/v.v`
|
||||
|
||||
fn main() {
|
||||
cbuilder.start()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module main
|
||||
|
||||
import v.builder.jsbuilder
|
||||
|
||||
fn main() {
|
||||
jsbuilder.start()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module main
|
||||
|
||||
import v.builder.wasmbuilder
|
||||
|
||||
fn main() {
|
||||
wasmbuilder.start()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
## this script is used by the makefiles in the top level folder
|
||||
echo
|
||||
echo 'Compilation of v.c failed.'
|
||||
echo 'See https://github.com/vlang/v/wiki/Installing-a-C-compiler-on-Linux-and-macOS .'
|
||||
echo
|
||||
false
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
## this script is used by the makefiles in the top level folder
|
||||
echo
|
||||
echo 'Compilation of v.c failed.'
|
||||
echo 'See https://github.com/vlang/v/wiki/Installing-a-C-compiler-on-Windows .'
|
||||
echo
|
||||
false
|
||||
@@ -0,0 +1,594 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
|
||||
const delete_skipped = true
|
||||
|
||||
const git_log_cmd = 'git log -n 500 --pretty=format:"%s" --simplify-merges'
|
||||
|
||||
enum Category {
|
||||
checker
|
||||
breaking
|
||||
improvements
|
||||
parser
|
||||
stdlib
|
||||
web
|
||||
orm
|
||||
db
|
||||
cgen
|
||||
js_backend
|
||||
comptime
|
||||
tools
|
||||
compiler_internals
|
||||
examples
|
||||
vfmt
|
||||
os_support
|
||||
interpreter
|
||||
}
|
||||
|
||||
const category_titles = '#### Improvements in the language
|
||||
|
||||
#### V interpreter
|
||||
|
||||
#### Breaking changes
|
||||
|
||||
#### Checker improvements/fixes
|
||||
|
||||
#### Parser improvements
|
||||
|
||||
#### Compiler internals
|
||||
|
||||
#### Standard library
|
||||
|
||||
#### Web
|
||||
|
||||
#### ORM
|
||||
|
||||
#### Database drivers
|
||||
|
||||
#### C backend
|
||||
|
||||
#### JavaScript backend
|
||||
|
||||
#### vfmt
|
||||
|
||||
#### Tools
|
||||
|
||||
#### Operating System support
|
||||
|
||||
#### Examples
|
||||
'
|
||||
|
||||
struct Line {
|
||||
category Category
|
||||
text string
|
||||
}
|
||||
|
||||
const log_txt = 'log.txt'
|
||||
|
||||
struct App {
|
||||
version string // e.g. "0.4.5"
|
||||
total_lines int
|
||||
mut:
|
||||
result string // resulting CHANGELOG.md
|
||||
counter int
|
||||
}
|
||||
|
||||
const is_interactive = false
|
||||
|
||||
fn main() {
|
||||
mut version := ''
|
||||
|
||||
if os.args.len == 2 && os.args[1].starts_with('0.') {
|
||||
version = os.args[1]
|
||||
} else {
|
||||
println('Usage: v run tools/changelog_helper.v 0.4.5')
|
||||
return
|
||||
}
|
||||
if !os.exists(log_txt) {
|
||||
os.execute(git_log_cmd + ' > ' + log_txt)
|
||||
println('log.txt generated')
|
||||
// println('log.txt generated, remove unnecessary commits from it and run the tool again')
|
||||
// return
|
||||
}
|
||||
mut lines := os.read_lines(log_txt)!
|
||||
// Trim everything before current version, commit "(tag: 0.4.4) V 0.4.4"
|
||||
mut prev_version := get_prev_version(version)
|
||||
println('prev version=${prev_version}')
|
||||
for i, line in lines {
|
||||
if line == ('V ${prev_version}') {
|
||||
lines = lines[..i].clone()
|
||||
break
|
||||
}
|
||||
}
|
||||
os.write_file(log_txt, lines.join('\n'))!
|
||||
mut app := &App{
|
||||
total_lines: lines.len
|
||||
}
|
||||
// Write categories at the top first
|
||||
app.result = os.read_file('CHANGELOG.md')!.replace_once('V ${version} TODO', 'V ${version}\n' +
|
||||
category_titles)
|
||||
os.write_file('CHANGELOG.md', app.result)!
|
||||
changelog_txt := os.read_file('CHANGELOG.md')!.to_lower()
|
||||
// mut counter := 0 // to display how many commits are left
|
||||
for line in lines {
|
||||
s := line.trim_space()
|
||||
if s == '' {
|
||||
app.counter++
|
||||
}
|
||||
}
|
||||
// println('${counter} / ${lines.len}')
|
||||
for line in lines {
|
||||
s := line.to_lower()
|
||||
if line != '' && (changelog_txt.contains(s) || changelog_txt.contains(s.after(':'))) {
|
||||
println('Duplicate: "${line}"')
|
||||
// skip duplicate
|
||||
delete_processed_line_from_log(line)!
|
||||
continue
|
||||
}
|
||||
|
||||
app.process_line(line.trim_space())!
|
||||
}
|
||||
println('writing changelog.md')
|
||||
if !is_interactive {
|
||||
os.write_file('CHANGELOG.md', app.result)!
|
||||
}
|
||||
println('done.')
|
||||
}
|
||||
|
||||
fn (mut app App) process_line(text string) ! {
|
||||
if text == '' {
|
||||
return
|
||||
}
|
||||
semicolon_pos := text.index(': ') or {
|
||||
println('no `:` in commit, skipping: "${text}"')
|
||||
return
|
||||
}
|
||||
prefix := text[..semicolon_pos]
|
||||
// Get category based on keywords in the commit message/prefix
|
||||
mut category := Category.examples
|
||||
if text.contains('checker:') {
|
||||
category = .checker
|
||||
} else if is_interpreter(text) {
|
||||
category = .interpreter
|
||||
} else if is_examples(text) {
|
||||
category = .examples
|
||||
// println("Skipping line (example) ${text}")
|
||||
// return
|
||||
} else if is_skip(text) {
|
||||
// Always skip cleanups, typos etc
|
||||
println('Skipping line (cleanup/typo)\n${text}\n')
|
||||
if delete_skipped {
|
||||
delete_processed_line_from_log(text)!
|
||||
}
|
||||
return
|
||||
} else if is_os_support(text) {
|
||||
category = .os_support
|
||||
} else if is_cgen(text) {
|
||||
category = .cgen
|
||||
} else if is_js_backend(text) {
|
||||
category = .js_backend
|
||||
} else if is_comptime(text) {
|
||||
category = .comptime
|
||||
} else if is_db(text) {
|
||||
category = .db
|
||||
} else if is_stdlib(text) {
|
||||
category = .stdlib
|
||||
} else if is_orm(text) {
|
||||
category = .orm
|
||||
} else if is_web(text) {
|
||||
category = .web
|
||||
} else if is_tools(text) {
|
||||
category = .tools
|
||||
} else if is_parser(text) {
|
||||
category = .parser
|
||||
} else if is_internal(text) {
|
||||
category = .compiler_internals
|
||||
} else if is_improvements(text) {
|
||||
category = .improvements
|
||||
} else if is_vfmt(text) {
|
||||
category = .vfmt
|
||||
} else if text.contains('docs:') || text.contains('doc:') {
|
||||
// Always skip docs
|
||||
delete_processed_line_from_log(text)!
|
||||
return
|
||||
} else {
|
||||
println('Skipping line (unknown category)\n${text}\n')
|
||||
// if delete_skipped {
|
||||
// delete_processed_line_from_log(text)!
|
||||
//}
|
||||
return
|
||||
}
|
||||
println('process_line: cat=${category} "${text}"')
|
||||
|
||||
// Trim everything to the left of `:` for some commits (e.g. `checker: `)
|
||||
mut s := text
|
||||
// println("PREFIX='${prefix}'")
|
||||
// if true {
|
||||
// exit(0)
|
||||
//}
|
||||
if (semicolon_pos < 15
|
||||
&& prefix in ['checker', 'cgen', 'orm', 'parser', 'v.parser', 'native', 'ast', 'jsgen', 'v.gen.js', 'fmt', 'vfmt', 'tools', 'examples', 'eval'])
|
||||
|| (semicolon_pos < 30 && prefix.contains(', ')) {
|
||||
s = '- ' + text[semicolon_pos + 2..].capitalize()
|
||||
}
|
||||
|
||||
if is_interactive {
|
||||
// Get input from the user
|
||||
print('\033[H\033[J')
|
||||
println('${app.counter} / ${app.total_lines}')
|
||||
// println('\n')
|
||||
println(text)
|
||||
input := os.input('${category}? ')
|
||||
println("INPUT='${input}'")
|
||||
match input {
|
||||
'' {
|
||||
println('GOT ENTER')
|
||||
line := Line{category, s}
|
||||
save_line_interactive(line)!
|
||||
}
|
||||
'n', '0', 'no' {
|
||||
// Ignore commit
|
||||
println('ignored.')
|
||||
}
|
||||
's', 'skip' {
|
||||
// Skip
|
||||
println('skipped.')
|
||||
return
|
||||
}
|
||||
'c', 'change' {
|
||||
// Change category
|
||||
for {
|
||||
print_category_hint()
|
||||
custom_category := os.input('${category} ?').int()
|
||||
if custom_category == 0 {
|
||||
println('wrong category')
|
||||
} else {
|
||||
unsafe {
|
||||
line := Line{Category(custom_category - 1), s}
|
||||
save_line_interactive(line)!
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
|
||||
app.counter++
|
||||
} else {
|
||||
line := Line{category, s}
|
||||
app.save_line(line)!
|
||||
}
|
||||
// Don't forget to remove the line we just processed from log.txt
|
||||
delete_processed_line_from_log(text)!
|
||||
}
|
||||
|
||||
fn (mut app App) save_line(line Line) ! {
|
||||
// println('save line ${line}')
|
||||
app.result = line.write_at_category(app.result) or { return error('') }
|
||||
}
|
||||
|
||||
fn save_line_interactive(line Line) ! {
|
||||
println('save line interactive ${line}')
|
||||
mut txt := os.read_file('CHANGELOG.md')!
|
||||
txt = line.write_at_category(txt) or { return error('') }
|
||||
os.write_file('CHANGELOG.md', txt)!
|
||||
}
|
||||
|
||||
const category_map = {
|
||||
Category.checker: '#### Checker improvements'
|
||||
.breaking: '#### Breaking changes'
|
||||
.improvements: '#### Improvements in the'
|
||||
.interpreter: '#### V interpreter'
|
||||
.parser: '#### Parser improvements'
|
||||
.stdlib: '#### Standard library'
|
||||
.web: '#### Web'
|
||||
.orm: '#### ORM'
|
||||
.db: '#### Database drivers'
|
||||
.cgen: '#### C backend'
|
||||
.js_backend: '#### JavaScript backend'
|
||||
.comptime: '#### Comptime'
|
||||
.tools: '#### Tools'
|
||||
.compiler_internals: '#### Compiler internals'
|
||||
.examples: '#### Examples'
|
||||
.vfmt: '#### vfmt'
|
||||
.os_support: '#### Operating System'
|
||||
}
|
||||
|
||||
fn (l Line) write_at_category(txt string) ?string {
|
||||
title := category_map[l.category]
|
||||
title_pos := txt.index(title)?
|
||||
// Find the position of the ### category title
|
||||
pos := txt.index_after('\n', title_pos + 1) or { return none }
|
||||
first_half := txt[..pos]
|
||||
second_half := txt[pos..]
|
||||
if txt.contains(l.text) {
|
||||
// Avoid duplicates (just in case)
|
||||
println("Got a duplicate: '${txt}'")
|
||||
return txt
|
||||
}
|
||||
// Now insert the line in the middle, under the ### category title
|
||||
mut line_text := l.text
|
||||
|
||||
// Trim "prefix:" for some categories
|
||||
// mut capitalized := false
|
||||
mut has_prefix := true
|
||||
if l.category in [.cgen, .checker, .improvements, .orm, .interpreter] {
|
||||
has_prefix = false
|
||||
if semicolon_pos := line_text.index(': ') {
|
||||
prefix := line_text[..semicolon_pos]
|
||||
println("PREFIX='${prefix}'")
|
||||
if semicolon_pos < 15 {
|
||||
line_text = line_text[semicolon_pos + 2..].capitalize()
|
||||
// capitalized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !has_prefix {
|
||||
line_text = line_text.capitalize()
|
||||
}
|
||||
if !line_text.starts_with('- ') {
|
||||
line_text = '- ' + line_text
|
||||
}
|
||||
return first_half + '\n' + line_text + second_half
|
||||
}
|
||||
|
||||
fn delete_processed_line_from_log(line string) ! {
|
||||
text := os.read_file(log_txt)!
|
||||
new_text := text.replace_once(line, '')
|
||||
os.write_file(log_txt, new_text)!
|
||||
}
|
||||
|
||||
const db_strings = [
|
||||
'db:',
|
||||
'db.sqlite',
|
||||
'db.mysql',
|
||||
'db.pg',
|
||||
'db.redis',
|
||||
'pg:',
|
||||
'mysql:',
|
||||
]
|
||||
|
||||
const parser_strings = [
|
||||
'parser:',
|
||||
'ast:',
|
||||
]
|
||||
|
||||
const stdlib_strings = [
|
||||
'gg:',
|
||||
'json:',
|
||||
'json2:',
|
||||
'time:',
|
||||
'sync:',
|
||||
'datatypes:',
|
||||
'math:',
|
||||
'math.',
|
||||
'math.big',
|
||||
'crypto',
|
||||
'sokol',
|
||||
'os:',
|
||||
'rand:',
|
||||
'rand.',
|
||||
'math:',
|
||||
'toml:',
|
||||
'vlib:',
|
||||
'arrays:',
|
||||
'os.',
|
||||
'term:',
|
||||
'sync.',
|
||||
'builtin:',
|
||||
'builtin,',
|
||||
'builtin.',
|
||||
'strconv',
|
||||
'readline',
|
||||
'cli:',
|
||||
'eventbus:',
|
||||
'encoding.',
|
||||
'bitfield:',
|
||||
'io:',
|
||||
'io.',
|
||||
'log:',
|
||||
'flag:',
|
||||
'regex:',
|
||||
'regex.',
|
||||
'tmpl:',
|
||||
'hash:',
|
||||
'stbi:',
|
||||
'atomic:',
|
||||
'context:',
|
||||
'thirdparty',
|
||||
]
|
||||
|
||||
fn is_stdlib(text string) bool {
|
||||
return is_xxx(text, stdlib_strings)
|
||||
}
|
||||
|
||||
fn is_db(text string) bool {
|
||||
return is_xxx(text, db_strings)
|
||||
}
|
||||
|
||||
const orm_strings = [
|
||||
'orm:',
|
||||
]
|
||||
|
||||
fn is_orm(text string) bool {
|
||||
return is_xxx(text, orm_strings)
|
||||
}
|
||||
|
||||
const cgen_strings = [
|
||||
'cgen:',
|
||||
'cgen,',
|
||||
'v.gen.c:',
|
||||
]
|
||||
|
||||
fn is_cgen(text string) bool {
|
||||
return is_xxx(text, cgen_strings)
|
||||
}
|
||||
|
||||
const js_backend_strings = [
|
||||
'js:',
|
||||
'v.gen.js:',
|
||||
'jsgen:',
|
||||
]
|
||||
|
||||
fn is_js_backend(text string) bool {
|
||||
return is_xxx(text, js_backend_strings)
|
||||
}
|
||||
|
||||
const internal_strings = [
|
||||
'scanner:',
|
||||
'transformer:',
|
||||
'markused:',
|
||||
'builder:',
|
||||
'pref:',
|
||||
'v.util',
|
||||
'v.generic',
|
||||
'v.comptime',
|
||||
'table:',
|
||||
]
|
||||
|
||||
fn is_internal(text string) bool {
|
||||
return is_xxx(text, internal_strings)
|
||||
}
|
||||
|
||||
const improvements_strings = [
|
||||
'all:',
|
||||
'v:',
|
||||
'coroutines:',
|
||||
'autofree',
|
||||
]
|
||||
|
||||
fn is_improvements(text string) bool {
|
||||
return is_xxx(text, improvements_strings)
|
||||
}
|
||||
|
||||
const examples_strings = [
|
||||
'example',
|
||||
]
|
||||
const skip_strings = [
|
||||
'tests',
|
||||
'readme:',
|
||||
'.md:',
|
||||
'typos',
|
||||
' typo',
|
||||
'cleanup',
|
||||
'clean up',
|
||||
'build(deps)',
|
||||
'FUNDING',
|
||||
]
|
||||
|
||||
fn is_examples(text string) bool {
|
||||
return is_xxx(text, examples_strings)
|
||||
}
|
||||
|
||||
fn is_skip(text string) bool {
|
||||
return is_xxx(text, skip_strings)
|
||||
}
|
||||
|
||||
const tools_strings = [
|
||||
'tools:',
|
||||
'vpm:',
|
||||
'ci:',
|
||||
'github:',
|
||||
'gitignore',
|
||||
'benchmark',
|
||||
'v.help:',
|
||||
'vtest',
|
||||
'repl',
|
||||
'REPL',
|
||||
'vet',
|
||||
'tools.',
|
||||
'GNUmakefile',
|
||||
'Dockerfile',
|
||||
'vcomplete',
|
||||
'vwatch',
|
||||
'changelog',
|
||||
]
|
||||
|
||||
fn is_tools(text string) bool {
|
||||
return is_xxx(text, tools_strings)
|
||||
}
|
||||
|
||||
fn is_parser(text string) bool {
|
||||
return is_xxx(text, parser_strings)
|
||||
}
|
||||
|
||||
const web_strings = [
|
||||
'veb',
|
||||
'websocket:',
|
||||
'pico',
|
||||
'x.sessions',
|
||||
'picoev:',
|
||||
'mbedtls',
|
||||
'net:',
|
||||
'net.',
|
||||
'wasm:',
|
||||
'http:',
|
||||
]
|
||||
|
||||
fn is_web(text string) bool {
|
||||
return is_xxx(text, web_strings)
|
||||
}
|
||||
|
||||
const vfmt_strings = [
|
||||
'vfmt:',
|
||||
'fmt:',
|
||||
]
|
||||
|
||||
fn is_vfmt(text string) bool {
|
||||
return is_xxx(text, vfmt_strings)
|
||||
}
|
||||
|
||||
const os_support_strings = [
|
||||
'FreeBSD',
|
||||
'freebsd',
|
||||
'OpenBSD',
|
||||
'openbsd',
|
||||
'macOS',
|
||||
'macos',
|
||||
'Windows',
|
||||
'windows',
|
||||
'Linux',
|
||||
'linux',
|
||||
'msvc:',
|
||||
]
|
||||
|
||||
fn is_os_support(text string) bool {
|
||||
return is_xxx(text, os_support_strings)
|
||||
}
|
||||
|
||||
fn is_comptime(text string) bool {
|
||||
return text.contains('comptime:')
|
||||
}
|
||||
|
||||
fn is_interpreter(text string) bool {
|
||||
return text.starts_with('eval:')
|
||||
}
|
||||
|
||||
fn is_xxx(text string, words []string) bool {
|
||||
for s in words {
|
||||
if text.contains(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn print_category_hint() {
|
||||
$for val in Category.values {
|
||||
println('${int(val.value) + 1} - ${val.name}; ')
|
||||
}
|
||||
}
|
||||
|
||||
// For 0.4.12 returns 0.4.11 etc
|
||||
fn get_prev_version(version string) string {
|
||||
parts := version.split('.')
|
||||
if parts.len != 3 {
|
||||
return ''
|
||||
}
|
||||
major := parts[0]
|
||||
minor := parts[1]
|
||||
patch := parts[2].int()
|
||||
return '${major}.${minor}.${patch - 1}'
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import v.util
|
||||
import v.util.diff
|
||||
import v.pref
|
||||
import v.builder
|
||||
import v.builder.cbuilder
|
||||
import v.ast
|
||||
import term
|
||||
|
||||
const base_os = pref.get_host_os()
|
||||
const os_list = [pref.OS.linux, .macos, .windows, .freebsd, .openbsd, .solaris, .termux]
|
||||
const skip_modules = [
|
||||
'builtin.bare',
|
||||
'builtin.linux_bare.old',
|
||||
'builtin.js',
|
||||
'builtin.wasm',
|
||||
'strconv',
|
||||
'strconv.ftoa',
|
||||
'hash',
|
||||
'strings',
|
||||
'crypto.rand',
|
||||
'os.bare',
|
||||
'os2',
|
||||
'szip',
|
||||
]
|
||||
const is_verbose = os.getenv('VERBOSE') != ''
|
||||
|
||||
fn main() {
|
||||
vexe := os.real_path(os.getenv_opt('VEXE') or { @VEXE })
|
||||
vroot := os.dir(vexe)
|
||||
util.set_vroot_folder(vroot)
|
||||
os.chdir(vroot)!
|
||||
modules := if os.args.len > 1 { os.args[1..] } else { all_vlib_modules() }
|
||||
mut diff_modules := map[string]bool{}
|
||||
other_os_list := os_list.filter(it != base_os)
|
||||
for m in modules {
|
||||
if !is_verbose {
|
||||
eprintln('Checking module: ${m} ...')
|
||||
}
|
||||
api_base := gen_api_for_module_in_os(m, base_os)
|
||||
for other_os in other_os_list {
|
||||
api_os := gen_api_for_module_in_os(m, other_os)
|
||||
if api_base == api_os {
|
||||
continue
|
||||
}
|
||||
diff_modules[m] = true
|
||||
summary := 'Different APIs found for module: `${m}`, between OS base: `${base_os}` and OS: `${other_os}`'
|
||||
eprintln(term.header(summary, '-'))
|
||||
diff_ := diff.compare_text(api_base, api_os) or { continue }
|
||||
println(diff_)
|
||||
eprintln(term.h_divider('-'))
|
||||
}
|
||||
}
|
||||
if diff_modules.len > 0 {
|
||||
eprintln(term.header('Found ${diff_modules.len} modules with different APIs', '='))
|
||||
for m in diff_modules.keys() {
|
||||
eprintln('Module: ${m}')
|
||||
}
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn all_vlib_modules() []string {
|
||||
mut vlib_v_files := os.walk_ext('vlib', '.v')
|
||||
mut vmodulesmap := map[string]int{}
|
||||
for f in vlib_v_files {
|
||||
if f.contains('/tests/') || f.ends_with('_test.v') {
|
||||
continue
|
||||
}
|
||||
vmodulename := os.dir(f).replace('/', '.').replace('vlib.', '')
|
||||
if vmodulename in skip_modules {
|
||||
continue
|
||||
}
|
||||
vmodulesmap[vmodulename] = vmodulesmap[vmodulename] + 1
|
||||
}
|
||||
mut modules := vmodulesmap.keys()
|
||||
modules.sort()
|
||||
return modules
|
||||
}
|
||||
|
||||
fn gen_api_for_module_in_os(mod_name string, os_ pref.OS) string {
|
||||
if is_verbose {
|
||||
eprintln('Checking module: ${mod_name:-30} for OS: ${os_:-10} ...')
|
||||
}
|
||||
os_name := os_.str().to_lower()
|
||||
mpath := os.join_path('vlib', mod_name.replace('.', '/'))
|
||||
tmpname := '/tmp/${mod_name}_${os_name}.c'
|
||||
prefs, _ := pref.parse_args([], ['-os', os_name, '-o', tmpname, '-shared', mpath])
|
||||
mut b := builder.new_builder(prefs)
|
||||
cbuilder.compile_c(mut b)
|
||||
mut res := []string{}
|
||||
for f in b.parsed_files {
|
||||
for s in f.stmts {
|
||||
if s is ast.FnDecl && s.is_pub {
|
||||
fn_mod := s.modname()
|
||||
if fn_mod == mod_name {
|
||||
fn_signature := b.table.stringify_fn_decl(&s, mod_name, map[string]string{},
|
||||
false)
|
||||
fline := '${fn_mod}: ${fn_signature}'
|
||||
res << fline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res.sort()
|
||||
return res.join('\n')
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp
|
||||
|
||||
// This script is used by cmd/tools/vretry_test.v, to check that the `v retry`
|
||||
// subcommand works as expected, without relying on external commands like
|
||||
// `git`, or on non portable ones like `true`/`false` or `echo`.
|
||||
|
||||
fn main() {
|
||||
args := arguments()#[1..]
|
||||
println(args)
|
||||
if args == ['too', 'many', 'arguments'] {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
fn main() {
|
||||
$if tinyc {
|
||||
println('Your `tcc` is working. Good - it is much faster at compiling C source code.')
|
||||
exit(0)
|
||||
}
|
||||
|
||||
$if !macos {
|
||||
println('
|
||||
Note: `tcc` was not used, so unless you install it yourself, your backend
|
||||
C compiler will be `cc`, which is usually either `clang`, `gcc` or `msvc`.
|
||||
|
||||
These C compilers, are several times slower at compiling C source code,
|
||||
compared to `tcc`. They do produce more optimised executables, but that
|
||||
is done at the cost of compilation speed.
|
||||
')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fast
|
||||
index.html
|
||||
table.html
|
||||
v.c
|
||||
v2
|
||||
fast.log
|
||||
|
||||
website/
|
||||
|
||||
fast.vlang.io/
|
||||
docs.vlang.io/
|
||||
@@ -0,0 +1,332 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
import os
|
||||
import time
|
||||
import arrays
|
||||
import log
|
||||
|
||||
const args = arguments()
|
||||
const warmup_samples = 2
|
||||
|
||||
const max_samples = 20
|
||||
|
||||
const discard_highest_samples = 16
|
||||
|
||||
const voptions = ' -skip-unused -show-timings -stats '
|
||||
|
||||
const fast_dir = os.real_path(os.dir(@FILE))
|
||||
|
||||
const fast_log_path = os.real_path(os.join_path(fast_dir, 'fast.log'))
|
||||
|
||||
const vdir = os.real_path(os.dir(os.dir(os.dir(fast_dir))))
|
||||
|
||||
fn elog(msg string) {
|
||||
line := '${time.now().format_ss_micro()} ${msg}\n'
|
||||
if mut f := os.open_append(fast_log_path) {
|
||||
f.write_string(line) or {}
|
||||
f.close()
|
||||
}
|
||||
log.info(msg)
|
||||
}
|
||||
|
||||
fn lsystem(cmd string) int {
|
||||
elog('lsystem: ${cmd}')
|
||||
return os.system(cmd)
|
||||
}
|
||||
|
||||
fn lexec(cmd string) string {
|
||||
elog(' lexec: ${cmd}')
|
||||
res := os.execute(cmd)
|
||||
if res.exit_code != 0 {
|
||||
elog(' lexec FAILED, exit_code: ${res.exit_code}, output:\n${res.output}')
|
||||
}
|
||||
return res.output.trim_right('\r\n')
|
||||
}
|
||||
|
||||
// lexec_check runs cmd, logs its exit code and output, and returns true on success.
|
||||
// Use it for critical steps where a failure should short-circuit the rest of a sequence,
|
||||
// e.g. when a failed `git pull` would otherwise leave the docs.vlang.io repo in a state
|
||||
// where the subsequent `git push` is silently a no-op.
|
||||
fn lexec_check(cmd string) bool {
|
||||
elog(' lexec_check: ${cmd}')
|
||||
res := os.execute(cmd)
|
||||
if res.exit_code != 0 {
|
||||
elog(' lexec_check FAILED, exit_code: ${res.exit_code}, output:\n${res.output}')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// ensure all log messages will be visible to the observers, even if the program panics
|
||||
log.use_stdout()
|
||||
log.set_always_flush(true)
|
||||
|
||||
total_sw := time.new_stopwatch()
|
||||
elog('fast.html generator start')
|
||||
defer {
|
||||
elog('fast.html generator end, total: ${total_sw.elapsed().milliseconds():6} ms')
|
||||
}
|
||||
|
||||
mut ccompiler_path := 'tcc'
|
||||
if vdir.contains('/tmp/cirrus-ci-build') {
|
||||
ccompiler_path = 'clang'
|
||||
}
|
||||
if args.contains('-clang') {
|
||||
ccompiler_path = 'clang'
|
||||
}
|
||||
elog('fast_dir: ${fast_dir} | vdir: ${vdir} | compiler: ${ccompiler_path}')
|
||||
|
||||
os.chdir(fast_dir)!
|
||||
if !os.exists('${vdir}/v') && !os.is_dir('${vdir}/vlib') {
|
||||
elog('fast.html generator needs to be located in `v/cmd/tools/fast`')
|
||||
exit(1)
|
||||
}
|
||||
if !os.exists('table.html') {
|
||||
os.create('table.html')!
|
||||
}
|
||||
|
||||
if !args.contains('-noupdate') {
|
||||
elog('Fetching updates...')
|
||||
ret := lsystem('${vdir}/v up')
|
||||
if ret != 0 {
|
||||
elog('failed to update V, exit_code: ${ret}')
|
||||
// A failed `git pull --rebase` (e.g. on a shallow CI checkout)
|
||||
// leaves the worktree with conflict markers in source files,
|
||||
// which would break any subsequent V compilation step. Restore
|
||||
// a clean state before returning.
|
||||
lsystem('cd ${vdir} && git rebase --abort')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the last commit's hash
|
||||
commit := lexec('git rev-parse HEAD')[..8]
|
||||
if os.exists('fast.vlang.io/index.html') {
|
||||
uploaded_index := os.read_file('fast.vlang.io/index.html')!
|
||||
if uploaded_index.contains('>${commit}<') {
|
||||
elog('NOTE: commit ${commit} had been benchmarked already.')
|
||||
if !args.contains('-force') {
|
||||
elog('nothing more to do')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.chdir(vdir)!
|
||||
message := lexec('git log --pretty=format:"%s" -n1 ${commit}')
|
||||
commit_date := lexec('git log -n1 --pretty="format:%at" ${commit}')
|
||||
date := time.unix(commit_date.i64())
|
||||
|
||||
elog('Benchmarking commit ${commit} , with commit message: "${message}", commit_date: ${commit_date}, date: ${date}')
|
||||
|
||||
// build an optimized V
|
||||
if args.contains('-do-not-rebuild-vprod') {
|
||||
if !os.exists('vprod') {
|
||||
elog('Exiting, since if you use `-do-not-rebuild-vprod`, you should already have a `${vdir}/vprod` executable, but it is missing!')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
elog(' Building vprod...')
|
||||
if args.contains('-noprod') {
|
||||
lexec('./v -o vprod cmd/v') // for faster debugging
|
||||
} else {
|
||||
lexec('./v -o vprod -prod -prealloc cmd/v')
|
||||
}
|
||||
}
|
||||
|
||||
if !args.contains('-do-not-rebuild-caches') {
|
||||
elog('clearing caches...')
|
||||
// cache vlib modules
|
||||
lexec('${vdir}/v wipe-cache')
|
||||
lexec('${vdir}/v -o vwarm_caches -cc ${ccompiler_path} cmd/v')
|
||||
}
|
||||
|
||||
// measure
|
||||
diff1 := measure('${vdir}/vprod ${voptions} -o v.c cmd/v', 'v.c')
|
||||
diff2 := measure('${vdir}/vprod ${voptions} -cc ${ccompiler_path} -o v2 cmd/v', 'v2')
|
||||
diff3 := 0 // measure('${vdir}/vprod -native ${vdir}/cmd/tools/1mil.v', 'native 1mil')
|
||||
diff4 := measure('${vdir}/vprod ${voptions} -cc ${ccompiler_path} examples/hello_world.v',
|
||||
'hello.v')
|
||||
vc_size := os.file_size('v.c') / 1000
|
||||
scan, parse, check, cgen, vlines := measure_steps_minimal(vdir)!
|
||||
|
||||
html_message := message.replace_each(['<', '<', '>', '>'])
|
||||
|
||||
os.chdir(fast_dir)!
|
||||
// place the new row on top
|
||||
table := os.read_file('table.html')!
|
||||
new_table :=
|
||||
' <tr>
|
||||
<td>${date.format()}</td>
|
||||
<td><a target=_blank href="https://github.com/vlang/v/commit/${commit}">${commit}</a></td>
|
||||
<td>${html_message}</td>
|
||||
<td>${diff1}ms</td>
|
||||
<td>${diff2}ms</td>
|
||||
<td>${diff3}ms</td>
|
||||
<td>${diff4}ms</td>
|
||||
<td>${vc_size} KB</td>
|
||||
<td>${parse}ms</td>
|
||||
<td>${check}ms</td>
|
||||
<td>${cgen}ms</td>
|
||||
<td>${scan}ms</td>
|
||||
<td>${vlines}</td>
|
||||
<td>${int(f64(vlines) / f64(diff1) * 1000.0)}</td>
|
||||
</tr>\n' +
|
||||
table.trim_space() + '\n'
|
||||
os.write_file('table.html', new_table)!
|
||||
|
||||
// regenerate index.html
|
||||
header := os.read_file('header.html')!
|
||||
footer := os.read_file('footer.html')!
|
||||
mut res := os.create('index.html')!
|
||||
res.writeln(header)!
|
||||
res.writeln(new_table)!
|
||||
res.writeln(footer)!
|
||||
res.close()
|
||||
|
||||
// upload the result to github pages
|
||||
if args.contains('-upload') {
|
||||
$if freebsd {
|
||||
// Note: tcc currently can not compile vpm on FreeBSD, due to its dependence on net.ssl and net.mbedtls, so force using clang instead:
|
||||
elog('FreeBSD: compiling the VPM tool with clang...')
|
||||
lexec('${vdir}/vprod -cc clang ${vdir}/cmd/tools/vpm/')
|
||||
os.chdir('${fast_dir}/docs.vlang.io/docs_generator/')!
|
||||
elog('FreeBSD: installing the dependencies for the docs generator...')
|
||||
lexec('${vdir}/vprod install')
|
||||
os.chdir(fast_dir)!
|
||||
}
|
||||
|
||||
os.chdir('${fast_dir}/fast.vlang.io/')!
|
||||
elog('Uploading to fast.vlang.io/ ...')
|
||||
lexec('git checkout gh-pages')
|
||||
os.mv('../index.html', 'index.html')!
|
||||
elog(' adding changes...')
|
||||
lexec('git commit -am "update fast.vlang.io for commit ${commit}"')
|
||||
elog(' pushing...')
|
||||
lexec('git push origin gh-pages')
|
||||
elog(' uploading to fast.vlang.io/ done')
|
||||
os.chdir(fast_dir)!
|
||||
|
||||
os.chdir('${fast_dir}/docs.vlang.io/')!
|
||||
elog('Uploading to docs.vlang.io/ ...')
|
||||
elog(' pulling upstream changes...')
|
||||
if !lexec_check('git pull') {
|
||||
elog(' skipping docs.vlang.io upload: `git pull` failed')
|
||||
} else if !lexec_check('${vdir}/vprod run build.vsh') {
|
||||
elog(' skipping docs.vlang.io upload: `vprod run build.vsh` failed')
|
||||
} else {
|
||||
elog(' adding new docs...')
|
||||
lexec('git add .')
|
||||
// `git diff --cached --quiet` exits 0 when there is nothing staged,
|
||||
// 1 when there are staged changes, and >1 on real errors (e.g. a
|
||||
// broken index). Treat >1 as an error rather than as "changes exist".
|
||||
diff_cmd := 'git diff --cached --quiet'
|
||||
elog(' lexec: ${diff_cmd}')
|
||||
diff_res := os.execute(diff_cmd)
|
||||
match diff_res.exit_code {
|
||||
0 {
|
||||
elog(' nothing to commit; skipping push to docs.vlang.io/')
|
||||
}
|
||||
1 {
|
||||
elog(' commiting...')
|
||||
lexec('git commit -m "update docs for commit ${commit}"')
|
||||
elog(' pushing...')
|
||||
if !lexec_check('git push') {
|
||||
elog(' WARNING: `git push` to docs.vlang.io/ failed')
|
||||
} else {
|
||||
elog(' uploading to docs.vlang.io/ done')
|
||||
}
|
||||
}
|
||||
else {
|
||||
elog(' skipping docs.vlang.io upload: `${diff_cmd}` errored, exit_code: ${diff_res.exit_code}, output:\n${diff_res.output}')
|
||||
}
|
||||
}
|
||||
}
|
||||
os.chdir(fast_dir)!
|
||||
}
|
||||
}
|
||||
|
||||
// measure returns milliseconds
|
||||
fn measure(cmd string, description string) int {
|
||||
elog(' Measuring ${description}, warmups: ${warmup_samples}, samples: ${max_samples}, discard: ${discard_highest_samples}, with cmd: `${cmd}`')
|
||||
for _ in 0 .. warmup_samples {
|
||||
os.system(cmd)
|
||||
}
|
||||
mut runs := []int{}
|
||||
for r in 0 .. max_samples {
|
||||
sw := time.new_stopwatch()
|
||||
os.execute(cmd)
|
||||
sample := int(sw.elapsed().milliseconds())
|
||||
runs << sample
|
||||
elog(' Sample ${r + 1:2}/${max_samples:2} ... ${sample} ms')
|
||||
}
|
||||
runs.sort()
|
||||
elog(' runs before discarding: ${runs}, avg: ${f64(arrays.sum(runs) or { 0 }) / runs.len:5.2f}')
|
||||
// Discard the highest times, since on AWS, they are caused by random load spikes,
|
||||
// that are unpredictable, add noise and skew the statistics, without adding useful
|
||||
// insights:
|
||||
for _ in 0 .. discard_highest_samples {
|
||||
runs.pop()
|
||||
}
|
||||
elog(' runs after discarding: ${runs}, avg: ${f64(arrays.sum(runs) or { 0 }) / runs.len:5.2f}')
|
||||
return int(f64(arrays.sum(runs) or { 0 }) / runs.len)
|
||||
}
|
||||
|
||||
fn measure_steps_minimal(vdir string) !(int, int, int, int, int) {
|
||||
elog('measure_steps_minimal ${vdir}, samples: ${max_samples}')
|
||||
mut scans, mut parses, mut checks, mut cgens, mut vliness := []int{}, []int{}, []int{}, []int{}, []int{}
|
||||
for i in 0 .. max_samples {
|
||||
scan, parse, check, cgen, vlines, cmd := measure_steps_one_sample(vdir)
|
||||
scans << scan
|
||||
parses << parse
|
||||
checks << check
|
||||
cgens << cgen
|
||||
vliness << vlines
|
||||
elog(' [${i:2}/${max_samples:2}] scan: ${scan} ms, min parse: ${parse} ms, min check: ${check} ms, min cgen: ${cgen} ms, min vlines: ${vlines} ms, cmd: ${cmd}')
|
||||
}
|
||||
scan, parse, check, cgen, vlines := arrays.min(scans)!, arrays.min(parses)!, arrays.min(checks)!, arrays.min(cgens)!, arrays.min(vliness)!
|
||||
elog('measure_steps_minimal => min scan: ${scan} ms, min parse: ${parse} ms, min check: ${check} ms, min cgen: ${cgen} ms, min vlines: ${vlines} ms')
|
||||
return scan, parse, check, cgen, vlines
|
||||
}
|
||||
|
||||
fn measure_steps_one_sample(vdir string) (int, int, int, int, int, string) {
|
||||
cmd := '${vdir}/vprod ${voptions} -o v.c cmd/v'
|
||||
resp := os.execute(cmd)
|
||||
|
||||
mut scan, mut parse, mut check, mut cgen, mut vlines := 0, 0, 0, 0, 0
|
||||
lines := resp.output.split_into_lines()
|
||||
if lines.len == 3 {
|
||||
parse = lines[0].before('.').int()
|
||||
check = lines[1].before('.').int()
|
||||
cgen = lines[2].before('.').int()
|
||||
} else {
|
||||
ms_lines := lines.map(it.split(' ms '))
|
||||
for line in ms_lines {
|
||||
if line.len == 2 {
|
||||
if line[1] == 'SCAN' {
|
||||
scan = line[0].int()
|
||||
}
|
||||
if line[1] == 'PARSE' {
|
||||
parse = line[0].int()
|
||||
}
|
||||
if line[1] == 'CHECK' {
|
||||
check = line[0].int()
|
||||
}
|
||||
if line[1] == 'C GEN' {
|
||||
cgen = line[0].int()
|
||||
}
|
||||
} else {
|
||||
// fetch number of V lines
|
||||
if line[0].contains('V') && line[0].contains('source') && line[0].contains('size') {
|
||||
start := line[0].index(':') or { 0 }
|
||||
end := line[0].index('lines,') or { 0 }
|
||||
s := line[0][start + 1..end]
|
||||
vlines = s.trim_space().int()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return scan, parse, check, cgen, vlines, cmd
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
import os
|
||||
import time
|
||||
|
||||
// This program acts as a service/monitor/daemon, that runs in the background, checks for repo updates,
|
||||
// and runs fast.v, when there are changes. It should *never fail*, even if fast.v can fail. In that case,
|
||||
// this program should just loop, trying again on the next iteration of the loop, and hoping that the repo
|
||||
// was fixed enough, so that fast.v can succeed.
|
||||
|
||||
// It is the responsibility of * fast.v * , to do all measurements, process them, and to pushes the HTML result
|
||||
// of that processing to the fast.vlang.io GH pages repo.
|
||||
|
||||
const sleep_period = 60
|
||||
|
||||
const fast_dir = os.dir(@FILE)
|
||||
|
||||
const vdir = os.real_path(os.dir(os.dir(os.dir(fast_dir))))
|
||||
|
||||
const vexe = os.real_path(os.join_path(vdir, 'v'))
|
||||
|
||||
const old_path = os.getenv('PATH')
|
||||
|
||||
fn elog(msg string) {
|
||||
eprintln('${time.now().format_ss_micro()} ${msg}')
|
||||
}
|
||||
|
||||
fn delay() {
|
||||
elog('Sleeping for ${sleep_period} seconds...')
|
||||
time.sleep(sleep_period * time.second)
|
||||
}
|
||||
|
||||
fn check_output_repo(url string, branch string, folder string) {
|
||||
if os.exists(folder) {
|
||||
elog('Note: ${folder} already exists; using it.')
|
||||
return
|
||||
}
|
||||
cmd := 'git clone --filter=blob:none --branch=${branch} ${url} ${folder}'
|
||||
elog('Note: ${folder} is missing. Cloning to `${folder}`, with: `${cmd}` ...')
|
||||
res := os.system(cmd)
|
||||
elog('... cloning done, result: ${res}')
|
||||
}
|
||||
|
||||
fn main() {
|
||||
elog('fast_job start setup ...')
|
||||
// ensure a more stable working environment for the used tools, independent on how this executable was started:
|
||||
os.setenv('LANG', 'C', true)
|
||||
os.setenv('PATH', '${vdir}:${old_path}', true)
|
||||
elog('fast_job fast_dir: ${fast_dir}')
|
||||
elog('fast_job vdir: ${vdir}')
|
||||
elog('fast_job vexe: ${vexe}')
|
||||
elog('fast_job PATH: ${os.getenv('PATH')}')
|
||||
|
||||
os.chdir(fast_dir)!
|
||||
elog('fast_job start in os.getwd(): ${os.getwd()}')
|
||||
|
||||
defer {
|
||||
elog('fast_job end')
|
||||
}
|
||||
|
||||
elog('fast_job clone output repos...')
|
||||
check_output_repo('https://github.com/vlang/website', 'gh-pages', 'fast.vlang.io/')
|
||||
check_output_repo('https://github.com/vlang/docs/', 'main', 'docs.vlang.io/')
|
||||
check_output_repo('https://github.com/vlang/docs/', 'generator',
|
||||
'docs.vlang.io/docs_generator/')
|
||||
|
||||
mut i := 0
|
||||
for {
|
||||
i++
|
||||
elog('------------------- Checking for updates, loop: ${i} ... -------------------')
|
||||
os.chdir(fast_dir)!
|
||||
|
||||
res_pull := os.execute('git pull --rebase')
|
||||
elog('> res_pull.output: ${res_pull.output}')
|
||||
if res_pull.exit_code != 0 {
|
||||
elog('Git pull failed. You may have uncommitted changes?')
|
||||
delay()
|
||||
continue
|
||||
}
|
||||
if res_pull.output.contains('Already up to date.') {
|
||||
if os.args.contains('-force-update') {
|
||||
elog('The repository was already updated, but -force-update was passed too.')
|
||||
} else {
|
||||
delay()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
elog('recompiling V')
|
||||
os.system('${os.quoted_path(vexe)} self')
|
||||
os.system('ls -la ${os.quoted_path(vexe)}')
|
||||
|
||||
elog('recompiling ./fast')
|
||||
recompile_fast_v_code := os.system('${os.quoted_path(vexe)} -keepc -g fast.v')
|
||||
if recompile_fast_v_code != 0 {
|
||||
elog('WARNING: could not recompile fast.v')
|
||||
delay()
|
||||
continue
|
||||
}
|
||||
os.system('ls -la fast fast.v')
|
||||
|
||||
elog('running ./fast -upload')
|
||||
fast_exit_code := os.system('./fast -upload')
|
||||
if fast_exit_code != 0 {
|
||||
println('fast_exit_code = ${fast_exit_code}, != 0')
|
||||
}
|
||||
|
||||
delay()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
const delta = 18;
|
||||
|
||||
(function () {
|
||||
var table = document.querySelector("table");
|
||||
var isTbody = table.children[0].nodeName == "TBODY";
|
||||
var trs = isTbody
|
||||
? table.children[0].querySelectorAll("tr")
|
||||
: table.querySelectorAll("tr");
|
||||
trs.forEach(function (tr, idx) {
|
||||
if (idx != 0 && idx + 1 < trs.length) {
|
||||
var vc = 3, vv = 4, vf = 5, vh = 6;
|
||||
var textContent = {
|
||||
vc: tr.children[vc].textContent,
|
||||
vv: tr.children[vv].textContent,
|
||||
vf: tr.children[vf].textContent,
|
||||
vh: tr.children[vh].textContent
|
||||
};
|
||||
var currentData = {
|
||||
vc: int(textContent.vc.slice(0, -2)),
|
||||
vv: int(textContent.vv.slice(0, -2)),
|
||||
vf: int(textContent.vf.slice(0, -2)),
|
||||
vh: int(textContent.vh.slice(0, -2))
|
||||
};
|
||||
var prevData = {
|
||||
vc: int(trs[idx + 1].children[vc].textContent.slice(0, -2)),
|
||||
vv: int(trs[idx + 1].children[vv].textContent.slice(0, -2)),
|
||||
vf: int(trs[idx + 1].children[vf].textContent.slice(0, -2)),
|
||||
vh: int(trs[idx + 1].children[vh].textContent.slice(0, -2))
|
||||
};
|
||||
var result = {
|
||||
vc: currentData.vc - prevData.vc,
|
||||
vv: currentData.vv - prevData.vv,
|
||||
vf: currentData.vf - prevData.vf,
|
||||
vh: currentData.vh - prevData.vh
|
||||
};
|
||||
if (Math.abs(result.vc) > delta)
|
||||
tr.children[vc].appendChild(createElement(result.vc));
|
||||
if (Math.abs(result.vv) > delta * 2)
|
||||
tr.children[vv].appendChild(createElement(result.vv));
|
||||
if (Math.abs(result.vf) > delta * 2)
|
||||
tr.children[vf].appendChild(createElement(result.vf));
|
||||
if (Math.abs(result.vh) > delta * 2)
|
||||
tr.children[vh].appendChild(createElement(result.vh));
|
||||
}
|
||||
});
|
||||
function int(src) {
|
||||
return src - 0;
|
||||
}
|
||||
function getClassName(x) {
|
||||
if (x == 0)
|
||||
return "equal";
|
||||
return x < 0 ? "plus" : "minus";
|
||||
}
|
||||
function createElement(result) {
|
||||
var el = document.createElement("span");
|
||||
var parsedResult = parseResult(result);
|
||||
el.classList.add("diff");
|
||||
el.classList.add(getClassName(result));
|
||||
el.textContent = parsedResult;
|
||||
return el;
|
||||
}
|
||||
function parseResult(x) {
|
||||
if (x == 0)
|
||||
return "0";
|
||||
return x > 0 ? "+" + x : x;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,55 @@
|
||||
*, body {
|
||||
font-family: Menlo, Monospace, 'Courier New';
|
||||
}
|
||||
table, td, th {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #dfdfdf;
|
||||
}
|
||||
th {
|
||||
background-color: lightgray;
|
||||
color: black;
|
||||
padding: 5px;
|
||||
}
|
||||
td {
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
table td:nth-child(3) {
|
||||
width: 650px!important;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
border: 0px;
|
||||
border-bottom: 1px solid lightgray;
|
||||
}
|
||||
.diff {
|
||||
border-radius: 2.5px;
|
||||
color: #ffffff;
|
||||
padding: 0 5px 0 5px;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
.minus {
|
||||
background-color: rgb(195, 74, 104);
|
||||
}
|
||||
.plus {
|
||||
background-color: #8BC34A;
|
||||
}
|
||||
.equal {
|
||||
background-color: rgb(113, 68, 172);
|
||||
}
|
||||
|
||||
table {
|
||||
counter-reset: rowNumber -1;
|
||||
}
|
||||
table tr {
|
||||
counter-increment: rowNumber;
|
||||
}
|
||||
table tr td:first-child::after {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: 0px;
|
||||
content: counter(rowNumber);
|
||||
font-size: 10px;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
</table>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Is V still fast?</title>
|
||||
<link rel="icon" type="image/x-icon" sizes="32x32" href="favicon_32x32.ico">
|
||||
<link rel="icon" type="image/x-icon" sizes="16x16" href="favicon_16x16.ico">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<h2><a href='/'>Is V still fast?</a></h2>
|
||||
|
||||
Monitoring compilation speed for each commit. <br><br>
|
||||
Running on a free tier AWS t2.micro instance (1 vCPU). Typical desktop hardware is 2-3 times faster. <br><br>
|
||||
Source code: <a target=blank href='https://github.com/vlang/v/blob/master/cmd/tools/fast/fast.v'>fast.v</a> <br><br>
|
||||
Older stats: <a href="2024.html">2024</a>, <a href="2023.html">2023</a>, <a href="2022.html">2022</a>, <a href="2021.html">2021</a>, <a href="2020.html">2020</a><br><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th style='width:180px'> Timestamp </th>
|
||||
<th style='width:85px'>Commit</th>
|
||||
<th class='cmessage'>Commit Message</td>
|
||||
<th style='width:120px;white-space: nowrap'>v -o v.c</th>
|
||||
<th style='width:120px;white-space: nowrap'>v -o v</th>
|
||||
<th style='width:130px'>v -native 1mil.v</th>
|
||||
<th style='width:120px;white-space: nowrap'>v hello.v</th>
|
||||
<th style='width:85px;white-space: nowrap'>v.c size</th>
|
||||
<th style='width:55px;white-space: nowrap'>parse</th>
|
||||
<th style='width:55px'>check</th>
|
||||
<th style='width:55px'>cgen</th>
|
||||
<th style='width:55px'>scan</th>
|
||||
<th style='width:80px;white-space: nowrap'>V lines</th>
|
||||
<th style='width:95px;white-space: nowrap'>V lines/s</th>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import os
|
||||
|
||||
fn main() {
|
||||
args := arguments()
|
||||
if args.len < 2 {
|
||||
println('Usage: v run doc_comment_checker.v <file1.v> <file2.v>...')
|
||||
return
|
||||
}
|
||||
mut fpaths := []string{}
|
||||
for filepath in args[1..] {
|
||||
if os.is_file(filepath) {
|
||||
fpaths << filepath
|
||||
}
|
||||
if os.is_dir(filepath) {
|
||||
fpaths << os.walk_ext(filepath, '.v').filter(!it.ends_with('_test.v')
|
||||
&& !it.ends_with('_test.c.v'))
|
||||
}
|
||||
}
|
||||
fpaths.sort()
|
||||
mut ctx := Context{}
|
||||
for filepath in fpaths {
|
||||
ctx.process_fpath(filepath) or {
|
||||
eprintln('error with ${filepath}: ${err}')
|
||||
continue
|
||||
}
|
||||
}
|
||||
println('> Processed ${fpaths.len} .v files, found errors: ${ctx.errors} , in ${ctx.pub_symbols} `pub` declarations, and ${ctx.pub_comment_lines} pub comment lines.')
|
||||
if ctx.errors > 0 {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
struct Context {
|
||||
mut:
|
||||
errors int
|
||||
pub_symbols int
|
||||
pub_comment_lines int
|
||||
comments int
|
||||
}
|
||||
|
||||
fn (mut ctx Context) process_fpath(filepath string) ! {
|
||||
lines := os.read_lines(filepath)!
|
||||
mut prev := 0
|
||||
for iline, line in lines {
|
||||
if line.starts_with('pub ') {
|
||||
ctx.pub_symbols++
|
||||
mut comments := []CommentLine{}
|
||||
mut i := 0
|
||||
for i = int_max(0, iline - 1); i >= prev; i-- {
|
||||
pline := lines[i]
|
||||
if pline.starts_with('// ') {
|
||||
comments << CommentLine{pline, i + 1}
|
||||
} else {
|
||||
prev = iline + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if comments.len > 0 {
|
||||
cline := comments.last()
|
||||
fword := cline.comment.all_after('// ').all_before(' ')
|
||||
if !line.contains(fword) {
|
||||
continue
|
||||
}
|
||||
ctx.pub_comment_lines += comments.len
|
||||
if !cline.comment.ends_with('.') {
|
||||
println('${filepath}:${cline.line}: ${cline.comment}')
|
||||
ctx.errors++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CommentLine {
|
||||
comment string
|
||||
line int
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
cores=$(nproc --all)
|
||||
|
||||
echo Number of cores: $cores
|
||||
echo Compiling...
|
||||
./v -cc clang -o cmd/tools/fuzz/map_fuzz cmd/tools/fuzz/map_fuzz.v -prod -cflags "-fsanitize=memory"
|
||||
|
||||
echo Fuzzing:
|
||||
while true
|
||||
do
|
||||
for ((i=1;i<=cores;++i))
|
||||
do
|
||||
sleep 0.001
|
||||
./cmd/tools/fuzz/map_fuzz &
|
||||
done
|
||||
wait
|
||||
done
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Note: radamsa is a fuzzer, available from https://gitlab.com/akihe/radamsa
|
||||
|
||||
export VFUZZER=true
|
||||
export OUTPUT_FILE=${1:-x.v}
|
||||
export PARSER_EXECUTABLE=${2:-cmd/tools/measure/parser_speed}
|
||||
|
||||
echo "Fuzzing parameters | OUTPUT FILE: ${OUTPUT_FILE} | PARSER_EXECUTABLE: ${PARSER_EXECUTABLE}"
|
||||
|
||||
if [ ! -f $PARSER_EXECUTABLE ]; then
|
||||
v -g -o "${PARSER_EXECUTABLE}" cmd/tools/measure/parser_speed.v
|
||||
fi
|
||||
|
||||
while true; do
|
||||
radamsa --meta "${OUTPUT_FILE}.autofuzz.log" examples/hello_world.v > "${OUTPUT_FILE}";
|
||||
echo -ne "OFILE: ${OUTPUT_FILE}"; ./"${PARSER_EXECUTABLE}" ${OUTPUT_FILE} || break;
|
||||
done
|
||||
@@ -0,0 +1,144 @@
|
||||
import rand
|
||||
import time
|
||||
|
||||
fn generate_strings(str_len int, arr_len int) []string {
|
||||
mut arr := []string{len: arr_len}
|
||||
for i in 0 .. arr_len {
|
||||
arr[i] = rand.string(str_len)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
fn fuzz1() {
|
||||
amount := 200000 - rand.intn(100000) or { 0 }
|
||||
amount2 := 200000 - rand.intn(100000) or { 0 }
|
||||
len := 25 - rand.intn(10) or { 0 }
|
||||
arr := generate_strings(len, amount)
|
||||
arr2 := generate_strings(len, amount2)
|
||||
mut m := map[string]int{}
|
||||
for i in 0 .. amount {
|
||||
m[arr[i]] = i
|
||||
assert i == m[arr[i]]
|
||||
}
|
||||
for i in 0 .. amount {
|
||||
assert i == m[arr[i]]
|
||||
}
|
||||
for i in 0 .. amount2 {
|
||||
assert 0 == m[arr2[i]]
|
||||
}
|
||||
unsafe {
|
||||
m.free()
|
||||
arr.free()
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzz2() {
|
||||
mut m := map[string]int{}
|
||||
amount := rand.intn(500000) or { 0 } + 1
|
||||
len := 25 - rand.intn(10) or { 0 }
|
||||
arr := generate_strings(len, amount)
|
||||
for i, x in arr {
|
||||
m[x] = i
|
||||
}
|
||||
mut i := 0
|
||||
for key, val in m {
|
||||
assert key == arr[i]
|
||||
assert val == i
|
||||
i++
|
||||
}
|
||||
unsafe {
|
||||
m.free()
|
||||
arr.free()
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzz3() {
|
||||
mut m := map[string]int{}
|
||||
amount := rand.intn(500000) or { 0 } + 1
|
||||
len := 25 - rand.intn(10) or { 0 }
|
||||
arr := generate_strings(len, amount)
|
||||
for i, x in arr {
|
||||
if (i % 10000) == 0 {
|
||||
keys := m.keys()
|
||||
assert keys.len == i
|
||||
assert keys == arr[0..i]
|
||||
}
|
||||
m[x] = i
|
||||
}
|
||||
assert m.keys() == arr
|
||||
assert m.keys().len == amount
|
||||
unsafe {
|
||||
m.free()
|
||||
arr.free()
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzz4() {
|
||||
amount := rand.intn(500000) or { 0 }
|
||||
len := 25 - rand.intn(10) or { 0 }
|
||||
arr := generate_strings(len, amount)
|
||||
mut m := map[string]int{}
|
||||
for i in 0 .. amount {
|
||||
m[arr[i]] = i
|
||||
}
|
||||
for i in 0 .. amount {
|
||||
m.delete(arr[i])
|
||||
assert m[arr[i]] == 0
|
||||
}
|
||||
assert m.len == 0
|
||||
unsafe {
|
||||
m.free()
|
||||
arr.free()
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzz5() {
|
||||
amount := rand.intn(500000) or { 0 } + 1
|
||||
arr := generate_strings(20, amount)
|
||||
mut m := map[string]int{}
|
||||
for i in 0 .. amount {
|
||||
m[arr[i]] = i
|
||||
assert (arr[i] in m) == true
|
||||
}
|
||||
for i in 0 .. amount {
|
||||
m.delete(arr[i])
|
||||
assert (arr[i] !in m) == true
|
||||
assert m.len == amount - i - 1
|
||||
}
|
||||
assert m.len == 0
|
||||
unsafe {
|
||||
m.free()
|
||||
arr.free()
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzz6() {
|
||||
mut m := map[string]int{}
|
||||
amount := rand.intn(500000) or { 0 } + 1
|
||||
len := 25 - rand.intn(10) or { 0 }
|
||||
arr := generate_strings(len, amount)
|
||||
for i, x in arr {
|
||||
m[x]++
|
||||
m[x] += i
|
||||
assert m[x] == i + 1
|
||||
}
|
||||
for i, x in arr {
|
||||
assert m[x] == i + 1
|
||||
}
|
||||
unsafe {
|
||||
m.free()
|
||||
arr.free()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
seed := u32(time.ticks())
|
||||
println('seed: ${seed.hex()}')
|
||||
rand.seed([seed, seed])
|
||||
fuzz1()
|
||||
fuzz2()
|
||||
fuzz3()
|
||||
fuzz4()
|
||||
fuzz5()
|
||||
fuzz6()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
fn main() {
|
||||
for i in 0 .. 100000 {
|
||||
println('
|
||||
fn foo${i}() {
|
||||
x := ${i}
|
||||
mut a := 1 + x
|
||||
a += 2
|
||||
print(a)
|
||||
a = 0
|
||||
a = 1
|
||||
}
|
||||
')
|
||||
}
|
||||
// println('fn main() {foo1()} ')
|
||||
println('fn main() { println("1m DONE") } ')
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import log
|
||||
import flag
|
||||
import time
|
||||
import veb
|
||||
import net.urllib
|
||||
|
||||
// This tool regenerates V's bootstrap .c files
|
||||
// every time the V master branch is updated.
|
||||
// if run with the --serve flag it will run in webhook
|
||||
// server mode awaiting a request to http://host:port/genhook
|
||||
// available command line flags:
|
||||
// --work-dir gen_vc's working directory
|
||||
// --purge force purge the local repositories
|
||||
// --serve run in webhook server mode
|
||||
// --port port for http server to listen on
|
||||
// --log-to either 'file' or 'terminal'
|
||||
// --log-file path to log file used when --log-to is 'file'
|
||||
// --dry-run dont push anything to remote repo
|
||||
// --force force update even if already up to date
|
||||
|
||||
// git credentials
|
||||
const git_username = os.getenv('GITUSER')
|
||||
const git_password = os.getenv('GITPASS')
|
||||
|
||||
// repository
|
||||
// git repo
|
||||
const git_repo_v = 'github.com/vlang/v'
|
||||
const git_repo_vc = 'github.com/vlang/vc'
|
||||
// local repo directories
|
||||
const git_repo_dir_v = 'v'
|
||||
const git_repo_dir_vc = 'vc'
|
||||
|
||||
// gen_vc
|
||||
// name
|
||||
const app_name = 'gen_vc'
|
||||
// version
|
||||
const app_version = '0.1.3'
|
||||
// description
|
||||
const app_description = "This tool regenerates V's bootstrap .c files every time the V master branch is updated."
|
||||
// assume something went wrong if file size less than this
|
||||
const too_short_file_limit = 5000
|
||||
// create a .c file for these os's
|
||||
const vc_build_oses = [
|
||||
'nix',
|
||||
// all nix based os
|
||||
'windows',
|
||||
]
|
||||
|
||||
// default options (overridden by flags)
|
||||
// gen_vc working directory
|
||||
const work_dir = '/tmp/gen_vc'
|
||||
// dont push anything to remote repo
|
||||
const dry_run = false
|
||||
// server port
|
||||
const server_port = 7171
|
||||
// log file
|
||||
const log_file = '${work_dir}/log.txt'
|
||||
// log_to is either 'file' or 'terminal'
|
||||
const log_to = 'terminal'
|
||||
|
||||
// errors
|
||||
const err_msg_build = 'error building'
|
||||
const err_msg_make = 'make failed'
|
||||
const err_msg_gen_c = 'failed to generate .c file'
|
||||
const err_msg_cmd_x = 'error running cmd'
|
||||
|
||||
struct GenVC {
|
||||
// logger
|
||||
// flag options
|
||||
options FlagOptions
|
||||
mut:
|
||||
logger &log.Log = unsafe { nil }
|
||||
// true if error was experienced running generate
|
||||
gen_error bool
|
||||
}
|
||||
|
||||
// webhook server
|
||||
struct WebhookServer {
|
||||
mut:
|
||||
gen_vc &GenVC = unsafe { nil } // initialized in init_server
|
||||
}
|
||||
|
||||
struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
// storage for flag options
|
||||
struct FlagOptions {
|
||||
work_dir string
|
||||
purge bool
|
||||
serve bool
|
||||
port int
|
||||
log_to string
|
||||
log_file string
|
||||
dry_run bool
|
||||
force bool
|
||||
}
|
||||
|
||||
fn main() {
|
||||
log.use_stdout()
|
||||
mut fp := flag.new_flag_parser(os.args.clone())
|
||||
fp.application(app_name)
|
||||
fp.version(app_version)
|
||||
fp.description(app_description)
|
||||
fp.skip_executable()
|
||||
show_help := fp.bool('help', 0, false, 'Show this help screen\n')
|
||||
flag_options := parse_flags(mut fp)
|
||||
if show_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
fp.finalize() or {
|
||||
eprintln(err)
|
||||
println(fp.usage())
|
||||
return
|
||||
}
|
||||
// webhook server mode
|
||||
if flag_options.serve {
|
||||
mut server := &WebhookServer{}
|
||||
veb.run_at[WebhookServer, Context](mut server, port: flag_options.port)!
|
||||
} else {
|
||||
// cmd mode
|
||||
mut gen_vc := new_gen_vc(flag_options)
|
||||
gen_vc.init()
|
||||
gen_vc.generate()
|
||||
}
|
||||
}
|
||||
|
||||
// new GenVC
|
||||
fn new_gen_vc(flag_options FlagOptions) &GenVC {
|
||||
mut logger := &log.Log{}
|
||||
logger.set_level(.debug)
|
||||
if flag_options.log_to == 'file' {
|
||||
logger.set_full_logpath(flag_options.log_file)
|
||||
}
|
||||
return &GenVC{
|
||||
options: flag_options
|
||||
logger: logger
|
||||
}
|
||||
}
|
||||
|
||||
// WebhookServer init
|
||||
pub fn (mut ws WebhookServer) init_server() {
|
||||
mut fp := flag.new_flag_parser(os.args.clone())
|
||||
flag_options := parse_flags(mut fp)
|
||||
ws.gen_vc = new_gen_vc(flag_options)
|
||||
ws.gen_vc.init()
|
||||
// ws.gen_vc = new_gen_vc(flag_options)
|
||||
}
|
||||
|
||||
pub fn (mut ws WebhookServer) index() {
|
||||
eprintln('WebhookServer.index() called')
|
||||
}
|
||||
|
||||
// gen webhook
|
||||
pub fn (mut ws WebhookServer) genhook() veb.Result {
|
||||
// request data
|
||||
// println(ws.req.data)
|
||||
// TODO: parse request. json or urlencoded
|
||||
// json.decode or net.urllib.parse
|
||||
ws.gen_vc.generate()
|
||||
// error in generate
|
||||
if ws.gen_vc.gen_error {
|
||||
return ctx.json('{status: "failed"}')
|
||||
}
|
||||
return ctx.json('{status: "ok"}')
|
||||
}
|
||||
|
||||
pub fn (ws &WebhookServer) reset() {
|
||||
}
|
||||
|
||||
// parse flags to FlagOptions struct
|
||||
fn parse_flags(mut fp flag.FlagParser) FlagOptions {
|
||||
return FlagOptions{
|
||||
serve: fp.bool('serve', 0, false, 'run in webhook server mode')
|
||||
work_dir: fp.string('work-dir', 0, work_dir, 'gen_vc working directory')
|
||||
purge: fp.bool('purge', 0, false, 'force purge the local repositories')
|
||||
port: fp.int('port', 0, server_port, 'port for web server to listen on')
|
||||
log_to: fp.string('log-to', 0, log_to, "log to is 'file' or 'terminal'")
|
||||
log_file: fp.string('log-file', 0, log_file, "log file to use when log-to is 'file'")
|
||||
dry_run: fp.bool('dry-run', 0, dry_run, 'when specified dont push anything to remote repo')
|
||||
force: fp.bool('force', 0, false, 'force update even if already up to date')
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut gen_vc GenVC) init() {
|
||||
// purge repos if flag is passed
|
||||
if gen_vc.options.purge {
|
||||
gen_vc.purge_repos()
|
||||
}
|
||||
}
|
||||
|
||||
// regenerate
|
||||
fn (mut gen_vc GenVC) generate() {
|
||||
// set errors to false
|
||||
gen_vc.gen_error = false
|
||||
// check if gen_vc dir exists
|
||||
if !os.is_dir(gen_vc.options.work_dir) {
|
||||
// try create
|
||||
os.mkdir(gen_vc.options.work_dir) or { panic(err) }
|
||||
// still doesn't exist... we have a problem
|
||||
if !os.is_dir(gen_vc.options.work_dir) {
|
||||
gen_vc.logger.error('error creating directory: ${gen_vc.options.work_dir}')
|
||||
gen_vc.gen_error = true
|
||||
return
|
||||
}
|
||||
}
|
||||
// cd to gen_vc dir
|
||||
os.chdir(gen_vc.options.work_dir) or {}
|
||||
// if we are not running with the --serve flag (webhook server)
|
||||
// rather than deleting and re-downloading the repo each time
|
||||
// first check to see if the local v repo is behind master
|
||||
// if it isn't behind there's no point continuing further
|
||||
if !gen_vc.options.serve && os.is_dir(git_repo_dir_v) {
|
||||
gen_vc.cmd_exec('git -C ${git_repo_dir_v} checkout master')
|
||||
// fetch the remote repo just in case there are newer commits there
|
||||
gen_vc.cmd_exec('git -C ${git_repo_dir_v} fetch')
|
||||
git_status := gen_vc.cmd_exec('git -C ${git_repo_dir_v} status')
|
||||
if !git_status.contains('behind') && !gen_vc.options.force {
|
||||
gen_vc.logger.warn('v repository is already up to date.')
|
||||
return
|
||||
}
|
||||
}
|
||||
// delete repos
|
||||
gen_vc.purge_repos()
|
||||
// clone repos
|
||||
gen_vc.cmd_exec('git clone --filter=blob:none https://${git_repo_v} ${git_repo_dir_v}')
|
||||
gen_vc.cmd_exec('git clone --filter=blob:none https://${git_repo_vc} ${git_repo_dir_vc}')
|
||||
// get output of git log -1 (last commit)
|
||||
git_log_v :=
|
||||
gen_vc.cmd_exec('git -C ${git_repo_dir_v} log -1 --format="commit %H%nDate: %ci%nDate Unix: %ct%nSubject: %s"')
|
||||
git_log_vc :=
|
||||
gen_vc.cmd_exec('git -C ${git_repo_dir_vc} log -1 --format="Commit %H%nDate: %ci%nDate Unix: %ct%nSubject: %s"')
|
||||
// date of last commit in each repo
|
||||
ts_v := git_log_v.find_between('Date:', '\n').trim_space()
|
||||
ts_vc := git_log_vc.find_between('Date:', '\n').trim_space()
|
||||
// parse time as string to time.Time
|
||||
last_commit_time_v := time.parse(ts_v) or { panic(err) }
|
||||
last_commit_time_vc := time.parse(ts_vc) or { panic(err) }
|
||||
// git dates are in users local timezone and v time.parse does not parse
|
||||
// timezones at the moment, so for now get unix timestamp from output also
|
||||
t_unix_v := git_log_v.find_between('Date Unix:', '\n').trim_space().int()
|
||||
t_unix_vc := git_log_vc.find_between('Date Unix:', '\n').trim_space().int()
|
||||
// last commit hash in v repo
|
||||
last_commit_hash_v := git_log_v.find_between('commit', '\n').trim_space()
|
||||
last_commit_hash_v_short := last_commit_hash_v[..7]
|
||||
// subject
|
||||
last_commit_subject := git_log_v.find_between('Subject:', '\n').trim_space().replace("'", '"')
|
||||
// log some info
|
||||
gen_vc.logger.debug('last commit time (${git_repo_v}): ' + last_commit_time_v.format_ss())
|
||||
gen_vc.logger.debug('last commit time (${git_repo_vc}): ' + last_commit_time_vc.format_ss())
|
||||
gen_vc.logger.debug('last commit hash (${git_repo_v}): ${last_commit_hash_v}')
|
||||
gen_vc.logger.debug('last commit subject (${git_repo_v}): ${last_commit_subject}')
|
||||
// if vc repo already has a newer commit than the v repo, assume it's up to date
|
||||
if t_unix_vc >= t_unix_v && !gen_vc.options.force {
|
||||
gen_vc.logger.warn('vc repository is already up to date.')
|
||||
return
|
||||
}
|
||||
// try build v for current os (linux in this case)
|
||||
gen_vc.cmd_exec('make -C ${git_repo_dir_v}')
|
||||
v_exec := '${git_repo_dir_v}/v'
|
||||
// check if make was successful
|
||||
gen_vc.assert_file_exists_and_is_not_too_short(v_exec, err_msg_make)
|
||||
// build v.c for each os
|
||||
for os_name in vc_build_oses {
|
||||
c_file := if os_name == 'nix' { 'v.c' } else { 'v_win.c' }
|
||||
v_flags := if os_name == 'nix' { '-os cross' } else { '-os ${os_name}' }
|
||||
// try generate .c file
|
||||
gen_vc.cmd_exec('${v_exec} ${v_flags} -o ${c_file} ${git_repo_dir_v}/cmd/v')
|
||||
// check if the c file seems ok
|
||||
gen_vc.assert_file_exists_and_is_not_too_short(c_file, err_msg_gen_c)
|
||||
// embed the latest v commit hash into the c file
|
||||
gen_vc.cmd_exec('sed -i \'1s/^/#define V_COMMIT_HASH "${last_commit_hash_v_short}"\\n/\' ${c_file}')
|
||||
// move to vc repo
|
||||
gen_vc.cmd_exec('mv ${c_file} ${git_repo_dir_vc}/${c_file}')
|
||||
// add new .c file to local vc repo
|
||||
gen_vc.cmd_exec('git -C ${git_repo_dir_vc} add ${c_file}')
|
||||
}
|
||||
// check if the vc repo actually changed
|
||||
git_status := gen_vc.cmd_exec('git -C ${git_repo_dir_vc} status')
|
||||
if git_status.contains('nothing to commit') {
|
||||
gen_vc.logger.error('no changes to vc repo: something went wrong.')
|
||||
gen_vc.gen_error = true
|
||||
}
|
||||
// commit changes to local vc repo
|
||||
gen_vc.cmd_exec_safe("git -C ${git_repo_dir_vc} commit -m '[v:master] ${last_commit_hash_v_short} - ${last_commit_subject}'")
|
||||
// push changes to remote vc repo
|
||||
gen_vc.cmd_exec_safe('git -C ${git_repo_dir_vc} push https://${urllib.query_escape(git_username)}:${urllib.query_escape(git_password)}@${git_repo_vc} master')
|
||||
}
|
||||
|
||||
// only execute when dry_run option is false, otherwise just log
|
||||
fn (mut gen_vc GenVC) cmd_exec_safe(cmd string) string {
|
||||
return gen_vc.command_execute(cmd, gen_vc.options.dry_run)
|
||||
}
|
||||
|
||||
// always execute command
|
||||
fn (mut gen_vc GenVC) cmd_exec(cmd string) string {
|
||||
return gen_vc.command_execute(cmd, false)
|
||||
}
|
||||
|
||||
// execute command
|
||||
fn (mut gen_vc GenVC) command_execute(cmd string, dry bool) string {
|
||||
// if dry is true then dont execute, just log
|
||||
if dry {
|
||||
return gen_vc.command_execute_dry(cmd)
|
||||
}
|
||||
gen_vc.logger.info('cmd: ${cmd}')
|
||||
r := os.execute(cmd)
|
||||
if r.exit_code < 0 {
|
||||
gen_vc.logger.error('${err_msg_cmd_x}: "${cmd}" could not start.')
|
||||
gen_vc.logger.error(r.output)
|
||||
// something went wrong, better start fresh next time
|
||||
gen_vc.purge_repos()
|
||||
gen_vc.gen_error = true
|
||||
return ''
|
||||
}
|
||||
if r.exit_code != 0 {
|
||||
gen_vc.logger.error('${err_msg_cmd_x}: "${cmd}" failed.')
|
||||
gen_vc.logger.error(r.output)
|
||||
// something went wrong, better start fresh next time
|
||||
gen_vc.purge_repos()
|
||||
gen_vc.gen_error = true
|
||||
return ''
|
||||
}
|
||||
return r.output
|
||||
}
|
||||
|
||||
// just log cmd, dont execute
|
||||
fn (mut gen_vc GenVC) command_execute_dry(cmd string) string {
|
||||
gen_vc.logger.info('cmd (dry): "${cmd}"')
|
||||
return ''
|
||||
}
|
||||
|
||||
// delete repo directories
|
||||
fn (mut gen_vc GenVC) purge_repos() {
|
||||
// delete old repos (better to be fully explicit here, since these are destructive operations)
|
||||
mut repo_dir := '${gen_vc.options.work_dir}/${git_repo_dir_v}'
|
||||
if os.is_dir(repo_dir) {
|
||||
gen_vc.logger.info('purging local repo: "${repo_dir}"')
|
||||
gen_vc.cmd_exec('rm -rf ${repo_dir}')
|
||||
}
|
||||
repo_dir = '${gen_vc.options.work_dir}/${git_repo_dir_vc}'
|
||||
if os.is_dir(repo_dir) {
|
||||
gen_vc.logger.info('purging local repo: "${repo_dir}"')
|
||||
gen_vc.cmd_exec('rm -rf ${repo_dir}')
|
||||
}
|
||||
}
|
||||
|
||||
// check if file size is too short
|
||||
fn (mut gen_vc GenVC) assert_file_exists_and_is_not_too_short(f string, emsg string) {
|
||||
if !os.exists(f) {
|
||||
gen_vc.logger.error('${err_msg_build}: ${emsg} .')
|
||||
gen_vc.gen_error = true
|
||||
return
|
||||
}
|
||||
fsize := os.file_size(f)
|
||||
if fsize < too_short_file_limit {
|
||||
gen_vc.logger.error('${err_msg_build}: ${f} exists, but is too short: only ${fsize} bytes.')
|
||||
gen_vc.gen_error = true
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp
|
||||
|
||||
module main
|
||||
|
||||
import os
|
||||
import log
|
||||
|
||||
fn write_chunk(n int, min_size int, original string, start int, end int) {
|
||||
size := end - start
|
||||
if size < min_size {
|
||||
return
|
||||
}
|
||||
frame_file := 'frame_${n:06}.bin'
|
||||
log.warn('writing ${frame_file}, from start: ${start:9}, to end: ${end:9} | size: ${size:9} >= ${min_size:9}, memuse: ${gc_memory_use()}')
|
||||
os.write_file(frame_file, original#[start..end]) or { log.error(err.str()) }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
log.info('Start.')
|
||||
fpath := os.args[2] or { 'memdump.bin' }
|
||||
separator := os.args[3] or { '@@ gg_memory_trace_frame' }
|
||||
min_size := os.args[1] or { '${separator.len}' }.int()
|
||||
log.info('Splitting chunk min_size (arg 1): ${min_size}, file (arg 2): ${fpath}, by string separator (arg 3): `${separator}` ...')
|
||||
log.info('Memory use before reading: ${gc_memory_use()}')
|
||||
original := os.read_file(fpath)!
|
||||
log.info('file size: ${original.len}')
|
||||
log.info('Memory use after reading: ${gc_memory_use()}')
|
||||
mut n := 0
|
||||
for start := 0; start < original.len; {
|
||||
gc_collect()
|
||||
idx :=
|
||||
original.index_after_(separator, start + separator.len) // ensure that we advance each time
|
||||
if idx < 0 {
|
||||
write_chunk(n, min_size, original, start, original.len)
|
||||
break
|
||||
}
|
||||
write_chunk(n, min_size, original, start, idx)
|
||||
start = idx
|
||||
n++
|
||||
}
|
||||
log.info('Memory use after processing: ${gc_memory_use()}')
|
||||
log.info('Done.')
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp
|
||||
|
||||
import os
|
||||
import term
|
||||
|
||||
// This script can be used to ensure that all committed V files are vfmt-ed automatically.
|
||||
// By default, once setup, it will run `v fmt -w` on them, before committing them.
|
||||
|
||||
// To use the script in your V project, you need to be in the main folder
|
||||
// of your project, then do the equivalent of:
|
||||
// ```sh
|
||||
// cp /PATH/TO_YOUR/V/cmd/tools/git_pre_commit_hook.vsh .git/hooks/pre-commit
|
||||
// chmod 755 .git/hooks/pre-commit
|
||||
// ```
|
||||
//
|
||||
// You could also use `vgit-fmt-hook` tool to install/update/remove and get
|
||||
// status for this hook.
|
||||
// This tool copies the git hook VSH script in .git/hooks/pre-commit
|
||||
//
|
||||
// Note: you can use this command:
|
||||
// `git config --bool --add hooks.stopCommitOfNonVfmtedVFiles true`
|
||||
// ... to make it just *prevent* the committing of unformatted .v files,
|
||||
// i.e. stop the committing, if they are not, but *without modifying them*
|
||||
// automatically (you will then need to run `v fmt -w` on them manually).
|
||||
//
|
||||
// Note 2: Git supports skipping the hooks, by passing the `--no-verify` option.
|
||||
// That can be used to commit some .v files that are not formatted, without removing
|
||||
// the hook.
|
||||
|
||||
fn main() {
|
||||
// This hook cares only about the changed V files, that will be committed, as reported by git itself:
|
||||
changed :=
|
||||
os.execute('git diff --cached --name-only --diff-filter=ACMR -- "*.v" "*.vsh" "*.vv"')
|
||||
|
||||
all_changed_vfiles := changed.output.trim_space().split('\n')
|
||||
// _input.vv files are NOT formatted on purpose.
|
||||
// There is no point in verifying them, or ruining them over with `v fmt -w`.
|
||||
// Just filter them out, but still report to the user, that they will not be formatted:
|
||||
vfiles := all_changed_vfiles.filter(!it.ends_with('_input.vv'))
|
||||
input_vfiles := all_changed_vfiles.filter(it.ends_with('_input.vv'))
|
||||
if input_vfiles.len > 0 {
|
||||
eprintln('>>> ${input_vfiles.len} `_input.vv` files found, that *will NOT be* formatted.')
|
||||
for ifile in input_vfiles {
|
||||
eprintln(' ${ifile}')
|
||||
}
|
||||
}
|
||||
if changed.output == '' || vfiles.len == 0 {
|
||||
eprintln('>>> 0 changed V files, that may need formatting found.')
|
||||
exit(0)
|
||||
}
|
||||
configured_stop_committing := os.execute('git config --bool hooks.stopCommitOfNonVfmtedVFiles')
|
||||
if configured_stop_committing.output.trim_space().bool() {
|
||||
verify_result := os.execute('${os.quoted_path(@VEXE)} fmt -verify ${vfiles.join(' ')}')
|
||||
if verify_result.exit_code != 0 {
|
||||
eprintln(verify_result.output)
|
||||
}
|
||||
exit(verify_result.exit_code)
|
||||
} else {
|
||||
eprintln('The V pre commit hook will format ${vfiles.len} V file(s):')
|
||||
// vfmt off
|
||||
for vfile in vfiles {
|
||||
eprintln(' ${term.bold('${vfile}')}')
|
||||
}
|
||||
// vfmt on
|
||||
all_vfiles_on_a_line := vfiles.map(os.quoted_path(it)).join(' ')
|
||||
os.system('${os.quoted_path(@VEXE)} fmt -w ${all_vfiles_on_a_line}')
|
||||
os.system('git add ${all_vfiles_on_a_line}')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp
|
||||
|
||||
import os
|
||||
import net.http
|
||||
|
||||
const github_job = os.getenv('GITHUB_JOB')
|
||||
|
||||
struct JQ {
|
||||
tag_name string
|
||||
}
|
||||
|
||||
fn main() {
|
||||
root := os.real_path(os.dir(os.getenv_opt('VEXE') or { @VEXE }))
|
||||
os.chdir(root)! // make sure that the workfolder is stable
|
||||
|
||||
tloc := os.join_path(root, 'thirdparty')
|
||||
loc := os.join_path(tloc, 'binaryen')
|
||||
|
||||
if os.exists(loc) {
|
||||
eprintln('thirdparty/binaryen exists, will not overwrite')
|
||||
eprintln('delete the folder, and execute again')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: add retries here, github requests can fail
|
||||
jq := http.get_text('https://api.github.com/repos/WebAssembly/binaryen/releases/latest')
|
||||
tag := json.decode(JQ, jq)!.tag_name
|
||||
if github_job != '' {
|
||||
dump(jq)
|
||||
dump(tag)
|
||||
}
|
||||
*/
|
||||
tag := 'version_112'
|
||||
|
||||
name := $if windows {
|
||||
'x86_64-windows'
|
||||
} $else $if macos {
|
||||
$if arm64 {
|
||||
'arm64-macos'
|
||||
} $else {
|
||||
'x86_64-macos'
|
||||
}
|
||||
} $else $if linux {
|
||||
'x86_64-linux'
|
||||
} $else {
|
||||
eprintln('A premade binary library is not available for your system.')
|
||||
eprintln('Build it from source, following the documentation here: https://github.com/WebAssembly/binaryen/#building')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
fname := 'binaryen-${tag}'
|
||||
url := 'https://github.com/WebAssembly/binaryen/releases/download/${tag}/${fname}-${name}.tar.gz'
|
||||
|
||||
saveloc := os.join_path(tloc, '${fname}.tar.gz')
|
||||
if !os.exists(saveloc) {
|
||||
println('Downloading archive: ${saveloc}, from url: ${url} ...')
|
||||
http.download_file(url, saveloc)!
|
||||
// defer { os.rm(saveloc) or {}! }
|
||||
}
|
||||
|
||||
println('Extracting `${tloc}/${fname}` to `${tloc}/binaryen` ...')
|
||||
cmd := 'tar -xvf ${saveloc} --directory ${tloc}'
|
||||
if os.system(cmd) != 0 {
|
||||
eprintln('`${cmd}` exited with a non zero exit code')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println(cmd)
|
||||
println('Moving `${tloc}/${fname}` to `${tloc}/binaryen` ...')
|
||||
|
||||
os.rename_dir('${tloc}/${fname}', loc)!
|
||||
println('Done. You can now use `v -b wasm file.v` .')
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp
|
||||
|
||||
import os
|
||||
import net.http
|
||||
|
||||
fn main() {
|
||||
os.chdir(@VEXEROOT)! // make sure that the workfolder is stable
|
||||
tloc := os.join_path(@VEXEROOT, 'thirdparty')
|
||||
loc := os.join_path(tloc, 'wabt')
|
||||
if os.exists(loc) {
|
||||
eprintln('thirdparty/wabt exists, will not overwrite')
|
||||
eprintln('delete the folder, and execute again')
|
||||
exit(1)
|
||||
}
|
||||
tag := '1.0.32'
|
||||
fname := 'wabt-${tag}'
|
||||
mut platform := ''
|
||||
$if windows {
|
||||
platform = 'windows'
|
||||
} $else $if macos {
|
||||
platform = 'macos-14'
|
||||
} $else $if linux {
|
||||
platform = 'ubuntu'
|
||||
} $else {
|
||||
eprintln('A premade binary library is not available for your system.')
|
||||
eprintln('Build it from source, following the documentation here: https://github.com/WebAssembly/wabt/')
|
||||
exit(1)
|
||||
}
|
||||
url := 'https://github.com/WebAssembly/wabt/releases/download/${tag}/${fname}-${platform}.tar.gz'
|
||||
saveloc := os.join_path(tloc, '${fname}.tar.gz')
|
||||
println('>> Url: ${url}')
|
||||
println('>> Archive: ${saveloc}')
|
||||
if !os.exists(saveloc) {
|
||||
println('Downloading archive: ${saveloc}, from url: ${url} ...')
|
||||
http.download_file(url, saveloc)!
|
||||
// defer { os.rm(saveloc) or {}! }
|
||||
}
|
||||
println('Extracting `${tloc}/${fname}` to `${tloc}/wabt` ...')
|
||||
cmd := 'tar -xvf ${saveloc} --directory ${tloc}'
|
||||
if os.system(cmd) != 0 {
|
||||
eprintln('`${cmd}` exited with a non zero exit code')
|
||||
exit(1)
|
||||
}
|
||||
println(cmd)
|
||||
println('Moving `${tloc}/${fname}` to `${tloc}/wabt` ...')
|
||||
os.rename_dir('${tloc}/${fname}', loc)!
|
||||
println('Done. You can now use `v test vlib/wasm` .')
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
module file_lists
|
||||
|
||||
import os
|
||||
|
||||
// expand_files accepts a list of files and folders, and returns a list of all the .v and .vsh files, found in them.
|
||||
// The input list of files, supports recursive `@file.lst` expansion, where each line is treated as another file/folder.
|
||||
pub fn expand_files(files []string) ![]string {
|
||||
mut res := []string{}
|
||||
for file in files {
|
||||
if file == '' {
|
||||
continue
|
||||
}
|
||||
if file.starts_with('@') {
|
||||
lst_path := files[0].all_after('@').trim_space()
|
||||
listed_files := os.read_file(lst_path)!.split('\n').map(it.trim_space())
|
||||
res << expand_files(listed_files)!
|
||||
continue
|
||||
}
|
||||
if os.is_dir(file) {
|
||||
res << os.walk_ext(file, '.vsh')
|
||||
res << os.walk_ext(file, '.v')
|
||||
continue
|
||||
}
|
||||
if os.exists(file) {
|
||||
res << file
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import time
|
||||
import v.ast
|
||||
import v.fmt
|
||||
import v.pref
|
||||
import v.parser
|
||||
import v.errors
|
||||
import v.scanner
|
||||
import term
|
||||
import file_lists
|
||||
|
||||
const skip_tests = os.getenv('SKIP_TESTS').bool()
|
||||
const fuzzer_mode = os.getenv('VFUZZER').bool()
|
||||
const comments_mode = scanner.CommentsMode.from(os.getenv('SCANNER_MODE')) or {
|
||||
scanner.CommentsMode.parse_comments
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if !fuzzer_mode {
|
||||
dump(comments_mode)
|
||||
}
|
||||
all_files := file_lists.expand_files(os.args#[1..])!
|
||||
process_files(all_files)!
|
||||
}
|
||||
|
||||
fn hline() {
|
||||
if fuzzer_mode {
|
||||
return
|
||||
}
|
||||
println('----------------------------------------------------------------------------------------------------------------------------------------------------------')
|
||||
}
|
||||
|
||||
fn theader() {
|
||||
if fuzzer_mode {
|
||||
return
|
||||
}
|
||||
println(' Time Tokens Bytes Lines Bytes/Token Errors FMT.len')
|
||||
}
|
||||
|
||||
fn process_files(files []string) ! {
|
||||
nthreads := 1 // TODO
|
||||
mut pref_ := pref.new_preferences()
|
||||
pref_.is_fmt = true
|
||||
pref_.skip_warnings = true
|
||||
pref_.output_mode = .silent
|
||||
mut sw := time.new_stopwatch()
|
||||
mut total_us := i64(0)
|
||||
mut total_bytes := i64(0)
|
||||
mut total_tokens := i64(0)
|
||||
mut total_lines := i64(0)
|
||||
mut total_errors := i64(0)
|
||||
mut total_files := i64(0)
|
||||
mut total_fmt_len := i64(0)
|
||||
for f in files {
|
||||
mut table := ast.new_table()
|
||||
if f == '' {
|
||||
continue
|
||||
}
|
||||
if skip_tests && f.ends_with('_test.v') {
|
||||
continue
|
||||
}
|
||||
total_files++
|
||||
mut p := new_parser(f, comments_mode, table, pref_)
|
||||
ast_file := p.parse()
|
||||
///
|
||||
// do not measure the scanning, and parsing, but only the formatting:
|
||||
sw.restart()
|
||||
formatted_content := fmt.fmt(ast_file, mut table, pref_, false)
|
||||
f_us := sw.elapsed().microseconds()
|
||||
// eprint(formatted_content) // this should be identical to the output of `v fmt file.v`
|
||||
///
|
||||
total_us += f_us
|
||||
total_bytes += p.scanner.text.len
|
||||
total_tokens += p.scanner.all_tokens.len
|
||||
total_lines += ast_file.nr_lines
|
||||
total_errors += p.errors.len
|
||||
total_fmt_len += formatted_content.len
|
||||
if !fuzzer_mode {
|
||||
println('${f_us:10}us ${p.scanner.all_tokens.len:10} ${p.scanner.text.len:10} ${ast_file.nr_lines:10} ${(f64(p.scanner.text.len) / p.scanner.all_tokens.len):13.3} ${p.errors.len:10} ${formatted_content.len:8} ${f}')
|
||||
}
|
||||
}
|
||||
hline()
|
||||
theader()
|
||||
hline()
|
||||
speed_mb_s := term.colorize(term.bright_yellow, '${(f64(total_bytes) / total_us):6.3f} MB/s')
|
||||
speed_lines_s := term.colorize(term.bright_yellow,
|
||||
'${(1_000_000 * f64(total_lines) / total_us):10.1f} lines/s')
|
||||
println('${total_us:10}us ${total_tokens:10} ${total_bytes:10} ${total_lines:10} ${(f64(total_bytes) / total_tokens):13.3} ${total_errors:10} ${total_fmt_len:7} FMT speed: ${speed_mb_s}, ${speed_lines_s}, ${nthreads:3} thread(s), ${total_files:5} files.')
|
||||
}
|
||||
|
||||
fn new_parser(path string, comments_mode scanner.CommentsMode, table &ast.Table, pref_ &pref.Preferences) &parser.Parser {
|
||||
mut p := &parser.Parser{
|
||||
scanner: scanner.new_scanner_file(path, -1, comments_mode, pref_) or { panic(err) }
|
||||
table: table
|
||||
pref: pref_
|
||||
scope: &ast.Scope{
|
||||
start_pos: 0
|
||||
parent: table.global_scope
|
||||
}
|
||||
errors: []errors.Error{}
|
||||
warnings: []errors.Warning{}
|
||||
}
|
||||
p.set_path(path)
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import os
|
||||
import time
|
||||
import v.ast
|
||||
import v.pref
|
||||
import v.parser
|
||||
import v.errors
|
||||
import v.scanner
|
||||
import term
|
||||
import file_lists
|
||||
|
||||
const skip_tests = os.getenv('SKIP_TESTS').bool()
|
||||
const fuzzer_mode = os.getenv('VFUZZER').bool()
|
||||
const comments_mode = scanner.CommentsMode.from(os.getenv('SCANNER_MODE')) or {
|
||||
scanner.CommentsMode.skip_comments
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if !fuzzer_mode {
|
||||
dump(comments_mode)
|
||||
}
|
||||
all_files := file_lists.expand_files(os.args#[1..])!
|
||||
process_files(all_files)!
|
||||
}
|
||||
|
||||
fn hline() {
|
||||
if fuzzer_mode {
|
||||
return
|
||||
}
|
||||
println('---------------------------------------------------------------------------------------------------------------------------------------------------')
|
||||
}
|
||||
|
||||
fn theader() {
|
||||
if fuzzer_mode {
|
||||
return
|
||||
}
|
||||
println(' Time Tokens Bytes Lines Bytes/Token Errors')
|
||||
}
|
||||
|
||||
fn process_files(files []string) ! {
|
||||
nthreads := 1 // TODO
|
||||
mut pref_ := pref.new_preferences()
|
||||
pref_.is_fmt = true
|
||||
pref_.skip_warnings = true
|
||||
pref_.output_mode = .silent
|
||||
mut sw := time.new_stopwatch()
|
||||
mut total_us := i64(0)
|
||||
mut total_bytes := i64(0)
|
||||
mut total_tokens := i64(0)
|
||||
mut total_lines := i64(0)
|
||||
mut total_errors := i64(0)
|
||||
mut total_files := i64(0)
|
||||
for f in files {
|
||||
mut table := ast.new_table()
|
||||
if f == '' {
|
||||
continue
|
||||
}
|
||||
if skip_tests && f.ends_with('_test.v') {
|
||||
continue
|
||||
}
|
||||
// do not measure the scanning, but only the parsing:
|
||||
mut p := new_parser(f, comments_mode, table, pref_)
|
||||
if fuzzer_mode {
|
||||
p.scanner.max_eofs = 200
|
||||
}
|
||||
|
||||
sw.restart()
|
||||
ast_file := p.parse()
|
||||
f_us := sw.elapsed().microseconds()
|
||||
|
||||
total_us += f_us
|
||||
total_bytes += p.scanner.text.len
|
||||
total_tokens += p.scanner.all_tokens.len
|
||||
total_lines += ast_file.nr_lines
|
||||
total_errors += p.errors.len
|
||||
if !fuzzer_mode {
|
||||
println('${f_us:10}us ${p.scanner.all_tokens.len:10} ${p.scanner.text.len:10} ${ast_file.nr_lines:10} ${(f64(p.scanner.text.len) / p.scanner.all_tokens.len):13.3} ${p.errors.len:10} ${f}')
|
||||
}
|
||||
total_files++
|
||||
}
|
||||
hline()
|
||||
theader()
|
||||
hline()
|
||||
speed_mb_s := term.colorize(term.bright_yellow, '${(f64(total_bytes) / total_us):6.3f} MB/s')
|
||||
speed_lines_s := term.colorize(term.bright_yellow,
|
||||
'${(1_000_000 * f64(total_lines) / total_us):10.1f} lines/s')
|
||||
println('${total_us:10}us ${total_tokens:10} ${total_bytes:10} ${total_lines:10} ${(f64(total_bytes) / total_tokens):13.3} ${total_errors:10} Parser speed: ${speed_mb_s}, ${speed_lines_s}, ${nthreads:3} thread(s), ${total_files:5} files.')
|
||||
}
|
||||
|
||||
fn new_parser(path string, comments_mode scanner.CommentsMode, table &ast.Table, pref_ &pref.Preferences) &parser.Parser {
|
||||
mut p := &parser.Parser{
|
||||
scanner: scanner.new_scanner_file(path, -1, comments_mode, pref_) or { panic(err) }
|
||||
table: table
|
||||
pref: pref_
|
||||
scope: &ast.Scope{
|
||||
start_pos: 0
|
||||
parent: table.global_scope
|
||||
}
|
||||
errors: []errors.Error{}
|
||||
warnings: []errors.Warning{}
|
||||
}
|
||||
p.set_path(path)
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
import time
|
||||
import term
|
||||
import v.scanner
|
||||
import file_lists
|
||||
import v.pref
|
||||
|
||||
const skip_tests = os.getenv('SKIP_TESTS').bool()
|
||||
const fuzzer_mode = os.getenv('VFUZZER').bool()
|
||||
const comments_mode = scanner.CommentsMode.from(os.getenv('SCANNER_MODE')) or {
|
||||
scanner.CommentsMode.skip_comments
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if !fuzzer_mode {
|
||||
dump(comments_mode)
|
||||
}
|
||||
all_files := file_lists.expand_files(os.args#[1..])!
|
||||
process_files(all_files)!
|
||||
}
|
||||
|
||||
fn hline() {
|
||||
if fuzzer_mode {
|
||||
return
|
||||
}
|
||||
println('----------------------------------------------------------------------------------------------------------------------------------------------------')
|
||||
}
|
||||
|
||||
fn theader() {
|
||||
if fuzzer_mode {
|
||||
return
|
||||
}
|
||||
println(' Time Tokens Bytes Lines Bytes/Token Errors')
|
||||
}
|
||||
|
||||
fn process_files(files []string) ! {
|
||||
nthreads := 1 // TODO
|
||||
mut pref_ := pref.new_preferences()
|
||||
pref_.is_fmt = true
|
||||
pref_.skip_warnings = true
|
||||
pref_.output_mode = .silent
|
||||
mut sw := time.new_stopwatch()
|
||||
mut total_us := i64(0)
|
||||
mut total_bytes := i64(0)
|
||||
mut total_tokens := i64(0)
|
||||
mut total_lines := i64(0)
|
||||
mut total_errors := i64(0)
|
||||
mut total_files := i64(0)
|
||||
for f in files {
|
||||
if f == '' {
|
||||
continue
|
||||
}
|
||||
if skip_tests && f.ends_with('_test.v') {
|
||||
continue
|
||||
}
|
||||
total_files++
|
||||
sw.restart()
|
||||
s := scanner.new_scanner_file(f, -1, comments_mode, pref_)!
|
||||
f_us := sw.elapsed().microseconds()
|
||||
total_us += f_us
|
||||
total_bytes += s.text.len
|
||||
total_tokens += s.all_tokens.len
|
||||
total_lines += s.nr_lines
|
||||
total_errors += s.errors.len
|
||||
if !fuzzer_mode {
|
||||
println('${f_us:10}us ${s.all_tokens.len:10} ${s.text.len:10} ${s.nr_lines:10} ${(f64(s.text.len) / s.all_tokens.len):13.3f} ${s.errors.len:10} ${f}')
|
||||
}
|
||||
}
|
||||
hline()
|
||||
theader()
|
||||
hline()
|
||||
speed_mb_s := term.colorize(term.bright_yellow, '${(f64(total_bytes) / total_us):6.3f} MB/s')
|
||||
speed_lines_s := term.colorize(term.bright_yellow,
|
||||
'${(1_000_000 * f64(total_lines) / total_us):10.1f} lines/s')
|
||||
println('${total_us:10}us ${total_tokens:10} ${total_bytes:10} ${total_lines:10} ${(f64(total_bytes) / total_tokens):13.3} ${total_errors:10} Scanner speed: ${speed_mb_s}, ${speed_lines_s}, ${nthreads:3} thread(s), ${total_files:5} files.')
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
module scripting
|
||||
|
||||
import os
|
||||
import term
|
||||
import time
|
||||
|
||||
const term_colors = term.can_show_color_on_stdout()
|
||||
|
||||
pub fn set_verbose(on bool) {
|
||||
// setting a global here would be the obvious solution,
|
||||
// but V does not have globals normally.
|
||||
if on {
|
||||
os.setenv('VERBOSE', '1', true)
|
||||
} else {
|
||||
os.unsetenv('VERBOSE')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cprint(omessage string) {
|
||||
mut message := omessage
|
||||
if term_colors {
|
||||
message = term.cyan(message)
|
||||
}
|
||||
print(message)
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
pub fn cprint_strong(omessage string) {
|
||||
mut message := omessage
|
||||
if term_colors {
|
||||
message = term.bright_green(message)
|
||||
}
|
||||
print(message)
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
pub fn cprintln(omessage string) {
|
||||
cprint(omessage)
|
||||
println('')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
pub fn cprintln_strong(omessage string) {
|
||||
cprint_strong(omessage)
|
||||
println('')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
pub fn verbose_trace(label string, message string) {
|
||||
if os.getenv('VERBOSE').len > 0 {
|
||||
slabel := '${time.now().format_ss_milli()} ${label}'
|
||||
cprintln('# ${slabel:-43s} : ${message}')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verbose_trace_strong(label string, omessage string) {
|
||||
if os.getenv('VERBOSE').len > 0 {
|
||||
slabel := '${time.now().format_ss_milli()} ${label}'
|
||||
mut message := omessage
|
||||
if term_colors {
|
||||
message = term.bright_green(message)
|
||||
}
|
||||
cprintln('# ${slabel:-43s} : ${message}')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verbose_trace_exec_result(x os.Result) {
|
||||
if os.getenv('VERBOSE').len > 0 {
|
||||
cprintln('# cmd.exit_code : ${x.exit_code.str():-4s} cmd.output:')
|
||||
mut lnum := 1
|
||||
lines := x.output.split_into_lines()
|
||||
for oline in lines {
|
||||
mut line := oline
|
||||
if term_colors {
|
||||
line = term.bright_green(line)
|
||||
}
|
||||
cprintln('# ${lnum:3d}: ${line}')
|
||||
lnum++
|
||||
}
|
||||
cprintln('# ----------------------------------------------------------------------')
|
||||
}
|
||||
}
|
||||
|
||||
fn modfn(mname string, fname string) string {
|
||||
return '${mname}.${fname}'
|
||||
}
|
||||
|
||||
pub fn chdir(path string) {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), 'cd ${path}')
|
||||
os.chdir(path) or {
|
||||
verbose_trace(modfn(@MOD, @FN), '## failed.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mkdir(path string) ! {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), 'mkdir ${path}')
|
||||
os.mkdir(path) or {
|
||||
verbose_trace(modfn(@MOD, @FN), '## failed.')
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mkdir_all(path string) ! {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), 'mkdir -p ${path}')
|
||||
os.mkdir_all(path) or {
|
||||
verbose_trace(modfn(@MOD, @FN), '## failed.')
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rmrf(path string) {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), 'rm -rf ${path}')
|
||||
if os.exists(path) {
|
||||
if os.is_dir(path) {
|
||||
os.rmdir_all(path) or { panic(err) }
|
||||
} else {
|
||||
os.rm(path) or { panic(err) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// execute a command, and return a result, or an error, if it failed in any way.
|
||||
pub fn exec(cmd string) !os.Result {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), cmd)
|
||||
x := os.execute(cmd)
|
||||
if x.exit_code != 0 {
|
||||
verbose_trace(modfn(@MOD, @FN), '## failed.')
|
||||
return error(x.output)
|
||||
}
|
||||
verbose_trace_exec_result(x)
|
||||
return x
|
||||
}
|
||||
|
||||
// run a command, tracing its results, and returning ONLY its output
|
||||
pub fn run(cmd string) string {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), cmd)
|
||||
x := os.execute(cmd)
|
||||
if x.exit_code < 0 {
|
||||
verbose_trace(modfn(@MOD, @FN), '## failed.')
|
||||
return ''
|
||||
}
|
||||
verbose_trace_exec_result(x)
|
||||
if x.exit_code == 0 {
|
||||
return x.output.trim_right('\r\n')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// frun runs a command, tracing its results, and returning ONLY its output, or an error, if it failed
|
||||
pub fn frun(cmd string) !string {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), cmd)
|
||||
x := os.execute(cmd)
|
||||
if x.exit_code != 0 {
|
||||
verbose_trace(modfn(@MOD, @FN), '## failed.')
|
||||
verbose_trace(modfn(@MOD, @FN), '## failure code: ${x.exit_code}')
|
||||
verbose_trace(modfn(@MOD, @FN), '## failure output: ${x.output}')
|
||||
return error_with_code('failed cmd: ${cmd}', x.exit_code)
|
||||
}
|
||||
verbose_trace_exec_result(x)
|
||||
return x.output.trim_right('\r\n')
|
||||
}
|
||||
|
||||
pub fn exit_0_status(cmd string) bool {
|
||||
verbose_trace_strong(modfn(@MOD, @FN), cmd)
|
||||
x := os.execute(cmd)
|
||||
if x.exit_code < 0 {
|
||||
verbose_trace(modfn(@MOD, @FN), '## failed.')
|
||||
return false
|
||||
}
|
||||
verbose_trace_exec_result(x)
|
||||
if x.exit_code == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
pub fn tool_must_exist(toolcmd string) {
|
||||
verbose_trace(modfn(@MOD, @FN), toolcmd)
|
||||
where_is_cmd := if os.user_os() == 'windows' { 'where' } else { 'type' }
|
||||
if exit_0_status('${where_is_cmd} ${toolcmd}') {
|
||||
return
|
||||
}
|
||||
eprintln('Missing tool: ${toolcmd}')
|
||||
eprintln('Please try again after you install it.')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
pub fn used_tools_must_exist(tools []string) {
|
||||
for t in tools {
|
||||
tool_must_exist(t)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_sizes_of_files(files []string) {
|
||||
for f in files {
|
||||
size := os.file_size(f)
|
||||
println('${size} ${f}') // println('${size:10d} ${f}')
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,50 @@
|
||||
module testing
|
||||
|
||||
import time
|
||||
|
||||
pub enum MessageKind {
|
||||
compile_begin // sent right before *each* _test.v file compilation, the resulting status is not known yet, but the _test.v file itself is
|
||||
compile_end // sent right after *each* _test.v file compilation, the message contains the output of that compilation
|
||||
cmd_begin // sent right before *each* _test.v file execution, the resulting status is not known yet, but the _test.v file itself is
|
||||
cmd_end // sent right after *each* _test.v file execution, the message contains the output of that execution
|
||||
stats_output // captured `v -stats test` output that should be shown verbatim on stdout
|
||||
stats_error // captured `v -stats test` output for failed commands, shown verbatim on stderr
|
||||
|
||||
ok // success of a _test.v file
|
||||
fail // failed _test.v file, one or more assertions failed
|
||||
skip // the _test.v file was skipped for some reason
|
||||
info // a generic information message, detailing the actions of the `v test` program (some tests could be repeated for example, and the details are sent with an .info status)
|
||||
|
||||
cannot_compile // when the _test.v file compiled with errors
|
||||
|
||||
sentinel // send just once after all executions are done; it signals that the reporting/printing thread should stop the loop and exit
|
||||
}
|
||||
|
||||
pub struct LogMessage {
|
||||
pub:
|
||||
kind MessageKind // see the MessageKind declaration above
|
||||
file string // the _test.v file that the message is about
|
||||
when time.Time // when was the message sent (messages are sent by the execution threads at the *end* of each event)
|
||||
flow_id string // the messages of each thread, producing LogMessage, will have all the same unique flow_id. Messages by other threads will have other flow_id. If you use VJOBS=1 to serialise the execution, then all messages will have the same flow_id.
|
||||
took time.Duration // the duration of the event, that this message describes
|
||||
message string // the actual message text; the result of the event, that the message describes; most reporters could ignore this, since it could be reconstructed by the other fields
|
||||
}
|
||||
|
||||
pub interface Reporter {
|
||||
mut:
|
||||
session_start(message string, mut ts TestSession) // called once per test session, in the main thread, suitable for setting up supporting infrastructure.
|
||||
session_stop(message string, mut ts TestSession) // called once per test session, in the main thread, after everything else, suitable for summaries, creating .xml reports, uploads etc.
|
||||
worker_threads_start(files []string, mut ts TestSession) // called once per test session, in the main thread, right before all the worker threads start
|
||||
worker_threads_finish(mut ts TestSession) // called once per test session, in the main thread, right after all the worker threads finish
|
||||
|
||||
report(index int, log_msg LogMessage) // called once per each message, that will be shown (ok/fail/skip etc), only in the reporting thread.
|
||||
report_stop() // called just once after all messages are processed, only in the reporting thread, but before stop_session.
|
||||
|
||||
// TODO: reconsider, whether the next methods, should be kept for all reporters, or just moved inside the normal reporter, to simplify the interface
|
||||
progress(index int, message string)
|
||||
update_last_line(index int, message string)
|
||||
update_last_line_and_move_to_next(index int, message string)
|
||||
message(index int, message string)
|
||||
divider() // called to show a long ---------- horizontal line; can be safely ignored in most reporters; used in the main thread.
|
||||
list_of_failed_commands(cmds []string) // called after all testing is done, to produce a small summary that only lists the failed commands, so that they can be retried manually if needed, without forcing the user to scroll and find them.
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
module testing
|
||||
|
||||
// DumpReporter implements the interface testing.Reporter.
|
||||
// It is used by `v -test-runner dump test .`
|
||||
pub struct DumpReporter {
|
||||
mut:
|
||||
files []string
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
pub fn (mut r DumpReporter) worker_threads_start(files []string, mut ts TestSession) {
|
||||
eprintln('> ${@METHOD} | files: ${files}')
|
||||
r.files = files
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) worker_threads_finish(mut ts TestSession) {
|
||||
eprintln('> ${@METHOD}')
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
pub fn (r DumpReporter) session_start(message string, mut ts TestSession) {
|
||||
eprintln('> ${@METHOD} | message: ${message}')
|
||||
// dump(ts)
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) session_stop(message string, mut ts TestSession) {
|
||||
eprintln('> ${@METHOD} | message: ${message}')
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
pub fn (r DumpReporter) report(index int, message LogMessage) {
|
||||
eprintln('> ${@METHOD} | index: ${index} | message: ${message}')
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) report_stop() {
|
||||
eprintln('> ${@METHOD}')
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) progress(index int, message string) {
|
||||
eprintln('> ${@METHOD} | index: ${index} | message: ${message}')
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) update_last_line(index int, message string) {
|
||||
eprintln('> ${@METHOD} | index: ${index} | message: ${message}')
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) update_last_line_and_move_to_next(index int, message string) {
|
||||
eprintln('> ${@METHOD} | index: ${index} | message: ${message}')
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) message(index int, message string) {
|
||||
eprintln('> ${@METHOD} | index: ${index} | message: ${message}')
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) divider() {
|
||||
eprintln('> ${@METHOD}')
|
||||
}
|
||||
|
||||
pub fn (r DumpReporter) list_of_failed_commands(failed_cmds []string) {
|
||||
eprintln('> ${@METHOD} | failed_cmds: ${failed_cmds}')
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
module testing
|
||||
|
||||
import os
|
||||
import time
|
||||
import term
|
||||
import strings
|
||||
import runtime
|
||||
|
||||
pub const empty = term.header(' ', ' ')
|
||||
|
||||
// TODO: AGAIN --- this !!!*reliably*!!! fails compilation of `v cmd/tools/vbuild-examples.v` with a cgen error, without `-no-parallel`:
|
||||
// pub const report_running_period_ms = os.getenv_opt('VTEST_REPORT_RUNNING_PERIOD_MS') or { '60000' }.int() * time.millisecond
|
||||
|
||||
pub const report_running_period_ms = get_report_running_period_ms()
|
||||
|
||||
fn get_report_running_period_ms() time.Duration {
|
||||
return os.getenv_opt('VTEST_REPORT_RUNNING_PERIOD_MS') or { '300_000' }.int() * time.millisecond // 5 minutes by default
|
||||
}
|
||||
|
||||
// NormalReporter implements the interface testing.Reporter.
|
||||
// It is used by default by `v test .`
|
||||
// It was extracted by the original non customiseable output implementation directly in cmd/tools/modules/testing/common.v
|
||||
pub struct NormalReporter {
|
||||
mut:
|
||||
vroot string
|
||||
runtime time.Duration
|
||||
comptime time.Duration
|
||||
nfiles int
|
||||
njobs int
|
||||
//
|
||||
running shared map[string]LogMessage
|
||||
compiling shared map[string]LogMessage
|
||||
rtimes shared []TaskDuration
|
||||
ctimes shared []TaskDuration
|
||||
}
|
||||
|
||||
pub fn (mut r NormalReporter) session_start(message string, mut ts TestSession) {
|
||||
r.vroot = ts.vroot.replace('\\', '/') + '/'
|
||||
header(message)
|
||||
r.nfiles = ts.files.len
|
||||
r.njobs = runtime.nr_jobs()
|
||||
spawn r.report_current_running_and_compiling_status_periodically()
|
||||
}
|
||||
|
||||
fn (r &NormalReporter) report_current_running_and_compiling_status_periodically() {
|
||||
if report_running_period_ms == 0 {
|
||||
return
|
||||
}
|
||||
mut start_t := time.now()
|
||||
mut pi := 0
|
||||
mut ccompiling := map[string]LogMessage{}
|
||||
mut crunning := map[string]LogMessage{}
|
||||
mut sb := strings.new_builder(1024)
|
||||
for {
|
||||
pi++
|
||||
time.sleep(report_running_period_ms)
|
||||
t := time.now()
|
||||
rlock r.compiling {
|
||||
ccompiling = r.compiling.clone()
|
||||
}
|
||||
rlock r.running {
|
||||
crunning = r.running.clone()
|
||||
}
|
||||
ckeys := ccompiling.keys()
|
||||
rkeys := crunning.keys()
|
||||
sb.writeln('')
|
||||
sb.writeln(' >>>>> ${t.format_ss_micro()} | period ${pi:2} | started: ${t - start_t:10} ago | vjobs: ${r.njobs} | _test.v files: ${r.nfiles:5} | C: ${ckeys.len:3} | R: ${rkeys.len:3}')
|
||||
for ik, k in ckeys {
|
||||
cval := ccompiling[k]
|
||||
sb.writeln(' >>>>> compiling ${ik + 1:2}/${ckeys.len:-2}, T: ${cval.flow_id:2}, started: ${t - cval.when:10} ago, `${k}`')
|
||||
}
|
||||
for ik, k in rkeys {
|
||||
cval := crunning[k]
|
||||
sb.writeln(' >>>>> running ${ik + 1:2}/${rkeys.len:-2}, T: ${cval.flow_id:2}, started: ${t - cval.when:10} ago, `${k}`')
|
||||
}
|
||||
sb.writeln('')
|
||||
eprint(sb.str()) // write everything at once, to minimise the chance of interference with the normal output of `v test`
|
||||
}
|
||||
}
|
||||
|
||||
fn (r &NormalReporter) show_longest(label string, limit int, kind TaskKind) {
|
||||
if limit <= 0 {
|
||||
return
|
||||
}
|
||||
println('> Longest ${limit} by ${label}:')
|
||||
mut tasks := []TaskDuration{cap: r.nfiles}
|
||||
match kind {
|
||||
.comptime {
|
||||
rlock r.ctimes {
|
||||
tasks << r.ctimes
|
||||
}
|
||||
}
|
||||
.runtime {
|
||||
rlock r.rtimes {
|
||||
tasks << r.rtimes
|
||||
}
|
||||
}
|
||||
.totaltime {
|
||||
mut tall := []TaskDuration{}
|
||||
rlock r.rtimes {
|
||||
tall << r.rtimes
|
||||
}
|
||||
rlock r.ctimes {
|
||||
tall << r.ctimes
|
||||
}
|
||||
mut mall := map[string]TaskDuration{}
|
||||
for task in tall {
|
||||
if current := mall[task.path] {
|
||||
mall[task.path].duration = current.duration + task.duration
|
||||
continue
|
||||
}
|
||||
mall[task.path] = task
|
||||
}
|
||||
tasks = mall.values()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.sort(a.duration > b.duration)
|
||||
for tidx, task in tasks {
|
||||
npath := task.path.replace('\\', '/').replace(r.vroot, '')
|
||||
println(' ${tidx + 1:3} | ${task.duration:10s} | ${npath}')
|
||||
if tidx + 1 >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (r &NormalReporter) session_stop(message string, mut ts TestSession) {
|
||||
r.show_longest('compilation time', show_longest_by_comptime, .comptime)
|
||||
r.show_longest('run time', show_longest_by_runtime, .runtime)
|
||||
r.show_longest('total time', show_longest_by_totaltime, .totaltime)
|
||||
println('${ts.benchmark.total_message(message)} Comptime: ${r.comptime.microseconds() / 1000} ms. Runtime: ${r.runtime.microseconds() / 1000} ms.')
|
||||
}
|
||||
|
||||
// the most general form; it may be useful for other reporters
|
||||
// in the normal one, it currently does nothing
|
||||
pub fn (mut r NormalReporter) report(index int, message LogMessage) {
|
||||
// eprintln('> ${@METHOD} index: ${index} | message: ${message}')
|
||||
if message.kind == .compile_begin {
|
||||
lock r.compiling {
|
||||
r.compiling[message.file] = message
|
||||
}
|
||||
}
|
||||
if message.kind == .compile_end {
|
||||
r.comptime += message.took
|
||||
lock r.ctimes {
|
||||
r.ctimes << TaskDuration{message.file, message.took}
|
||||
}
|
||||
lock r.compiling {
|
||||
r.compiling.delete(message.file)
|
||||
}
|
||||
}
|
||||
if message.kind == .cmd_end {
|
||||
lock r.rtimes {
|
||||
r.rtimes << TaskDuration{message.file, message.took}
|
||||
}
|
||||
r.runtime += message.took
|
||||
}
|
||||
if message.kind == .cmd_begin {
|
||||
lock r.running {
|
||||
r.running[message.file] = message
|
||||
}
|
||||
}
|
||||
if message.kind == .cmd_end {
|
||||
lock r.running {
|
||||
r.running.delete(message.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (r NormalReporter) report_stop() {
|
||||
// eprintln('> ${@METHOD}')
|
||||
eprintln('')
|
||||
}
|
||||
|
||||
// progress will show the given message normally
|
||||
// TODO: reconsider if these should be public:
|
||||
pub fn (r NormalReporter) progress(index int, message string) {
|
||||
eprintln(message)
|
||||
}
|
||||
|
||||
// in progress mode, the last line will be rewritten many times, and does not end with \n
|
||||
// the \n will be printed just once when some progress has been made.
|
||||
pub fn (r NormalReporter) update_last_line(index int, message string) {
|
||||
print('\r${empty}\r${message}')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
pub fn (r NormalReporter) update_last_line_and_move_to_next(index int, message string) {
|
||||
// the last \n is needed, so SKIP/FAIL messages
|
||||
// will not get overwritten by the OK ones
|
||||
eprint('\r${empty}\r${message}\n')
|
||||
}
|
||||
|
||||
pub fn (r NormalReporter) message(index int, message string) {
|
||||
eprintln(message)
|
||||
}
|
||||
|
||||
pub fn (r NormalReporter) divider() {
|
||||
h_divider()
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
pub fn (r NormalReporter) worker_threads_start(files []string, mut ts TestSession) {
|
||||
// eprintln('> ${@METHOD}')
|
||||
}
|
||||
|
||||
pub fn (r NormalReporter) worker_threads_finish(mut ts TestSession) {
|
||||
// eprintln('> ${@METHOD}')
|
||||
}
|
||||
|
||||
pub fn (r NormalReporter) list_of_failed_commands(failed_cmds []string) {
|
||||
for i, cmd in failed_cmds {
|
||||
eprintln(term.failed('To reproduce just failure ${i + 1} run:') + ' ${cmd}')
|
||||
}
|
||||
if failed_cmds.len > 0 {
|
||||
vflags := os.getenv('VFLAGS')
|
||||
if vflags != '' {
|
||||
eprintln(term.failed('VFLAGS was: "${vflags}"'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TaskKind {
|
||||
comptime
|
||||
runtime
|
||||
totaltime
|
||||
}
|
||||
|
||||
struct TaskDuration {
|
||||
mut:
|
||||
path string
|
||||
duration time.Duration
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
module testing
|
||||
|
||||
// TeamcityReporter implements the interface `testing.Reporter`.
|
||||
// It is used by `v -test-runner teamcity test .`
|
||||
pub struct TeamcityReporter {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) session_start(message string, mut ts TestSession) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) session_stop(message string, mut ts TestSession) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) report(index int, message LogMessage) {
|
||||
name := r.get_test_suite_name_by_file(message.file)
|
||||
match message.kind {
|
||||
.cmd_begin {
|
||||
eprintln("##teamcity[testSuiteStarted name='${name}' flowId='${message.flow_id}']")
|
||||
}
|
||||
.cmd_end {
|
||||
eprintln("##teamcity[testSuiteFinished name='${name}' flowId='${message.flow_id}' duration='${message.took}']")
|
||||
}
|
||||
.cannot_compile {
|
||||
eprintln("##teamcity[testFailed name='${name}' message='${message.message}']")
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) get_test_suite_name_by_file(path string) string {
|
||||
file_name := path.replace('\\', '/').split('/').last()
|
||||
return file_name.split('.').first()
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) report_stop() {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) progress(index int, message string) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) update_last_line(index int, message string) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) update_last_line_and_move_to_next(index int, message string) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) message(index int, message string) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) divider() {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) worker_threads_start(files []string, mut ts TestSession) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) worker_threads_finish(mut ts TestSession) {
|
||||
}
|
||||
|
||||
pub fn (r TeamcityReporter) list_of_failed_commands(failed_cmds []string) {
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
module vbugreport
|
||||
|
||||
import os
|
||||
|
||||
pub struct StoredCErrorReport {
|
||||
pub:
|
||||
c_file_name string
|
||||
target_os string
|
||||
ccompiler string
|
||||
error_string string
|
||||
lines string
|
||||
v_lines string
|
||||
}
|
||||
|
||||
// new_stored_c_error_report builds the fields stored by the C error report receiver.
|
||||
// The generated C context is stored in `lines`, while the corresponding V source
|
||||
// context (the V line that caused the C error and its surrounding lines) is stored
|
||||
// separately in `v_lines`.
|
||||
pub fn new_stored_c_error_report(c_file string, target_os string, ccompiler string, c_error string, c_lines []string, v_lines []string) StoredCErrorReport {
|
||||
return StoredCErrorReport{
|
||||
c_file_name: normalized_file_name(c_file)
|
||||
target_os: target_os
|
||||
ccompiler: ccompiler
|
||||
error_string: c_error_string(c_error)
|
||||
lines: c_lines.join('\n')
|
||||
v_lines: v_lines.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_file_name(path string) string {
|
||||
return os.file_name(path.replace('\\', '/'))
|
||||
}
|
||||
|
||||
fn c_error_string(c_output string) string {
|
||||
for line in c_output.split_into_lines() {
|
||||
trimmed := line.trim_space()
|
||||
lower := trimmed.to_lower_ascii()
|
||||
if lower.starts_with('warning:') {
|
||||
continue
|
||||
}
|
||||
if error_string := normalized_error_string(trimmed, lower) {
|
||||
return error_string
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn normalized_error_string(trimmed string, lower string) ?string {
|
||||
if lower.starts_with('fatal error:') || lower.starts_with('fatal error ') {
|
||||
return 'error: ${trimmed}'
|
||||
}
|
||||
if lower.starts_with('error:') {
|
||||
return trimmed
|
||||
}
|
||||
if lower.starts_with('error ') {
|
||||
return 'error: ${trimmed['error '.len..]}'
|
||||
}
|
||||
for needle in [' fatal error:', ' fatal error '] {
|
||||
if idx := lower.index(needle) {
|
||||
return 'error: ${trimmed[idx + 1..]}'
|
||||
}
|
||||
}
|
||||
if idx := lower.index(' error:') {
|
||||
return trimmed[idx + 1..]
|
||||
}
|
||||
if idx := lower.index(' error ') {
|
||||
return 'error: ${trimmed[idx + ' error '.len..]}'
|
||||
}
|
||||
return none
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
module vbugreport
|
||||
|
||||
fn test_new_stored_c_error_report_extracts_sql_fields() {
|
||||
report := new_stored_c_error_report('/tmp/v/program.tmp.c', 'linux', 'clang',
|
||||
'/tmp/v/program.tmp.c:12:7: error: unknown type name "Foo"', [
|
||||
'void main__main(void) {',
|
||||
'\tFoo x;',
|
||||
], [
|
||||
'foo := Foo{}',
|
||||
])
|
||||
assert report.c_file_name == 'program.tmp.c'
|
||||
assert report.target_os == 'linux'
|
||||
assert report.ccompiler == 'clang'
|
||||
assert report.error_string == 'error: unknown type name "Foo"'
|
||||
assert report.lines == 'void main__main(void) {\n\tFoo x;'
|
||||
assert report.v_lines == 'foo := Foo{}'
|
||||
}
|
||||
|
||||
fn test_new_stored_c_error_report_handles_windows_c_file_path() {
|
||||
report := new_stored_c_error_report('C:\\tmp\\program.tmp.c', 'windows', 'msvc',
|
||||
'error: syntax error', []string{}, []string{})
|
||||
assert report.c_file_name == 'program.tmp.c'
|
||||
}
|
||||
|
||||
fn test_c_error_string_skips_warning_lines() {
|
||||
c_output := 'warning: unused command line argument\n warning: note before error\nerror: invalid conversion'
|
||||
assert c_error_string(c_output) == 'error: invalid conversion'
|
||||
}
|
||||
|
||||
fn test_c_error_string_handles_msvc_error_codes() {
|
||||
c_output := 'C:\\tmp\\program.tmp.c(19): error C2143: syntax error'
|
||||
assert c_error_string(c_output) == 'error: C2143: syntax error'
|
||||
}
|
||||
|
||||
fn test_c_error_string_preserves_fatal_error_diagnostics() {
|
||||
c_output := '/tmp/program.tmp.c:7:1: fatal error: missing.h: No such file or directory'
|
||||
assert c_error_string(c_output) == 'error: fatal error: missing.h: No such file or directory'
|
||||
}
|
||||
|
||||
fn test_c_error_string_returns_empty_without_error_line() {
|
||||
assert c_error_string('warning: unused command line argument\nnote: build stopped') == ''
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
module vbugreport
|
||||
|
||||
import net.urllib
|
||||
import os
|
||||
|
||||
const github_bug_issue_form_base_uri = 'https://github.com/vlang/v/issues/new?template=bug-report.yml'
|
||||
const github_issue_form_url_soft_limit = 7500
|
||||
|
||||
pub struct BugReport {
|
||||
description string
|
||||
reproduction string
|
||||
expected string
|
||||
current string
|
||||
solution string
|
||||
context string
|
||||
version string
|
||||
environment string
|
||||
}
|
||||
|
||||
struct IssueFormField {
|
||||
name string
|
||||
label string
|
||||
value string
|
||||
}
|
||||
|
||||
pub enum BugReportDeliveryMode {
|
||||
github_form
|
||||
local_report
|
||||
}
|
||||
|
||||
pub struct BugReportDelivery {
|
||||
pub:
|
||||
mode BugReportDeliveryMode
|
||||
uri string
|
||||
local_report_path string
|
||||
local_report_body string
|
||||
}
|
||||
|
||||
fn field_value_or_default(value string, default_value string) string {
|
||||
trimmed := value.trim_space()
|
||||
if trimmed.len == 0 {
|
||||
return default_value
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
fn fenced_block(language string, value string) string {
|
||||
content := field_value_or_default(value, 'N/A')
|
||||
return '```' + language + '\n' + content + '\n```'
|
||||
}
|
||||
|
||||
fn quoted_command(file_path string, generated_file string, user_args string) string {
|
||||
mut compile_cmd := './vdbg'
|
||||
if user_args.len > 0 {
|
||||
compile_cmd += ' ${user_args}'
|
||||
}
|
||||
compile_cmd += ' ${os.quoted_path(file_path)}'
|
||||
return './v -g -o vdbg cmd/v && ${compile_cmd} && ${os.quoted_path(os.real_path(generated_file))}'
|
||||
}
|
||||
|
||||
// new_bug_report builds the structured fields used by the GitHub bug report form.
|
||||
pub fn new_bug_report(file_path string, generated_file string, user_args string, expected_result string, version string, vdoctor_output string, file_content string, build_output string) BugReport {
|
||||
command := quoted_command(file_path, generated_file, user_args)
|
||||
source_name := os.file_name(file_path)
|
||||
return BugReport{
|
||||
description: 'Running `${command}` on `${source_name}` produced a compiler error.'
|
||||
reproduction: '${fenced_block('sh', command)}\n\n${fenced_block('v', file_content)}'
|
||||
expected: field_value_or_default(expected_result, 'N/A')
|
||||
current: fenced_block('', build_output)
|
||||
solution: '_No response_'
|
||||
context: 'Generated by `v bug` from `${file_path}`.'
|
||||
version: field_value_or_default(version, 'N/A')
|
||||
environment: fenced_block('', vdoctor_output)
|
||||
}
|
||||
}
|
||||
|
||||
// issue_form_fields must stay aligned with `.github/ISSUE_TEMPLATE/bug-report.yml`.
|
||||
fn issue_form_fields(report BugReport) []IssueFormField {
|
||||
return [
|
||||
IssueFormField{
|
||||
name: 'description'
|
||||
label: 'Describe the bug'
|
||||
value: report.description
|
||||
},
|
||||
IssueFormField{
|
||||
name: 'reproduction'
|
||||
label: 'Reproduction Steps'
|
||||
value: report.reproduction
|
||||
},
|
||||
IssueFormField{
|
||||
name: 'expected'
|
||||
label: 'Expected Behavior'
|
||||
value: report.expected
|
||||
},
|
||||
IssueFormField{
|
||||
name: 'current'
|
||||
label: 'Current Behavior'
|
||||
value: report.current
|
||||
},
|
||||
IssueFormField{
|
||||
name: 'solution'
|
||||
label: 'Possible Solution'
|
||||
value: report.solution
|
||||
},
|
||||
IssueFormField{
|
||||
name: 'context'
|
||||
label: 'Additional Information/Context'
|
||||
value: report.context
|
||||
},
|
||||
IssueFormField{
|
||||
name: 'version'
|
||||
label: 'V version'
|
||||
value: report.version
|
||||
},
|
||||
IssueFormField{
|
||||
name: 'environment'
|
||||
label: 'Environment details (OS name and version, etc.)'
|
||||
value: report.environment
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn github_issue_form_uri(report BugReport) string {
|
||||
mut query_params := []string{}
|
||||
for field in issue_form_fields(report) {
|
||||
query_params << '${field.name}=${urllib.query_escape(field.value)}'
|
||||
}
|
||||
return '${github_bug_issue_form_base_uri}&${query_params.join('&')}'
|
||||
}
|
||||
|
||||
fn bug_report_markdown(report BugReport) string {
|
||||
mut lines := [
|
||||
'# V Bug Report',
|
||||
'',
|
||||
'Generated by `v bug`.',
|
||||
'',
|
||||
]
|
||||
for field in issue_form_fields(report) {
|
||||
lines << '### ${field.label}'
|
||||
lines << ''
|
||||
lines << field.value
|
||||
lines << ''
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
fn bug_report_file_path(file_path string) string {
|
||||
base_name := os.file_name(file_path)
|
||||
stem := if base_name.contains('.') { base_name.all_before_last('.') } else { base_name }
|
||||
file_name := if stem.len == 0 { 'v-bug-report.md' } else { '${stem}.bug-report.md' }
|
||||
file_dir := os.dir(file_path)
|
||||
return if file_dir.len == 0 || file_dir == '.' {
|
||||
file_name
|
||||
} else {
|
||||
os.join_path(file_dir, file_name)
|
||||
}
|
||||
}
|
||||
|
||||
// prepare_bug_report_delivery chooses between a prefilled issue-form URL and a local markdown fallback.
|
||||
pub fn prepare_bug_report_delivery(report BugReport, file_path string) BugReportDelivery {
|
||||
uri := github_issue_form_uri(report)
|
||||
if uri.len <= github_issue_form_url_soft_limit {
|
||||
return BugReportDelivery{
|
||||
mode: .github_form
|
||||
uri: uri
|
||||
}
|
||||
}
|
||||
return BugReportDelivery{
|
||||
mode: .local_report
|
||||
uri: github_bug_issue_form_base_uri
|
||||
local_report_path: bug_report_file_path(file_path)
|
||||
local_report_body: bug_report_markdown(report)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
module vbugreport
|
||||
|
||||
import os
|
||||
import net.urllib
|
||||
|
||||
fn sample_bug_report() BugReport {
|
||||
return BugReport{
|
||||
description: 'Running `./vdbg examples/fail.v` produced a compiler error.'
|
||||
reproduction: '```sh\n./vdbg examples/fail.v\n```\n\n```v\nfn main() {}\n```'
|
||||
expected: 'An error message.'
|
||||
current: '```\nbuilder error\n```'
|
||||
solution: '_No response_'
|
||||
context: 'Generated by `v bug`.'
|
||||
version: 'V 0.0.0 deadbeef'
|
||||
environment: '```\nOS: linux\n```'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_github_issue_form_uri_prefills_bug_report_fields() {
|
||||
report := sample_bug_report()
|
||||
uri := github_issue_form_uri(report)
|
||||
assert uri.starts_with(github_bug_issue_form_base_uri + '&')
|
||||
assert !uri.contains('body=')
|
||||
|
||||
query := urllib.parse_query(uri.all_after('?')) or { panic(err) }
|
||||
assert (query.get('template') or { panic(err) }) == 'bug-report.yml'
|
||||
assert (query.get('description') or { panic(err) }) == report.description
|
||||
assert (query.get('reproduction') or { panic(err) }) == report.reproduction
|
||||
assert (query.get('expected') or { panic(err) }) == report.expected
|
||||
assert (query.get('current') or { panic(err) }) == report.current
|
||||
assert (query.get('solution') or { panic(err) }) == report.solution
|
||||
assert (query.get('context') or { panic(err) }) == report.context
|
||||
assert (query.get('version') or { panic(err) }) == report.version
|
||||
assert (query.get('environment') or { panic(err) }) == report.environment
|
||||
}
|
||||
|
||||
fn test_prepare_bug_report_delivery_falls_back_to_local_report_for_large_reports() {
|
||||
report := BugReport{
|
||||
description: 'Running `./vdbg examples/fail.v` produced a compiler error.'
|
||||
reproduction: '```sh\n./vdbg examples/fail.v\n```\n\n```v\nfn main() {}\n```'
|
||||
expected: 'An error message.'
|
||||
current: 'X'.repeat(github_issue_form_url_soft_limit)
|
||||
solution: '_No response_'
|
||||
context: 'Generated by `v bug`.'
|
||||
version: 'V 0.0.0 deadbeef'
|
||||
environment: '```\nOS: linux\n```'
|
||||
}
|
||||
delivery := prepare_bug_report_delivery(report, os.join_path('examples', 'foo', 'bar.v'))
|
||||
assert delivery.mode == .local_report
|
||||
assert delivery.uri == github_bug_issue_form_base_uri
|
||||
assert delivery.local_report_path == os.join_path('examples', 'foo', 'bar.bug-report.md')
|
||||
assert delivery.local_report_body.starts_with('# V Bug Report')
|
||||
assert delivery.local_report_body.contains('### Current Behavior')
|
||||
assert delivery.local_report_body.contains(report.current)
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
module vgit
|
||||
|
||||
import os
|
||||
import flag
|
||||
import scripting
|
||||
|
||||
pub fn check_v_commit_timestamp_before_self_rebuilding(v_timestamp u64) {
|
||||
if v_timestamp >= 1561805697 {
|
||||
return
|
||||
}
|
||||
eprintln('##################################################################')
|
||||
eprintln('# WARNING: v self rebuilding, before 5b7a1e8 (2019-06-29 12:21) #')
|
||||
eprintln('# required the v executable to be built *inside* #')
|
||||
eprintln('# the toplevel compiler/ folder. #')
|
||||
eprintln('# #')
|
||||
eprintln('# That is not supported by this tool. #')
|
||||
eprintln('# You will have to build it manually there. #')
|
||||
eprintln('##################################################################')
|
||||
}
|
||||
|
||||
pub fn validate_commit_exists(commit string) {
|
||||
if commit != '' {
|
||||
return
|
||||
}
|
||||
cmd := 'git cat-file -t "${commit}" ' // windows's cmd.exe does not support ' for quoting
|
||||
if !scripting.exit_0_status(cmd) {
|
||||
eprintln('Commit: "${commit}" does not exist in the current repository.')
|
||||
exit(3)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_to_timestamp_and_commit(line string) (u64, string) {
|
||||
parts := line.split(' ')
|
||||
return parts[0].u64(), parts[1]
|
||||
}
|
||||
|
||||
pub fn normalized_workpath_for_commit(workdir string, commit string) string {
|
||||
nc := 'v_at_' + commit.replace('^', '_').replace('-', '_').replace('/', '_')
|
||||
return os.real_path(workdir + os.path_separator + nc)
|
||||
}
|
||||
|
||||
fn get_current_folder_commit_hash() string {
|
||||
vline := scripting.run('git rev-list -n1 --timestamp HEAD')
|
||||
_, v_commithash := line_to_timestamp_and_commit(vline)
|
||||
return v_commithash
|
||||
}
|
||||
|
||||
@[noreturn]
|
||||
fn fatal_error(error IError, label string) {
|
||||
eprintln('error: ${label}')
|
||||
eprintln(error)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
@[noreturn]
|
||||
fn co_fail(error IError, commit string) {
|
||||
fatal_error(error, 'git could not checkout `${commit}`')
|
||||
}
|
||||
|
||||
@[noreturn]
|
||||
fn net_fail(error IError, what string) {
|
||||
fatal_error(error, 'git failed at `${what}`')
|
||||
}
|
||||
|
||||
pub fn prepare_vc_source(vcdir string, cdir string, commit string) (string, string, u64) {
|
||||
scripting.chdir(cdir)
|
||||
// Building a historic v with the latest vc is not always possible ...
|
||||
// It is more likely, that the vc *at the time of the v commit*,
|
||||
// or slightly before that time will be able to build the historic v:
|
||||
vline := scripting.run('git rev-list -n1 --timestamp "${commit}" ')
|
||||
v_timestamp, v_commithash := line_to_timestamp_and_commit(vline)
|
||||
scripting.verbose_trace(@FN, 'v_timestamp: ${v_timestamp} | v_commithash: ${v_commithash}')
|
||||
check_v_commit_timestamp_before_self_rebuilding(v_timestamp)
|
||||
scripting.chdir(vcdir)
|
||||
scripting.frun('git checkout --quiet master') or { co_fail(err, 'master') }
|
||||
|
||||
mut vccommit := ''
|
||||
mut partial_hash := v_commithash[0..7]
|
||||
if '5b7a1e8'.starts_with(partial_hash) {
|
||||
// we need the following, otherwise --grep= below would find a93ef6e, which does include 5b7a1e8 in the commit message ... 🤦♂️
|
||||
partial_hash = '5b7a1e84a4d283071d12cb86dc17aeda9b5306a8'
|
||||
}
|
||||
vcbefore_subject_match :=
|
||||
scripting.run('git rev-list HEAD -n1 --timestamp --grep=${partial_hash} ')
|
||||
scripting.verbose_trace(@FN, 'vcbefore_subject_match: ${vcbefore_subject_match}')
|
||||
if vcbefore_subject_match.len > 3 {
|
||||
_, vccommit = line_to_timestamp_and_commit(vcbefore_subject_match)
|
||||
} else {
|
||||
scripting.verbose_trace(@FN,
|
||||
'the v commit did not match anything in the vc log; try --timestamp instead.')
|
||||
vcbefore := scripting.run('git rev-list HEAD -n1 --timestamp --before=${v_timestamp} ')
|
||||
_, vccommit = line_to_timestamp_and_commit(vcbefore)
|
||||
}
|
||||
scripting.verbose_trace(@FN, 'vccommit: ${vccommit}')
|
||||
scripting.frun('git checkout --quiet "${vccommit}" ') or { co_fail(err, vccommit) }
|
||||
scripting.run('wc *.c')
|
||||
scripting.chdir(cdir)
|
||||
return v_commithash, vccommit, v_timestamp
|
||||
}
|
||||
|
||||
pub fn clone_or_pull(remote_git_url string, local_worktree_path string) {
|
||||
// Note: after clone_or_pull, the current repo branch is === HEAD === master
|
||||
if os.is_dir(local_worktree_path) && os.is_dir(os.join_path_single(local_worktree_path, '.git')) {
|
||||
// Already existing ... Just pulling in this case is faster usually.
|
||||
scripting.frun('git -C "${local_worktree_path}" checkout --quiet master') or {
|
||||
co_fail(err, 'master')
|
||||
}
|
||||
scripting.frun('git -C "${local_worktree_path}" pull --quiet ') or {
|
||||
net_fail(err, 'pulling')
|
||||
}
|
||||
} else {
|
||||
// Clone a fresh local tree.
|
||||
if remote_git_url.starts_with('http') {
|
||||
// cloning an https remote with --filter=blob:none is usually much less bandwidth intensive, at the
|
||||
// expense of doing small network ops later when using checkouts.
|
||||
scripting.frun('git clone --filter=blob:none --quiet "${remote_git_url}" "${local_worktree_path}" ') or {
|
||||
net_fail(err, 'cloning')
|
||||
}
|
||||
return
|
||||
}
|
||||
mut is_blobless_clone := false
|
||||
remote_git_config_path := os.join_path(remote_git_url, '.git', 'config')
|
||||
if os.is_dir(remote_git_url) && os.is_file(remote_git_config_path) {
|
||||
lines := os.read_lines(remote_git_config_path) or { [] }
|
||||
is_blobless_clone = lines.any(it.contains('partialclonefilter = blob:none'))
|
||||
}
|
||||
if is_blobless_clone {
|
||||
// Note:
|
||||
// 1) cloning a *local folder* with `--filter=blob:none`, that *itself* was cloned with `--filter=blob:none`
|
||||
// leads to *extremely* slow checkouts for older commits later. It takes hours instead of milliseconds, for a commit
|
||||
// that is just several thousands of commits old :( .
|
||||
//
|
||||
// 2) Cloning it *without* the `--filter=blob:none`, leads to `error: unable to read sha1 file of`, later,
|
||||
// when checking out the older commits, depending on the local git client version (tested with git version 2.41.0).
|
||||
//
|
||||
// 3) => instead of cloning, it is much faster, and *bug free*, to just rsync the local repo directly,
|
||||
// at the expense of a little more space usage, which will make the new tree in local_worktree_path,
|
||||
// exactly 1:1 the same, as the one in remote_git_url, just independent from it .
|
||||
copy_cmd := if os.user_os() == 'windows' { 'robocopy /MIR' } else { 'rsync -a' }
|
||||
scripting.frun('${copy_cmd} "${remote_git_url}/" "${local_worktree_path}/"') or {
|
||||
fatal_error(err, 'copying to ${local_worktree_path}')
|
||||
}
|
||||
return
|
||||
}
|
||||
scripting.frun('git clone --quiet "${remote_git_url}" "${local_worktree_path}" ') or {
|
||||
net_fail(err, 'cloning')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VGitContext {
|
||||
pub:
|
||||
cc string = 'cc' // what C compiler to use for bootstrapping
|
||||
cc_options string // what additional C compiler options to use for bootstrapping
|
||||
workdir string = '/tmp' // the base working folder
|
||||
commit_v string = 'master' // the commit-ish that needs to be prepared
|
||||
path_v string // where is the local working copy v repo
|
||||
path_vc string // where is the local working copy vc repo
|
||||
v_repo_url string // the remote v repo URL
|
||||
vc_repo_url string // the remote vc repo URL
|
||||
pub mut:
|
||||
// these will be filled by vgitcontext.compile_oldv_if_needed()
|
||||
commit_v__hash string // the git commit of the v repo that should be prepared
|
||||
commit_vc_hash string // the git commit of the vc repo, corresponding to commit_v__hash
|
||||
commit_v__ts u64 // unix timestamp, that corresponds to commit_v__hash; filled by prepare_vc_source
|
||||
vexename string // v or v.exe
|
||||
vexepath string // the full absolute path to the prepared v/v.exe
|
||||
vvlocation string // v.v or compiler/ or cmd/v, depending on v version
|
||||
make_fresh_tcc bool // whether to do 'make fresh_tcc' before compiling an old V.
|
||||
show_vccommit bool // show the V and VC commits, corresponding to the V commit-ish, that can be used to build V
|
||||
}
|
||||
|
||||
pub fn (mut vgit_context VGitContext) compile_oldv_if_needed() {
|
||||
vgit_context.vexename = if os.user_os() == 'windows' { 'v.exe' } else { 'v' }
|
||||
vgit_context.vexepath = os.real_path(os.join_path_single(vgit_context.path_v,
|
||||
vgit_context.vexename))
|
||||
if os.is_dir(vgit_context.path_v) && os.is_executable(vgit_context.vexepath)
|
||||
&& !vgit_context.show_vccommit {
|
||||
// already compiled, no need to compile that specific v executable again
|
||||
vgit_context.commit_v__hash = get_current_folder_commit_hash()
|
||||
return
|
||||
}
|
||||
scripting.chdir(vgit_context.workdir)
|
||||
clone_or_pull(vgit_context.v_repo_url, vgit_context.path_v)
|
||||
clone_or_pull(vgit_context.vc_repo_url, vgit_context.path_vc)
|
||||
scripting.chdir(vgit_context.path_v)
|
||||
scripting.frun('git checkout --quiet ${vgit_context.commit_v}') or {
|
||||
co_fail(err, vgit_context.commit_v)
|
||||
}
|
||||
if os.is_dir(vgit_context.path_v) && os.exists(vgit_context.vexepath)
|
||||
&& !vgit_context.show_vccommit {
|
||||
// already compiled, so no need to compile v again
|
||||
vgit_context.commit_v__hash = get_current_folder_commit_hash()
|
||||
return
|
||||
}
|
||||
v_commithash, vccommit_before, v_timestamp := prepare_vc_source(vgit_context.path_vc,
|
||||
vgit_context.path_v, 'HEAD')
|
||||
vgit_context.commit_v__hash = v_commithash
|
||||
vgit_context.commit_v__ts = v_timestamp
|
||||
vgit_context.commit_vc_hash = vccommit_before
|
||||
|
||||
if vgit_context.show_vccommit {
|
||||
println('VHASH=${vgit_context.commit_v__hash}')
|
||||
println('VCHASH=${vgit_context.commit_vc_hash}')
|
||||
exit(0)
|
||||
}
|
||||
|
||||
if os.exists('cmd/v') {
|
||||
vgit_context.vvlocation = 'cmd/v'
|
||||
} else {
|
||||
vgit_context.vvlocation = if os.exists('v.v') { 'v.v' } else { 'compiler' }
|
||||
}
|
||||
if os.is_dir(vgit_context.path_v) && os.exists(vgit_context.vexepath) {
|
||||
// already compiled, so no need to compile v again
|
||||
return
|
||||
}
|
||||
|
||||
scripting.chdir(vgit_context.path_v)
|
||||
// Recompilation is needed. Just to be sure, clean up everything first.
|
||||
scripting.run('git clean -xf')
|
||||
if vgit_context.make_fresh_tcc {
|
||||
scripting.run('make fresh_tcc')
|
||||
}
|
||||
|
||||
// compiling the C sources with a C compiler:
|
||||
mut command_for_building_v_from_c_source := ''
|
||||
mut command_for_selfbuilding := ''
|
||||
mut c_flags := '-std=gnu11 -I ./thirdparty/stdatomic/nix -w'
|
||||
mut c_ldflags := '-lm -lpthread'
|
||||
mut vc_source_file_location := os.join_path_single(vgit_context.path_vc, 'v.c')
|
||||
mut vc_v_cpermissive_flags := '${vgit_context.cc_options} -Wno-error=incompatible-pointer-types -Wno-error=implicit-function-declaration -Wno-error=int-conversion -fpermissive'
|
||||
// after 85b58b0 2021-09-28, -no-parallel is supported, and can be used to force the cgen stage to be single threaded, which increases the chances of successful bootstraps
|
||||
mut vc_v_bootstrap_flags := ''
|
||||
if vgit_context.commit_v__ts >= 1632778086 {
|
||||
vc_v_bootstrap_flags += ' -no-parallel'
|
||||
}
|
||||
vc_v_bootstrap_flags = vc_v_bootstrap_flags.trim_space()
|
||||
scripting.verbose_trace(@FN, 'vc_v_bootstrap_flags: ${vc_v_bootstrap_flags}')
|
||||
scripting.verbose_trace(@FN, 'vc_v_cpermissive_flags: ${vc_v_cpermissive_flags}')
|
||||
scripting.verbose_trace(@FN, 'vgit_context.commit_v__ts: ${vgit_context.commit_v__ts}')
|
||||
|
||||
if 'windows' == os.user_os() {
|
||||
c_flags = '-std=c99 -I ./thirdparty/stdatomic/win -w'
|
||||
c_ldflags = ''
|
||||
v_win_c_location := os.join_path_single(vgit_context.path_vc, 'v_win.c')
|
||||
if os.exists(v_win_c_location) {
|
||||
vc_source_file_location = v_win_c_location
|
||||
}
|
||||
}
|
||||
if 'windows' == os.user_os() {
|
||||
if vgit_context.commit_v__ts >= 1589793086 && vgit_context.cc.contains('gcc') {
|
||||
// after 53ffee1 2020-05-18, gcc builds on windows do need `-municode`
|
||||
c_flags += '-municode'
|
||||
}
|
||||
// after 2023-11-07, windows builds need linking to ws2_32:
|
||||
if vgit_context.commit_v__ts >= 1699341818 && !vgit_context.cc.contains('msvc') {
|
||||
c_flags += '-lws2_32'
|
||||
}
|
||||
command_for_building_v_from_c_source = c(vgit_context.cc,
|
||||
'${vc_v_cpermissive_flags} ${c_flags} -o cv.exe "${vc_source_file_location}" ${c_ldflags}')
|
||||
command_for_selfbuilding = c('.\\cv.exe',
|
||||
'${vc_v_bootstrap_flags} -cflags "${vc_v_cpermissive_flags}" -o ${vgit_context.vexename} {SOURCE}')
|
||||
} else {
|
||||
command_for_building_v_from_c_source = c(vgit_context.cc,
|
||||
'${vc_v_cpermissive_flags} ${c_flags} -o cv "${vc_source_file_location}" ${c_ldflags}')
|
||||
command_for_selfbuilding = c('./cv',
|
||||
'${vc_v_bootstrap_flags} -cflags "${vc_v_cpermissive_flags}" -o ${vgit_context.vexename} {SOURCE}')
|
||||
}
|
||||
|
||||
scripting.run(command_for_building_v_from_c_source)
|
||||
build_cmd := command_for_selfbuilding.replace('{SOURCE}', vgit_context.vvlocation)
|
||||
scripting.run(build_cmd)
|
||||
// At this point, there exists a file vgit_context.vexepath
|
||||
// which should be a valid working V executable.
|
||||
}
|
||||
|
||||
fn c(cmd string, params string) string {
|
||||
// compose a command, while reducing the potential whitespaces, due to all the interpolations of optional flags above
|
||||
return '${cmd} ${params.trim_space()}'
|
||||
}
|
||||
|
||||
pub struct VGitOptions {
|
||||
pub mut:
|
||||
workdir string = os.temp_dir() // the working folder (typically /tmp), where the tool will write
|
||||
v_repo_url string // the url of the V repository. It can be a local folder path, if you want to eliminate network operations...
|
||||
vc_repo_url string // the url of the vc repository. It can be a local folder path, if you want to eliminate network operations...
|
||||
show_help bool // whether to show the usage screen
|
||||
verbose bool // should the tool be much more verbose
|
||||
}
|
||||
|
||||
pub fn add_common_tool_options(mut context VGitOptions, mut fp flag.FlagParser) []string {
|
||||
context.workdir = os.real_path(fp.string('workdir', `w`, context.workdir,
|
||||
'A writable base folder. Default: ${context.workdir}'))
|
||||
context.v_repo_url = fp.string('vrepo', 0, context.v_repo_url,
|
||||
'The url of the V repository. You can clone it locally too. See also --vcrepo below.')
|
||||
context.vc_repo_url = fp.string('vcrepo', 0, context.vc_repo_url, 'The url of the vc repository. You can clone it
|
||||
${flag.space}beforehand, and then just give the local folder
|
||||
${flag.space}path here. That will eliminate the network ops
|
||||
${flag.space}done by this tool, which is useful, if you want
|
||||
${flag.space}to script it/run it in a restrictive vps/docker.
|
||||
')
|
||||
context.show_help = fp.bool('help', `h`, false, 'Show this help screen.')
|
||||
context.verbose = fp.bool('verbose', `v`, false, 'Be more verbose.')
|
||||
if context.show_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
if context.verbose {
|
||||
scripting.set_verbose(true)
|
||||
}
|
||||
if os.is_dir(context.v_repo_url) {
|
||||
context.v_repo_url = os.real_path(context.v_repo_url)
|
||||
}
|
||||
if os.is_dir(context.vc_repo_url) {
|
||||
context.vc_repo_url = os.real_path(context.vc_repo_url)
|
||||
}
|
||||
commits := fp.finalize() or {
|
||||
eprintln('Error: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
for commit in commits {
|
||||
validate_commit_exists(commit)
|
||||
}
|
||||
return commits
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
module vshare
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
struct ClipboardCommand {
|
||||
executable string
|
||||
command string
|
||||
}
|
||||
|
||||
// copy_to_clipboard copies `text` to the first available OS clipboard command.
|
||||
pub fn copy_to_clipboard(text string) bool {
|
||||
return copy_to_clipboard_with_commands(text, clipboard_commands())
|
||||
}
|
||||
|
||||
fn copy_to_clipboard_with_commands(text string, commands []ClipboardCommand) bool {
|
||||
if text.len == 0 || commands.len == 0 {
|
||||
return false
|
||||
}
|
||||
temp_file := os.join_path(os.vtmp_dir(),
|
||||
'vshare_clipboard_${os.getpid()}_${time.now().unix_micro()}.txt')
|
||||
os.write_file(temp_file, text) or { return false }
|
||||
defer {
|
||||
os.rm(temp_file) or {}
|
||||
}
|
||||
for command in commands {
|
||||
if !os.exists_in_system_path(command.executable) {
|
||||
continue
|
||||
}
|
||||
cmd := command.command.replace('@FILE@', os.quoted_path(temp_file))
|
||||
if os.execute(cmd).exit_code == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn clipboard_commands() []ClipboardCommand {
|
||||
$if windows {
|
||||
return [
|
||||
ClipboardCommand{
|
||||
executable: 'clip.exe'
|
||||
command: 'type @FILE@ | clip'
|
||||
},
|
||||
]
|
||||
} $else $if macos {
|
||||
return [
|
||||
ClipboardCommand{
|
||||
executable: 'pbcopy'
|
||||
command: 'pbcopy < @FILE@'
|
||||
},
|
||||
]
|
||||
} $else {
|
||||
return [
|
||||
ClipboardCommand{
|
||||
executable: 'wl-copy'
|
||||
command: 'wl-copy < @FILE@'
|
||||
},
|
||||
ClipboardCommand{
|
||||
executable: 'xclip'
|
||||
command: 'xclip -selection clipboard @FILE@'
|
||||
},
|
||||
ClipboardCommand{
|
||||
executable: 'xsel'
|
||||
command: 'xsel --clipboard --input < @FILE@'
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
module vshare
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
fn test_copy_to_clipboard_with_commands_returns_false_without_candidates() {
|
||||
assert copy_to_clipboard_with_commands('https://play.vlang.io/p/test', []) == false
|
||||
}
|
||||
|
||||
fn test_copy_to_clipboard_with_commands_uses_the_first_working_command() {
|
||||
test_dir := os.join_path(os.vtmp_dir(), 'vshare_test_${os.getpid()}_${time.now().unix_micro()}')
|
||||
os.mkdir_all(test_dir)!
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
output_path := os.join_path(test_dir, 'clipboard.out')
|
||||
fake_clip := os.join_path(test_dir, fake_clipboard_executable_name())
|
||||
os.write_file(fake_clip, fake_clipboard_script())!
|
||||
$if !windows {
|
||||
os.chmod(fake_clip, 0o700)!
|
||||
}
|
||||
original_path := os.getenv('PATH')
|
||||
original_output_path := os.getenv('VSHARE_TEST_OUTPUT')
|
||||
new_path := if original_path.len == 0 {
|
||||
test_dir
|
||||
} else {
|
||||
'${test_dir}${os.path_delimiter}${original_path}'
|
||||
}
|
||||
os.setenv('PATH', new_path, true)
|
||||
os.setenv('VSHARE_TEST_OUTPUT', output_path, true)
|
||||
defer {
|
||||
os.setenv('PATH', original_path, true)
|
||||
if original_output_path.len == 0 {
|
||||
os.unsetenv('VSHARE_TEST_OUTPUT')
|
||||
} else {
|
||||
os.setenv('VSHARE_TEST_OUTPUT', original_output_path, true)
|
||||
}
|
||||
}
|
||||
assert copy_to_clipboard_with_commands('https://play.vlang.io/p/test', [
|
||||
ClipboardCommand{
|
||||
executable: 'missing-clipboard-command'
|
||||
command: 'missing-clipboard-command < @FILE@'
|
||||
},
|
||||
ClipboardCommand{
|
||||
executable: fake_clipboard_executable_name()
|
||||
command: fake_clipboard_command()
|
||||
},
|
||||
])
|
||||
output := os.read_file(output_path)!
|
||||
$if windows {
|
||||
assert output.trim_right('\r\n') == 'https://play.vlang.io/p/test'
|
||||
} $else {
|
||||
assert output == 'https://play.vlang.io/p/test'
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_clipboard_command() string {
|
||||
return '${fake_clipboard_executable_name()} < @FILE@'
|
||||
}
|
||||
|
||||
fn fake_clipboard_executable_name() string {
|
||||
$if windows {
|
||||
return 'fakeclip.bat'
|
||||
} $else {
|
||||
return 'fakeclip'
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_clipboard_script() string {
|
||||
$if windows {
|
||||
return '@echo off\r\nmore > "%VSHARE_TEST_OUTPUT%"\r\n'
|
||||
} $else {
|
||||
return '#!/bin/sh\ncat > "\$VSHARE_TEST_OUTPUT"\n'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import os
|
||||
import flag
|
||||
import scripting
|
||||
import vgit
|
||||
|
||||
const tool_version = '0.0.4'
|
||||
const tool_description = ' Checkout an old version of V and compile it as it was on a specific commit.
|
||||
| This tool is useful when you want to discover when something broke.
|
||||
| It is also useful when you just want to experiment with an older, historic V.
|
||||
|
|
||||
| The VCOMMIT argument can be a git commit object like HEAD or master and so on.
|
||||
| When oldv is used with git bisect, you probably want to use HEAD. For example:
|
||||
| git bisect start
|
||||
| git bisect bad
|
||||
| git checkout known_good_commit
|
||||
| git bisect good
|
||||
| ## git will automatically checkout a middle commit between the bad and the good
|
||||
| cmd/tools/oldv --bisect --command="run commands in oldv folder, to verify if the commit is good or bad
|
||||
| ## Examine the results, and either do: ...
|
||||
| git bisect good
|
||||
| ## ... or do:
|
||||
| git bisect bad
|
||||
| ## Repeat the above steps, each time running oldv with the same command, then mark the result as good
|
||||
| ## or bad until you find the commit where the problem first occurred.
|
||||
| ## When you finish, do not forget to do:
|
||||
| git bisect reset'.strip_margin()
|
||||
|
||||
struct Context {
|
||||
mut:
|
||||
vgo vgit.VGitOptions
|
||||
vgcontext vgit.VGitContext
|
||||
commit_v string = 'master' // the commit from which you want to produce a working v compiler (this may be a commit-ish too)
|
||||
commit_v_hash string // this will be filled from the commit-ish commit_v using rev-list. It IS a commit hash.
|
||||
path_v string // the full path to the v folder inside workdir.
|
||||
path_vc string // the full path to the vc folder inside workdir.
|
||||
cmd_to_run string // the command that you want to run *in* the oldv repo
|
||||
cleanup bool // should the tool run a cleanup first
|
||||
use_cache bool // use local cached copies for --vrepo and --vcrepo in
|
||||
fresh_tcc bool // do use `make fresh_tcc`
|
||||
is_bisect bool // bisect mode; usage: `cmd/tools/oldv -b -c './v run bug.v'`
|
||||
show_vccommit bool // show the V and VC commits, corresponding to the V commit-ish, that can be used to build V
|
||||
cc string = 'cc' // the C compiler to use for bootstrapping.
|
||||
cc_options string // additional options to pass to the C compiler while bootstrapping.
|
||||
}
|
||||
|
||||
fn (mut c Context) compile_oldv_if_needed() {
|
||||
c.vgcontext = vgit.VGitContext{
|
||||
workdir: c.vgo.workdir
|
||||
v_repo_url: c.vgo.v_repo_url
|
||||
vc_repo_url: c.vgo.vc_repo_url
|
||||
cc: c.cc
|
||||
cc_options: c.cc_options
|
||||
commit_v: c.commit_v
|
||||
path_v: c.path_v
|
||||
path_vc: c.path_vc
|
||||
make_fresh_tcc: c.fresh_tcc
|
||||
show_vccommit: c.show_vccommit
|
||||
}
|
||||
c.vgcontext.compile_oldv_if_needed()
|
||||
c.commit_v_hash = c.vgcontext.commit_v__hash
|
||||
if !os.exists(c.vgcontext.vexepath) && c.cmd_to_run.len > 0 {
|
||||
// Note: 125 is a special code, that git bisect understands as 'skip this commit'.
|
||||
// it is used to inform git bisect that the current commit leads to a build failure.
|
||||
exit(125)
|
||||
}
|
||||
}
|
||||
|
||||
const cache_oldv_folder = os.join_path(os.cache_dir(), 'oldv')
|
||||
|
||||
const cache_oldv_folder_v = os.join_path(cache_oldv_folder, 'v')
|
||||
|
||||
const cache_oldv_folder_vc = os.join_path(cache_oldv_folder, 'vc')
|
||||
|
||||
fn sync_cache() {
|
||||
scripting.verbose_trace(@FN, 'start')
|
||||
if !os.exists(cache_oldv_folder) {
|
||||
scripting.verbose_trace(@FN, 'creating ${cache_oldv_folder}')
|
||||
scripting.mkdir_all(cache_oldv_folder) or {
|
||||
scripting.verbose_trace(@FN, '## failed.')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
scripting.chdir(cache_oldv_folder)
|
||||
for reponame in ['v', 'vc'] {
|
||||
repofolder := os.join_path(cache_oldv_folder, reponame)
|
||||
repo_url := 'https://github.com/vlang/${reponame}'
|
||||
scripting.verbose_trace(@FN, 'syncing ${repo_url} to ${repofolder}')
|
||||
vgit.clone_or_pull(repo_url, repofolder)
|
||||
}
|
||||
scripting.verbose_trace(@FN, 'done')
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if os.user_os() == 'windows' {
|
||||
scripting.used_tools_must_exist(['git', 'wc', 'make', 'robocopy'])
|
||||
} else {
|
||||
scripting.used_tools_must_exist(['git', 'wc', 'make', 'rsync', 'cc'])
|
||||
}
|
||||
|
||||
// Resetting VEXE here allows for `v run cmd/tools/oldv.v'.
|
||||
// the parent V would have set VEXE, which later will
|
||||
// affect the V's run from the tool itself.
|
||||
os.setenv('VEXE', '', true)
|
||||
|
||||
mut context := Context{}
|
||||
context.vgo.workdir = cache_oldv_folder
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
fp.application(os.file_name(os.executable()))
|
||||
fp.version(tool_version)
|
||||
fp.description(tool_description)
|
||||
fp.arguments_description('VCOMMIT')
|
||||
fp.skip_executable()
|
||||
context.use_cache = fp.bool('cache', `u`, true,
|
||||
'Use a cache of local repositories for --vrepo and --vcrepo in \$HOME/.cache/oldv/')
|
||||
if context.use_cache {
|
||||
context.vgo.v_repo_url = cache_oldv_folder_v
|
||||
context.vgo.vc_repo_url = cache_oldv_folder_vc
|
||||
} else {
|
||||
context.vgo.v_repo_url = 'https://github.com/vlang/v'
|
||||
context.vgo.vc_repo_url = 'https://github.com/vlang/vc'
|
||||
}
|
||||
context.cc = fp.string('cc', 0, 'cc',
|
||||
'Use this C compiler for bootstrapping v.c (defaults to `cc`).')
|
||||
context.cc_options = fp.string('ccoptions', 0, '',
|
||||
'Use these C compiler options for bootstrapping v.c (defaults to ``).')
|
||||
env_cc_options := os.getenv('OLDV_CCOPTIONS')
|
||||
if env_cc_options != '' {
|
||||
context.cc_options = env_cc_options
|
||||
}
|
||||
context.cleanup = fp.bool('clean', 0, false, 'Clean before running (slower).')
|
||||
context.fresh_tcc = fp.bool('fresh_tcc', 0, true,
|
||||
'Do `make fresh_tcc` when preparing a V compiler.')
|
||||
context.cmd_to_run = fp.string('command', `c`, '', 'Command to run in the old V repo.\n')
|
||||
context.show_vccommit = fp.bool('show_VC_commit', 0, false,
|
||||
'Show the VC commit, that can be used to compile the given V commit, and exit.\n')
|
||||
context.is_bisect = fp.bool('bisect', `b`, false,
|
||||
'Bisect mode. Use the current commit in the repo where oldv is.')
|
||||
|
||||
should_sync := fp.bool('cache-sync', `s`, false, 'Update the local cache')
|
||||
if !should_sync && !context.is_bisect {
|
||||
fp.limit_free_args(1, 1)!
|
||||
}
|
||||
|
||||
////
|
||||
commits := vgit.add_common_tool_options(mut context.vgo, mut fp)
|
||||
if should_sync {
|
||||
sync_cache()
|
||||
println('Cache synced, cache folder: ${cache_oldv_folder} .')
|
||||
exit(0)
|
||||
}
|
||||
if context.use_cache {
|
||||
if !os.is_dir(cache_oldv_folder_v) || !os.is_dir(cache_oldv_folder_vc) {
|
||||
sync_cache()
|
||||
}
|
||||
}
|
||||
if commits.len > 0 {
|
||||
context.commit_v = commits[0]
|
||||
if context.is_bisect {
|
||||
eprintln('In bisect mode, you should not pass any commits, since oldv will use the current one.')
|
||||
exit(2)
|
||||
}
|
||||
} else {
|
||||
context.commit_v = scripting.run('git rev-list -n1 HEAD')
|
||||
}
|
||||
if !context.show_vccommit {
|
||||
scripting.cprintln('################# context.commit_v: ${context.commit_v} #####################')
|
||||
}
|
||||
context.path_v = vgit.normalized_workpath_for_commit(context.vgo.workdir, context.commit_v)
|
||||
context.path_vc = vgit.normalized_workpath_for_commit(context.vgo.workdir, 'vc')
|
||||
if !os.is_dir(context.vgo.workdir) {
|
||||
eprintln('Work folder: ${context.vgo.workdir} , does not exist.')
|
||||
exit(2)
|
||||
}
|
||||
ecc := os.getenv('CC')
|
||||
if ecc != '' {
|
||||
context.cc = ecc
|
||||
}
|
||||
if context.cleanup {
|
||||
scripting.rmrf(context.path_v)
|
||||
scripting.rmrf(context.path_vc)
|
||||
}
|
||||
context.compile_oldv_if_needed()
|
||||
scripting.chdir(context.path_v)
|
||||
shorter_hash := context.commit_v_hash[0..10]
|
||||
scripting.cprintln('# v commit hash: ${shorter_hash} | folder: ${context.path_v}')
|
||||
if context.cmd_to_run.len > 0 {
|
||||
scripting.cprintln_strong('# command: ${context.cmd_to_run:-34s}')
|
||||
cmdres := os.execute_or_exit(context.cmd_to_run)
|
||||
if cmdres.exit_code != 0 {
|
||||
scripting.cprintln_strong('# exit code: ${cmdres.exit_code:-4d}')
|
||||
}
|
||||
scripting.cprint_strong('# result: ')
|
||||
print(cmdres.output)
|
||||
if !cmdres.output.ends_with('\n') {
|
||||
println('')
|
||||
}
|
||||
exit(cmdres.exit_code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import os
|
||||
import flag
|
||||
import scripting
|
||||
import vgit
|
||||
|
||||
const tool_version = '0.0.6'
|
||||
const tool_description = " Compares V executable size and performance,
|
||||
| between 2 commits from V's local git history.
|
||||
| When only one commit is given, it is compared to master.
|
||||
| ".strip_margin()
|
||||
|
||||
struct Context {
|
||||
cwd string // current working folder
|
||||
mut:
|
||||
vgo vgit.VGitOptions
|
||||
a string // the full path to the 'after' folder inside workdir
|
||||
b string // the full path to the 'before' folder inside workdir
|
||||
vc string // the full path to the vc folder inside workdir. It is used during bootstrapping v from the C source.
|
||||
commit_before string // the git commit for the 'before' state
|
||||
commit_after string // the git commit for the 'after' state
|
||||
warmups int // how many times to execute a command before gathering stats
|
||||
hyperfineopts string // use for additional CLI options that will be given to the hyperfine command
|
||||
vflags string // other v options to pass to compared v commands
|
||||
}
|
||||
|
||||
fn new_context() Context {
|
||||
return Context{
|
||||
cwd: os.getwd()
|
||||
commit_after: 'master'
|
||||
warmups: 4
|
||||
}
|
||||
}
|
||||
|
||||
fn (c Context) compare_versions() {
|
||||
// Input is validated at this point...
|
||||
// Cleanup artifacts from previous runs of this tool:
|
||||
scripting.chdir(c.vgo.workdir)
|
||||
scripting.run('rm -rf "${c.a}" "${c.b}" "${c.vc}" ')
|
||||
// clone the VC source *just once per comparison*, and reuse it:
|
||||
scripting.run('git clone --filter=blob:none --quiet "${c.vgo.vc_repo_url}" "${c.vc}" ')
|
||||
println('Comparing V performance of commit ${c.commit_before} (before) vs commit ${c.commit_after} (after) ...')
|
||||
c.prepare_v(c.b, c.commit_before)
|
||||
c.prepare_v(c.a, c.commit_after)
|
||||
scripting.chdir(c.vgo.workdir)
|
||||
if c.vflags.len > 0 {
|
||||
os.setenv('VFLAGS', c.vflags, true)
|
||||
}
|
||||
// The first is the baseline, against which all the others will be compared.
|
||||
// It is the fastest, since hello_world.v has only a single println in it,
|
||||
mut perf_files := []string{}
|
||||
perf_files << c.compare_v_performance('source_hello', [
|
||||
'vprod @DEBUG@ -o source.c examples/hello_world.v',
|
||||
'vprod -o source.c examples/hello_world.v',
|
||||
'v @DEBUG@ -o source.c examples/hello_world.v',
|
||||
'v -o source.c examples/hello_world.v',
|
||||
])
|
||||
perf_files << c.compare_v_performance('source_v', [
|
||||
'vprod @DEBUG@ -o source.c @COMPILER@',
|
||||
'vprod -o source.c @COMPILER@',
|
||||
'v @DEBUG@ -o source.c @COMPILER@',
|
||||
'v -o source.c @COMPILER@',
|
||||
])
|
||||
perf_files << c.compare_v_performance('binary_hello', [
|
||||
'vprod -o hello examples/hello_world.v',
|
||||
'v -o hello examples/hello_world.v',
|
||||
])
|
||||
perf_files << c.compare_v_performance('binary_v', [
|
||||
'vprod -o binary @COMPILER@',
|
||||
'v -o binary @COMPILER@',
|
||||
])
|
||||
println('All performance files:')
|
||||
for f in perf_files {
|
||||
println(' ${f}')
|
||||
}
|
||||
}
|
||||
|
||||
fn (c &Context) prepare_v(cdir string, commit string) {
|
||||
mut cc := os.getenv('CC')
|
||||
if cc == '' {
|
||||
cc = 'cc'
|
||||
}
|
||||
mut vgit_context := vgit.VGitContext{
|
||||
cc: cc
|
||||
commit_v: commit
|
||||
path_v: cdir
|
||||
path_vc: c.vc
|
||||
workdir: c.vgo.workdir
|
||||
v_repo_url: c.vgo.v_repo_url
|
||||
vc_repo_url: c.vgo.vc_repo_url
|
||||
}
|
||||
vgit_context.compile_oldv_if_needed()
|
||||
scripting.chdir(cdir)
|
||||
scripting.run('${cdir}/v version')
|
||||
println('Making a v compiler in ${cdir}')
|
||||
scripting.run('./v -cc ${cc} -o v ${vgit_context.vvlocation}')
|
||||
println('Making a vprod compiler in ${cdir}')
|
||||
scripting.run('./v -cc ${cc} -prod -o vprod ${vgit_context.vvlocation}')
|
||||
println('Stripping and compressing cv v and vprod binaries in ${cdir}')
|
||||
scripting.run('cp cv cv_stripped')
|
||||
scripting.run('cp v v_stripped')
|
||||
scripting.run('cp vprod vprod_stripped')
|
||||
scripting.run('strip *_stripped')
|
||||
scripting.run('cp cv_stripped cv_stripped_upxed')
|
||||
scripting.run('cp v_stripped v_stripped_upxed')
|
||||
scripting.run('cp vprod_stripped vprod_stripped_upxed')
|
||||
scripting.run('upx -qqq --lzma cv_stripped_upxed')
|
||||
scripting.run('upx -qqq --lzma v_stripped_upxed')
|
||||
scripting.run('upx -qqq --lzma vprod_stripped_upxed')
|
||||
scripting.show_sizes_of_files(['${cdir}/cv', '${cdir}/cv_stripped', '${cdir}/cv_stripped_upxed'])
|
||||
scripting.show_sizes_of_files(['${cdir}/v', '${cdir}/v_stripped', '${cdir}/v_stripped_upxed'])
|
||||
scripting.show_sizes_of_files(['${cdir}/vprod', '${cdir}/vprod_stripped',
|
||||
'${cdir}/vprod_stripped_upxed'])
|
||||
vversion := scripting.run('${cdir}/v -version')
|
||||
vcommit := scripting.run('git rev-parse --short --verify HEAD')
|
||||
println('V version is: ${vversion} , local source commit: ${vcommit}')
|
||||
if vgit_context.vvlocation == 'cmd/v' {
|
||||
if os.exists('vlib/v/ast/ast.v') {
|
||||
println('Source lines of the compiler: ' +
|
||||
scripting.run('find cmd/v/ vlib/v/ -name "*.v" | grep -v /tests/ | xargs wc | tail -n -1'))
|
||||
} else {
|
||||
println('Source lines of the compiler: ' +
|
||||
scripting.run('wc cmd/v/*.v vlib/compiler/*.v | tail -n -1'))
|
||||
}
|
||||
} else if vgit_context.vvlocation == 'v.v' {
|
||||
println('Source lines of the compiler: ' +
|
||||
scripting.run('wc v.v vlib/compiler/*.v | tail -n -1'))
|
||||
} else {
|
||||
println('Source lines of the compiler: ' + scripting.run('wc compiler/*.v | tail -n -1'))
|
||||
}
|
||||
}
|
||||
|
||||
fn (c Context) compare_v_performance(label string, commands []string) string {
|
||||
println('---------------------------------------------------------------------------------')
|
||||
println('Compare v performance when doing the following commands (${label}):')
|
||||
mut source_location_a := ''
|
||||
mut source_location_b := ''
|
||||
if os.exists('${c.a}/cmd/v') {
|
||||
source_location_a = 'cmd/v'
|
||||
} else {
|
||||
source_location_a = if os.exists('${c.a}/v.v') { 'v.v ' } else { 'compiler/ ' }
|
||||
}
|
||||
if os.exists('${c.b}/cmd/v') {
|
||||
source_location_b = 'cmd/v'
|
||||
} else {
|
||||
source_location_b = if os.exists('${c.b}/v.v') { 'v.v ' } else { 'compiler/ ' }
|
||||
}
|
||||
timestamp_a, _ :=
|
||||
vgit.line_to_timestamp_and_commit(scripting.run('cd ${c.a}/ ; git rev-list -n1 --timestamp HEAD'))
|
||||
timestamp_b, _ :=
|
||||
vgit.line_to_timestamp_and_commit(scripting.run('cd ${c.b}/ ; git rev-list -n1 --timestamp HEAD'))
|
||||
// 1570877641 is 065ce39 2019-10-12
|
||||
debug_option_a := if timestamp_a > 1570877641 { '-cg ' } else { '-debug ' }
|
||||
debug_option_b := if timestamp_b > 1570877641 { '-cg ' } else { '-debug ' }
|
||||
mut hyperfine_commands_arguments := []string{}
|
||||
for cmd in commands {
|
||||
println(cmd)
|
||||
}
|
||||
for cmd in commands {
|
||||
hyperfine_commands_arguments << ' \'cd ${c.b:-34s} ; ./${cmd} \' '.replace_each([
|
||||
'@COMPILER@',
|
||||
source_location_b,
|
||||
'@DEBUG@',
|
||||
debug_option_b,
|
||||
])
|
||||
}
|
||||
for cmd in commands {
|
||||
hyperfine_commands_arguments << ' \'cd ${c.a:-34s} ; ./${cmd} \' '.replace_each([
|
||||
'@COMPILER@',
|
||||
source_location_a,
|
||||
'@DEBUG@',
|
||||
debug_option_a,
|
||||
])
|
||||
}
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
cmd_stats_file :=
|
||||
os.real_path([c.vgo.workdir, 'v_performance_stats_${label}.json'].join(os.path_separator))
|
||||
comparison_cmd := 'hyperfine ${c.hyperfineopts} ' + '--export-json ${cmd_stats_file} ' +
|
||||
'--time-unit millisecond ' + '--style full --warmup ${c.warmups} ' +
|
||||
hyperfine_commands_arguments.join(' ')
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
if c.vgo.verbose {
|
||||
println(comparison_cmd)
|
||||
}
|
||||
os.system(comparison_cmd)
|
||||
println('The detailed performance comparison report was saved to: ${cmd_stats_file} .')
|
||||
println('')
|
||||
return cmd_stats_file
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// allow for `v run cmd/tools/performance_compare.v`, see oldv.v
|
||||
os.setenv('VEXE', '', true)
|
||||
scripting.used_tools_must_exist(['cp', 'rm', 'strip', 'make', 'git', 'upx', 'cc', 'wc', 'tail',
|
||||
'find', 'xargs', 'hyperfine'])
|
||||
mut context := new_context()
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
fp.application(os.file_name(os.executable()))
|
||||
fp.version(tool_version)
|
||||
fp.description(tool_description)
|
||||
fp.arguments_description('COMMIT_BEFORE [COMMIT_AFTER]')
|
||||
fp.skip_executable()
|
||||
fp.limit_free_args(1, 2)!
|
||||
context.vflags = fp.string('vflags', 0, '',
|
||||
'Additional options to pass to the v commands, for example "-cc tcc"')
|
||||
context.hyperfineopts = fp.string('hyperfine_options', 0, '', 'Additional options passed to hyperfine.
|
||||
${flag.space}For example on linux, you may want to pass:
|
||||
${flag.space}--hyperfine_options "--prepare \'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches\'"
|
||||
')
|
||||
commits := vgit.add_common_tool_options(mut context.vgo, mut fp)
|
||||
context.commit_before = commits[0]
|
||||
if commits.len > 1 {
|
||||
context.commit_after = commits[1]
|
||||
}
|
||||
context.b = vgit.normalized_workpath_for_commit(context.vgo.workdir, context.commit_before)
|
||||
context.a = vgit.normalized_workpath_for_commit(context.vgo.workdir, context.commit_after)
|
||||
context.vc = vgit.normalized_workpath_for_commit(context.vgo.workdir, 'vc')
|
||||
if !os.is_dir(context.vgo.workdir) {
|
||||
eprintln('Work folder: `{context.vgo.workdir}` , does not exist. Use `--workdir /some/path` to set it.')
|
||||
exit(2)
|
||||
}
|
||||
context.compare_versions()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
import term
|
||||
import flag
|
||||
|
||||
const tools_folder = os.real_path(os.dir(os.executable()))
|
||||
|
||||
const oldvexe = fullpath(tools_folder, 'oldv')
|
||||
|
||||
const oldv_source = fullpath(tools_folder, 'oldv.v')
|
||||
|
||||
const vroot = os.real_path(os.dir(os.dir(tools_folder)))
|
||||
|
||||
const vexe = fullpath(vroot, 'v')
|
||||
|
||||
fn fullpath(folder string, fname string) string {
|
||||
return os.real_path(os.join_path_single(folder, exename(fname)))
|
||||
}
|
||||
|
||||
fn exename(n string) string {
|
||||
if n.ends_with('.v') || os.user_os() != 'windows' {
|
||||
return n
|
||||
}
|
||||
return '${n}.exe'
|
||||
}
|
||||
|
||||
struct Context {
|
||||
mut:
|
||||
old_commit string
|
||||
new_commit string
|
||||
command string
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
mut context := Context{}
|
||||
fp.application(os.file_name(os.executable()))
|
||||
fp.version('0.0.2')
|
||||
fp.description('\n Find at what commit a regression occurred.
|
||||
To find when a regression happened (regression_bug.v should fail on master):
|
||||
./v run cmd/tools/regress.v --old a7019ac --command " ./v run /abs/path/to/regression_bug.v"
|
||||
To find when a feature was implemented (feature.v should succeed on master):
|
||||
./v run cmd/tools/regress.v --old a7019ac --command "! ./v run /abs/path/to/feature.v"')
|
||||
fp.skip_executable()
|
||||
|
||||
context.new_commit = fp.string('new', `n`, 'master', 'The new commit, by default: master.')
|
||||
context.old_commit = fp.string('old', `o`, '',
|
||||
'A known old commit, required (for it, COMMAND should exit with 0).')
|
||||
context.command = fp.string('command', `c`, '',
|
||||
'A command to execute. Should exit with 0 for the *old* commits.')
|
||||
fp.finalize() or {}
|
||||
if context.old_commit == '' {
|
||||
eprintln('--old COMMIT is required')
|
||||
exit(1)
|
||||
}
|
||||
if context.command == '' {
|
||||
eprintln('--command "COMMAND" is required')
|
||||
exit(2)
|
||||
}
|
||||
if !os.exists(oldvexe) {
|
||||
if 0 != execute('${os.quoted_path(vexe)} -o ${os.quoted_path(oldvexe)} ${os.quoted_path(oldv_source)}') {
|
||||
panic('can not compile ${oldvexe}')
|
||||
}
|
||||
}
|
||||
os.execute('git checkout master')
|
||||
os.execute('git bisect reset')
|
||||
os.execute('git checkout ${context.new_commit}')
|
||||
os.execute('git bisect start')
|
||||
os.execute('git bisect new')
|
||||
os.execute('git checkout ${context.old_commit}')
|
||||
os.execute('git bisect old')
|
||||
println(term.colorize(term.bright_yellow, term.header('', '-')))
|
||||
execute('git bisect run ${os.quoted_path(oldvexe)} --bisect -c "${context.command}"')
|
||||
println(term.colorize(term.bright_yellow, term.header('', '-')))
|
||||
os.execute('git bisect reset')
|
||||
os.execute('git checkout master')
|
||||
}
|
||||
|
||||
fn execute(cmd string) int {
|
||||
eprintln('### ${cmd}')
|
||||
return os.system(cmd)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
|
||||
const vexe = @VEXE
|
||||
|
||||
const known_skip_patterns_env = os.getenv('VKNOWN_SKIP_PATTERNS')
|
||||
|
||||
const known_folder_patterns_that_are_not_module_ones = [
|
||||
'vlib/sync/bench',
|
||||
'/tests',
|
||||
'/testdata',
|
||||
'/preludes_js',
|
||||
'vlib/builtin/js', // TODO: fix compiler panic
|
||||
'vlib/fontstash', // used by `gg`
|
||||
'vlib/sokol/sfons', // used by `gg`, and by examples/sokol/fonts.v
|
||||
'vlib/sokol/sapp', // used by `gg`, and many examples/
|
||||
'vlib/sokol/gfx', // used by `gg`, `x.ttf`
|
||||
'vlib/sokol/sgl', // used by `gg`
|
||||
'vlib/toml', // toml is well tested, even if the top level folder does not have _test.v files, the ones below do
|
||||
'vlib/v/', // the compiler itself is well tested
|
||||
]
|
||||
|
||||
fn main() {
|
||||
mut places := if os.args.len > 1 {
|
||||
os.args#[1..]
|
||||
} else {
|
||||
eprintln('> check the current folder only by default ...')
|
||||
['.']
|
||||
}
|
||||
mut known_skip_patterns := known_folder_patterns_that_are_not_module_ones.clone()
|
||||
if known_skip_patterns_env != '' {
|
||||
known_skip_patterns = known_skip_patterns_env.split(',').filter(it != '')
|
||||
}
|
||||
for path in places {
|
||||
eprintln('> Checking folder: `${path}` ...')
|
||||
mut found := 0
|
||||
files := os.walk_ext(path, '.v')
|
||||
mut v_files := map[string]int{}
|
||||
mut v_test_files := map[string]int{}
|
||||
for f in files {
|
||||
folder := os.to_slash(os.dir(f))
|
||||
if known_skip_patterns.any(f.contains(it)) {
|
||||
continue
|
||||
}
|
||||
if f.ends_with('.v') {
|
||||
v_files[folder]++
|
||||
}
|
||||
if f.ends_with('_test.v') {
|
||||
v_test_files[folder]++
|
||||
}
|
||||
}
|
||||
eprintln('> Found ${v_files.len:5} potential V module folders (containing .v files).')
|
||||
for folder, n_v_files in v_files {
|
||||
n_test_v_files := v_test_files[folder]
|
||||
if n_v_files > 1 && n_test_v_files == 0 {
|
||||
println('> ${n_test_v_files:5} _test.v files, with ${n_v_files:5} .v files, in folder: ${folder}')
|
||||
compilation :=
|
||||
os.execute('${os.quoted_path(vexe)} -shared -W -Wfatal-errors -check ${os.quoted_path(folder)}')
|
||||
if compilation.exit_code != 0 {
|
||||
eprintln('> ${folder} has parser/checker errors!')
|
||||
eprintln(compilation.output)
|
||||
}
|
||||
found++
|
||||
}
|
||||
}
|
||||
eprintln('> Found ${found} module folders without _test.v files in `${path}` .')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
import term
|
||||
import time
|
||||
|
||||
struct Context {
|
||||
mut:
|
||||
cut_time time.Time
|
||||
deprecations int
|
||||
}
|
||||
|
||||
fn (mut ctx Context) analyze_line(line string, position_file string, position_line int) {
|
||||
blame_for_time := os.execute('git blame -L${position_line} --porcelain -- ${position_file}')
|
||||
if blame_for_time.exit_code != 0 {
|
||||
return
|
||||
}
|
||||
ts := blame_for_time.output.all_after('committer-time').all_before('\n').trim_space().int()
|
||||
t := time.unix(ts)
|
||||
if ctx.cut_time < t {
|
||||
println(term.colorize(term.gray,
|
||||
'>>> SKIPPING since t: ${t} > ${ctx.cut_time}, ${position_file}:${position_line}: ${line}'))
|
||||
return
|
||||
}
|
||||
ctx.deprecations++
|
||||
blame_for_context := os.execute('git blame -L${position_line},+5 -- ${position_file}')
|
||||
context := blame_for_context.output.trim_space().split_into_lines()
|
||||
println(term.colorize(term.red,
|
||||
'${position_file}:${position_line}: deprecation: ${ctx.deprecations}, timestamp: ${ts} - ${t}'))
|
||||
for cline in context {
|
||||
println(' ${cline}')
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if os.args.len < 2 {
|
||||
eprintln('Usage: v run cmd/tools/show_ancient_deprecations.v [DAYS]')
|
||||
exit(1)
|
||||
}
|
||||
cut_months := os.args[1].int()
|
||||
cut_time := time.now().add(-cut_months * 24 * time.hour)
|
||||
mut ctx := Context{
|
||||
cut_time: cut_time
|
||||
}
|
||||
println('> Deprecations that happened before ${cut_time}')
|
||||
all_v_files := os.walk_ext('.', '.v')
|
||||
for v_file in all_v_files {
|
||||
if v_file == './vlib/v/fmt/tests/attrs_keep.vv' {
|
||||
println(term.colorize(term.gray,
|
||||
'>>> SKIPPING deprecations attrs formatting test file ${v_file}'))
|
||||
continue
|
||||
}
|
||||
if v_file.starts_with('./vlib/v/checker/tests') && v_file.contains('deprec') {
|
||||
println(term.colorize(term.gray, '>>> SKIPPING deprecations test file ${v_file}'))
|
||||
continue
|
||||
}
|
||||
file_content := os.read_file(v_file)!
|
||||
if !file_content.contains('[deprecated') {
|
||||
continue
|
||||
}
|
||||
lines := file_content.split_into_lines()
|
||||
for line_num := lines.len - 1; line_num > 0; line_num-- {
|
||||
line := lines[line_num].all_before('//')
|
||||
mut is_deprecation_line := false
|
||||
if line.contains('\tif ') {
|
||||
continue
|
||||
}
|
||||
if line.contains('[deprecated:') {
|
||||
is_deprecation_line = true
|
||||
}
|
||||
if line.contains('[deprecated]') {
|
||||
is_deprecation_line = true
|
||||
}
|
||||
if !is_deprecation_line {
|
||||
continue
|
||||
}
|
||||
ctx.analyze_line(line, v_file, line_num + 1)
|
||||
}
|
||||
}
|
||||
println('> Summary: there were ${term.colorize(term.bright_yellow, ctx.deprecations.str())} deprecations found, done before ${term.colorize(term.magenta,
|
||||
cut_time.str())}.')
|
||||
if ctx.deprecations > 0 {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
module main
|
||||
|
||||
// This program verifies that `v test` propagates errors
|
||||
// and that it exits with code 1, when at least 1 FAIL happen.
|
||||
import os
|
||||
import rand
|
||||
import time
|
||||
|
||||
const vexe = os.quoted_path(get_vexe_path())
|
||||
const vroot = os.dir(vexe)
|
||||
const tdir = new_tdir()
|
||||
|
||||
fn get_vexe_path() string {
|
||||
env_vexe := os.getenv('VEXE')
|
||||
if env_vexe != '' {
|
||||
return env_vexe
|
||||
}
|
||||
me := os.executable()
|
||||
eprintln('me: ${me}')
|
||||
mut vexe_ := os.join_path(os.dir(os.dir(os.dir(me))), 'v')
|
||||
if os.user_os() == 'windows' {
|
||||
vexe_ += '.exe'
|
||||
}
|
||||
return vexe_
|
||||
}
|
||||
|
||||
fn new_tdir() string {
|
||||
dir := os.join_path(os.vtmp_dir(), rand.ulid())
|
||||
os.rmdir_all(dir) or {}
|
||||
os.mkdir_all(dir) or { panic(err) }
|
||||
at_exit(cleanup_tdir) or {}
|
||||
return dir
|
||||
}
|
||||
|
||||
fn cleanup_tdir() {
|
||||
println('... removing tdir: ${tdir}')
|
||||
os.rmdir_all(tdir) or { eprintln(err) }
|
||||
}
|
||||
|
||||
type MyResult = string
|
||||
|
||||
@[noreturn]
|
||||
fn (result MyResult) fail(reason string) {
|
||||
eprintln('> ${reason}, but it does not. Result:\n${result}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
fn (result MyResult) has(sub string) MyResult {
|
||||
if !result.contains(sub) {
|
||||
result.fail(' result should have the substring `${sub}`')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn (result MyResult) matches(gpattern string) MyResult {
|
||||
if !result.match_glob(gpattern) {
|
||||
result.fail('result should match the glob pattern `${gpattern}`')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn create_test(tname string, tcontent string) !string {
|
||||
tpath := os.join_path(tdir, tname)
|
||||
os.write_file(tpath, tcontent)!
|
||||
eprintln('>>>>>>>> tpath: ${tpath} | tcontent: ${tcontent}')
|
||||
return os.quoted_path(tpath)
|
||||
}
|
||||
|
||||
fn check_assert_continues_works() ! {
|
||||
os.chdir(tdir)!
|
||||
create_test('assert_continues_option_works_test.v',
|
||||
'fn test_fail1() { assert 2==4\nassert 2==1\nassert 2==0 }\nfn test_ok(){ assert true }\nfn test_fail2() { assert false }')!
|
||||
result := check_fail('${vexe} -assert continues assert_continues_option_works_test.v')
|
||||
result.has('assert_continues_option_works_test.v:1: fn test_fail1')
|
||||
result.has('assert_continues_option_works_test.v:2: fn test_fail1')
|
||||
result.has('assert_continues_option_works_test.v:3: fn test_fail1')
|
||||
result.has('assert_continues_option_works_test.v:5: fn test_fail2')
|
||||
result.has('> assert 2 == 4').has('> assert 2 == 1').has('> assert 2 == 0')
|
||||
// Check if a test function, tagged with [assert_continues], has the same behaviour, without needing additional options
|
||||
create_test('assert_continues_tag_works_test.v',
|
||||
'@[assert_continues]fn test_fail1() { assert 2==4\nassert 2==1\nassert 2==0 }\nfn test_ok(){ assert true }\nfn test_fail2() { assert false\n assert false }')!
|
||||
tag_res := check_fail('${vexe} assert_continues_tag_works_test.v')
|
||||
tag_res.has('assert_continues_tag_works_test.v:1: fn test_fail1')
|
||||
tag_res.has('assert_continues_tag_works_test.v:2: fn test_fail1')
|
||||
tag_res.has('assert_continues_tag_works_test.v:3: fn test_fail1')
|
||||
tag_res.has('assert_continues_tag_works_test.v:5: fn test_fail2')
|
||||
if tag_res.contains('assert_continues_tag_works_test.v:6: fn test_fail2') {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_ok(cmd string) MyResult {
|
||||
println('> check_ok cmd: ${cmd}')
|
||||
res := os.execute(cmd)
|
||||
if res.exit_code != 0 {
|
||||
eprintln('> check_ok failed.\n${res.output}')
|
||||
exit(1)
|
||||
}
|
||||
return res.output
|
||||
}
|
||||
|
||||
fn check_fail(cmd string) MyResult {
|
||||
println('> check_fail cmd: ${cmd}')
|
||||
res := os.execute(cmd)
|
||||
if res.exit_code == 0 {
|
||||
eprintln('> check_fail succeeded, but it should have failed.\n${res.output}')
|
||||
exit(1)
|
||||
}
|
||||
return res.output
|
||||
}
|
||||
|
||||
fn main() {
|
||||
defer {
|
||||
os.chdir(os.wd_at_startup) or {}
|
||||
}
|
||||
unbuffer_stdout()
|
||||
spawn fn () {
|
||||
time.sleep(120 * time.second)
|
||||
eprintln('>>> exiting due to an expired watchdog timer <<<')
|
||||
exit(1)
|
||||
}()
|
||||
println('> vroot: ${vroot} | vexe: ${vexe} | tdir: ${tdir}')
|
||||
os.setenv('VTEST_HIDE_OK', '0', true)
|
||||
ok_fpath := create_test('a_single_ok_test.v', 'fn test_ok(){ assert true }')!
|
||||
if check_ok('${vexe} ${ok_fpath}') != '' {
|
||||
exit(1)
|
||||
}
|
||||
check_ok('${vexe} test ${ok_fpath}').matches('*OK*a_single_ok_test.v*')
|
||||
check_ok('${vexe} test "${tdir}"').matches('*OK*a_single_ok_test.v*')
|
||||
check_ok('${vexe} -stats test "${tdir}"').matches('*OK*a_single_ok_test.v*')
|
||||
|
||||
fail_fpath := create_test('a_single_failing_test.v', 'fn test_fail(){ assert 1 == 2 }')!
|
||||
check_fail('${vexe} ${fail_fpath}').has('> assert 1 == 2').has('a_single_failing_test.v:1: fn test_fail')
|
||||
check_fail('${vexe} test ${fail_fpath}').has('> assert 1 == 2').has('a_single_failing_test.v:1: fn test_fail')
|
||||
check_fail('${vexe} test "${tdir}"').has('> assert 1 == 2')
|
||||
check_fail('${vexe} -stats test "${tdir}"').has('> assert 1 == 2')
|
||||
rel_dir := os.join_path(tdir, rand.ulid())
|
||||
os.mkdir(rel_dir)!
|
||||
os.chdir(rel_dir)!
|
||||
relative_path := '..' + os.path_separator + 'a_single_ok_test.v'
|
||||
check_ok('${vexe} test ${os.quoted_path(relative_path)}').has('OK').has('a_single_ok_test.v')
|
||||
|
||||
check_assert_continues_works()!
|
||||
println('> all done')
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
println(arguments()#[1..])
|
||||
@@ -0,0 +1,98 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import time
|
||||
import os.cmdline
|
||||
|
||||
enum Target {
|
||||
both
|
||||
stderr
|
||||
stdout
|
||||
alternate
|
||||
}
|
||||
|
||||
fn s2target(s string) Target {
|
||||
return match s {
|
||||
'both' { Target.both }
|
||||
'stderr' { Target.stderr }
|
||||
'alternate' { Target.alternate }
|
||||
else { Target.stdout }
|
||||
}
|
||||
}
|
||||
|
||||
struct Context {
|
||||
mut:
|
||||
timeout_ms int
|
||||
period_ms int
|
||||
exitcode int
|
||||
target Target
|
||||
omode Target
|
||||
is_verbose bool
|
||||
show_wd bool
|
||||
show_env bool
|
||||
}
|
||||
|
||||
fn (mut ctx Context) println(s string) {
|
||||
if ctx.target == .alternate {
|
||||
ctx.omode = if ctx.omode == .stderr { Target.stdout } else { Target.stderr }
|
||||
}
|
||||
if ctx.target in [.both, .stdout] || ctx.omode == .stdout {
|
||||
println('stdout, ${s}')
|
||||
}
|
||||
if ctx.target in [.both, .stderr] || ctx.omode == .stderr {
|
||||
eprintln('stderr, ${s}')
|
||||
}
|
||||
}
|
||||
|
||||
fn do_timeout(c &Context) {
|
||||
mut ctx := unsafe { c }
|
||||
time.sleep(ctx.timeout_ms * time.millisecond)
|
||||
exit(ctx.exitcode)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
unbuffer_stdout()
|
||||
mut ctx := Context{}
|
||||
args := os.args[1..]
|
||||
if '-h' in args || '--help' in args {
|
||||
println("Usage:
|
||||
test_os_process [-v] [-h] [-target stderr/stdout/both/alternate] [-show_wd] [-show_env] [-exitcode 0] [-timeout_ms 200] [-period_ms 50]
|
||||
Prints lines periodically (-period_ms), to stdout/stderr (-target).
|
||||
After a while (-timeout_ms), exit with (-exitcode).
|
||||
This program is useful for platform independent testing
|
||||
of child process/standard input/output control.
|
||||
It is used in V's `os` module tests.
|
||||
")
|
||||
return
|
||||
}
|
||||
ctx.is_verbose = '-v' in args
|
||||
ctx.show_wd = '-show_wd' in args
|
||||
ctx.show_env = '-show_env' in args
|
||||
ctx.target = s2target(cmdline.option(args, '-target', 'both'))
|
||||
ctx.exitcode = cmdline.option(args, '-exitcode', '0').int()
|
||||
ctx.timeout_ms = cmdline.option(args, '-timeout_ms', '200').int()
|
||||
ctx.period_ms = cmdline.option(args, '-period_ms', '50').int()
|
||||
if ctx.target == .alternate {
|
||||
ctx.omode = .stdout
|
||||
}
|
||||
if ctx.is_verbose {
|
||||
eprintln('> args: ${args} | context: ${ctx}')
|
||||
}
|
||||
if ctx.show_wd {
|
||||
ctx.println('WORK_DIR=${os.getwd()}')
|
||||
}
|
||||
if ctx.show_env {
|
||||
all := os.environ()
|
||||
for k, v in all {
|
||||
ctx.println('${k}=${v}')
|
||||
}
|
||||
}
|
||||
spawn do_timeout(&ctx)
|
||||
ctx.println('start')
|
||||
for i := 1; true; i++ {
|
||||
ctx.println('${i}')
|
||||
time.sleep(ctx.period_ms * time.millisecond)
|
||||
}
|
||||
ctx.println('done')
|
||||
time.sleep(100 * time.second)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
module main
|
||||
|
||||
import os
|
||||
import v.util
|
||||
|
||||
const vexe = os.getenv('VEXE')
|
||||
|
||||
fn normalize_translate_args(args []string) []string {
|
||||
if args.len > 0 && args[0] == '-wrapper' {
|
||||
mut normalized := []string{cap: args.len}
|
||||
normalized << 'wrapper'
|
||||
normalized << args[1..]
|
||||
return normalized
|
||||
}
|
||||
return args.clone()
|
||||
}
|
||||
|
||||
fn show_usage_and_exit() {
|
||||
eprintln('Wrong number of arguments. Use `v translate file.c` or `v translate wrapper file.c`.')
|
||||
exit(3)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
vmodules := os.vmodules_dir()
|
||||
c2v_dir := os.join_path(vmodules, 'c2v')
|
||||
mut c2v_bin := os.join_path(c2v_dir, 'c2v')
|
||||
$if windows {
|
||||
c2v_bin += '.exe'
|
||||
}
|
||||
// Git clone c2v
|
||||
if !os.exists(c2v_dir) {
|
||||
os.mkdir_all(vmodules)!
|
||||
println('C2V is not installed. Cloning C2V to ${c2v_dir} ...')
|
||||
os.chdir(vmodules)!
|
||||
res :=
|
||||
os.execute('${os.quoted_path(vexe)} retry -- git clone --filter=blob:none https://github.com/vlang/c2v')
|
||||
if res.exit_code != 0 {
|
||||
eprintln('Failed to download C2V.')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
// Compile c2v
|
||||
if !os.exists(c2v_bin) {
|
||||
os.chdir(c2v_dir)!
|
||||
println('Compiling c2v ...')
|
||||
res2 :=
|
||||
os.execute('${os.quoted_path(vexe)} -o ${os.quoted_path(c2v_bin)} -keepc -g -experimental .')
|
||||
if res2.exit_code != 0 {
|
||||
eprintln(res2.output)
|
||||
eprintln('Failed to compile C2V. This should not happen. Please report it via GitHub.')
|
||||
exit(2)
|
||||
}
|
||||
}
|
||||
translate_args := normalize_translate_args(os.args[2..])
|
||||
if translate_args.len == 0 || (translate_args.len == 1 && translate_args[0] == 'wrapper') {
|
||||
show_usage_and_exit()
|
||||
}
|
||||
passed_args := util.args_quote_paths(translate_args)
|
||||
// println(passed_args)
|
||||
os.chdir(os.wd_at_startup)!
|
||||
c2v_cmd := '${os.quoted_path(c2v_bin)} ${passed_args}'
|
||||
res := os.system(c2v_cmd)
|
||||
if res != 0 {
|
||||
eprintln('C2V command: ${c2v_cmd}')
|
||||
eprintln('C2V failed to translate the C files. Please report it via GitHub.')
|
||||
exit(4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
|
||||
const qvexe = os.quoted_path(@VEXE)
|
||||
|
||||
fn test_translate_legacy_wrapper_flag_uses_wrapper_mode() {
|
||||
test_dir := os.join_path(os.vtmp_dir(), 'translate_test_${os.getpid()}')
|
||||
os.rmdir_all(test_dir) or {}
|
||||
os.mkdir_all(test_dir)!
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
vmodules_dir := os.join_path(test_dir, 'vmodules')
|
||||
compile_fake_c2v(vmodules_dir)!
|
||||
sample_path := os.join_path(test_dir, 'usersapi.c')
|
||||
args_out := os.join_path(test_dir, 'c2v_args.txt')
|
||||
os.write_file(sample_path, 'int usersapi_get_number_of_users(void) { return 1; }\n')!
|
||||
original_c2v_args_out := os.getenv('C2V_ARGS_OUT')
|
||||
original_vmodules := os.getenv('VMODULES')
|
||||
os.setenv('C2V_ARGS_OUT', args_out, true)
|
||||
os.setenv('VMODULES', vmodules_dir, true)
|
||||
defer {
|
||||
restore_env('C2V_ARGS_OUT', original_c2v_args_out)
|
||||
restore_env('VMODULES', original_vmodules)
|
||||
}
|
||||
mut process := os.new_process(@VEXE)
|
||||
process.set_work_folder(@VEXEROOT)
|
||||
process.set_args(['translate', '-wrapper', sample_path])
|
||||
process.set_redirect_stdio()
|
||||
process.wait()
|
||||
stdout := process.stdout_slurp()
|
||||
stderr := process.stderr_slurp()
|
||||
exit_code := process.code
|
||||
process.close()
|
||||
assert exit_code == 0, 'stdout:\n${stdout}\nstderr:\n${stderr}'
|
||||
args := os.read_file(args_out)!.split_into_lines()
|
||||
assert args == ['wrapper', sample_path], args.str()
|
||||
}
|
||||
|
||||
fn compile_fake_c2v(vmodules_dir string) ! {
|
||||
c2v_dir := os.join_path(vmodules_dir, 'c2v')
|
||||
os.mkdir_all(c2v_dir)!
|
||||
fake_c2v_path := os.join_path(c2v_dir, 'fake_c2v.v')
|
||||
c2v_bin := os.join_path(c2v_dir, 'c2v' + exe_suffix())
|
||||
fake_c2v_source :=
|
||||
['import os', '', 'fn main() {', "\tout := os.getenv('C2V_ARGS_OUT')", "\tif out == '' {", "\t\teprintln('missing C2V_ARGS_OUT')", '\t\texit(1)', '\t}', "\tos.write_file(out, os.args[1..].join('\\n')) or {", '\t\teprintln(err)', '\t\texit(2)', '\t}', '}'].join('\n') +
|
||||
'\n'
|
||||
os.write_file(fake_c2v_path, fake_c2v_source)!
|
||||
res := os.execute('${qvexe} -o ${os.quoted_path(c2v_bin)} ${os.quoted_path(fake_c2v_path)}')
|
||||
assert res.exit_code == 0, res.output
|
||||
}
|
||||
|
||||
fn exe_suffix() string {
|
||||
return if os.user_os() == 'windows' { '.exe' } else { '' }
|
||||
}
|
||||
|
||||
fn restore_env(name string, value string) {
|
||||
if value == '' {
|
||||
os.unsetenv(name)
|
||||
} else {
|
||||
os.setenv(name, value, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
module main
|
||||
|
||||
import json.cjson
|
||||
|
||||
type Node = C.cJSON
|
||||
|
||||
fn as_n(p &cjson.Node) &Node {
|
||||
return unsafe { &Node(p) }
|
||||
}
|
||||
|
||||
fn as_c(p &Node) &cjson.Node {
|
||||
return unsafe { &cjson.Node(p) }
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn create_object() &Node {
|
||||
return as_n(cjson.create_object())
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn create_array() &Node {
|
||||
return as_n(cjson.create_array())
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn create_string(val string) &Node {
|
||||
return as_n(cjson.create_string(val))
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn create_number(val f64) &Node {
|
||||
return as_n(cjson.create_number(val))
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn create_true() &Node {
|
||||
return as_n(cjson.create_true())
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn create_false() &Node {
|
||||
return as_n(cjson.create_false())
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn create_null() &Node {
|
||||
return as_n(cjson.create_null())
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn add_item_to_object(mut obj Node, key string, item &Node) {
|
||||
mut o := unsafe { &cjson.Node(obj) }
|
||||
o.add_item_to_object(key, item)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn add_item_to_array(mut obj Node, item &Node) {
|
||||
mut o := as_c(obj)
|
||||
o.add_item_to_array(item)
|
||||
}
|
||||
|
||||
fn json_print(mut obj Node) string {
|
||||
mut o := as_c(obj)
|
||||
return o.print()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
demo.json
|
||||
@@ -0,0 +1,129 @@
|
||||
// usage test: v ast path_to_v/cmd/tools/vast/test/demo.v
|
||||
// will generate demo.json
|
||||
|
||||
// comment for module
|
||||
module main
|
||||
|
||||
// import module
|
||||
import os
|
||||
import math
|
||||
import time { Time, now }
|
||||
|
||||
// const decl
|
||||
const a = 1
|
||||
const b = 3
|
||||
const c = 'c'
|
||||
|
||||
// struct decl
|
||||
struct Point {
|
||||
x int
|
||||
mut:
|
||||
y int
|
||||
pub:
|
||||
z int
|
||||
pub mut:
|
||||
name string
|
||||
}
|
||||
|
||||
// method of Point
|
||||
pub fn (p Point) get_x() int {
|
||||
return p.x
|
||||
}
|
||||
|
||||
// embed struct
|
||||
struct MyPoint {
|
||||
Point
|
||||
title string
|
||||
}
|
||||
|
||||
// enum type
|
||||
enum Color {
|
||||
red
|
||||
green
|
||||
blue
|
||||
}
|
||||
|
||||
// type alias
|
||||
type Myint = int
|
||||
|
||||
// sum type
|
||||
type MySumType = bool | int | string
|
||||
|
||||
// function type
|
||||
type Myfn = fn (int) int
|
||||
|
||||
// interface type
|
||||
interface Myinterfacer {
|
||||
add(int, int) int
|
||||
sub(int, int) int
|
||||
}
|
||||
|
||||
// main function
|
||||
fn main() {
|
||||
add(1, 3)
|
||||
println(add(1, 2))
|
||||
println('ok') // comment println
|
||||
arr := [1, 3, 5, 7]
|
||||
for a in arr {
|
||||
println(a)
|
||||
add(1, 3)
|
||||
}
|
||||
color := Color.red
|
||||
println(color)
|
||||
println(os.args)
|
||||
m := math.max(1, 3)
|
||||
println(m)
|
||||
println(now())
|
||||
t := Time{}
|
||||
println(t)
|
||||
p := Point{
|
||||
x: 1
|
||||
y: 2
|
||||
z: 3
|
||||
}
|
||||
println(p)
|
||||
my_point := MyPoint{
|
||||
// x: 1
|
||||
// y: 3
|
||||
// z: 5
|
||||
}
|
||||
println(my_point.get_x())
|
||||
}
|
||||
|
||||
// normal function
|
||||
fn add(x int, y int) int {
|
||||
return x + y
|
||||
}
|
||||
|
||||
// function with defer stmt
|
||||
fn defer_fn() {
|
||||
mut x := 1
|
||||
println('start fn')
|
||||
defer {
|
||||
println('in defer block')
|
||||
println(x)
|
||||
}
|
||||
println('end fn')
|
||||
}
|
||||
|
||||
// generic function
|
||||
fn g_fn[T](p T) T {
|
||||
return p
|
||||
}
|
||||
|
||||
// generic struct
|
||||
struct GenericStruct[T] {
|
||||
point Point
|
||||
mut:
|
||||
model T
|
||||
}
|
||||
|
||||
// generic interface
|
||||
interface Gettable[T] {
|
||||
get() T
|
||||
}
|
||||
|
||||
// generic sumtype
|
||||
struct None {}
|
||||
|
||||
type MyOption[T] = Error | None | T
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2020-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module main
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
import v2.ast_dump
|
||||
import v2.parser
|
||||
import v2.pref
|
||||
import v2.token
|
||||
import v2.transformer
|
||||
import v2.types
|
||||
|
||||
fn main() {
|
||||
args := os.args[1..]
|
||||
|
||||
if args.len == 0 {
|
||||
eprintln('Usage: vast <file.v>')
|
||||
eprintln('Dumps AST to <file>_ast.json and <file>_ast_transformed.json')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
files := get_files(args)
|
||||
if files.len == 0 {
|
||||
eprintln('Usage: vast <file.v>')
|
||||
eprintln('Dumps AST to <file>_ast.json and <file>_ast_transformed.json')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Get base name for output files
|
||||
input_file := files[0]
|
||||
base_name := if input_file.ends_with('.v') {
|
||||
input_file[..input_file.len - 2]
|
||||
} else {
|
||||
input_file
|
||||
}
|
||||
|
||||
prefs := pref.new_preferences()
|
||||
mut file_set := token.FileSet.new()
|
||||
mut p := parser.Parser.new(&prefs)
|
||||
|
||||
// Parse builtin and dependencies first
|
||||
mut ast_files := []ast.File{}
|
||||
ast_files << p.parse_files(get_v_files_from_dir(prefs.get_vlib_module_path('builtin')), mut
|
||||
file_set)
|
||||
ast_files << p.parse_files(get_v_files_from_dir(prefs.get_vlib_module_path('strconv')), mut
|
||||
file_set)
|
||||
ast_files << p.parse_files(get_v_files_from_dir(prefs.get_vlib_module_path('strings')), mut
|
||||
file_set)
|
||||
ast_files << p.parse_files(get_v_files_from_dir(prefs.get_vlib_module_path('hash')), mut
|
||||
file_set)
|
||||
ast_files << p.parse_files(get_v_files_from_dir(prefs.get_vlib_module_path('math.bits')), mut
|
||||
file_set)
|
||||
|
||||
// Parse the user file(s)
|
||||
ast_files << p.parse_files(files, mut file_set)
|
||||
|
||||
// Write pre-transformation AST (only user files, not builtin)
|
||||
user_files := ast_files.filter(it.name in files)
|
||||
ast_json := ast_dump.dump_files(user_files)
|
||||
ast_file := '${base_name}_ast.json'
|
||||
os.write_file(ast_file, ast_json) or {
|
||||
eprintln('Failed to write ${ast_file}: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
println('Wrote ${ast_file}')
|
||||
|
||||
// Type check
|
||||
env := types.Environment.new()
|
||||
mut checker := types.Checker.new(&prefs, file_set, env)
|
||||
checker.check_files(ast_files)
|
||||
|
||||
// Transform
|
||||
mut trans := transformer.Transformer.new_with_pref(env, &prefs)
|
||||
transformed_files := trans.transform_files(ast_files)
|
||||
|
||||
// Write post-transformation AST (only user files)
|
||||
transformed_user_files := transformed_files.filter(it.name in files)
|
||||
transformed_json := ast_dump.dump_files(transformed_user_files)
|
||||
transformed_file := '${base_name}_ast_transformed.json'
|
||||
os.write_file(transformed_file, transformed_json) or {
|
||||
eprintln('Failed to write ${transformed_file}: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
println('Wrote ${transformed_file}')
|
||||
}
|
||||
|
||||
// get_files extracts source files from args, excluding options and their values
|
||||
fn get_files(args []string) []string {
|
||||
options_with_values := ['-o', '-output']
|
||||
mut files := []string{}
|
||||
mut skip_next := false
|
||||
for arg in args {
|
||||
if skip_next {
|
||||
skip_next = false
|
||||
continue
|
||||
}
|
||||
if arg.starts_with('-') {
|
||||
if arg in options_with_values {
|
||||
skip_next = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
files << arg
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
fn get_v_files_from_dir(dir string) []string {
|
||||
mut v_files := []string{}
|
||||
all_files := os.ls(dir) or { return v_files }
|
||||
for file in all_files {
|
||||
if file.ends_with('.v') && !file.ends_with('_test.v') {
|
||||
// Skip platform-specific files that don't match current platform
|
||||
if should_include_file(file) {
|
||||
v_files << os.join_path(dir, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
return v_files
|
||||
}
|
||||
|
||||
fn should_include_file(filename string) bool {
|
||||
// Skip test files (including _test.c.v)
|
||||
if filename.contains('_test.') {
|
||||
return false
|
||||
}
|
||||
// Skip JavaScript backend files
|
||||
if filename.ends_with('.js.v') {
|
||||
return false
|
||||
}
|
||||
// Handle platform-specific files
|
||||
current_os := os.user_os()
|
||||
// Check for OS-specific suffixes like _linux.v, _windows.v, _macos.v
|
||||
os_suffixes := ['_linux.v', '_windows.v', '_macos.v', '_darwin.v', '_freebsd.v', '_openbsd.v',
|
||||
'_netbsd.v', '_solaris.v', '_haiku.v', '_android.v']
|
||||
for suffix in os_suffixes {
|
||||
if filename.ends_with(suffix) {
|
||||
// Include only if it matches current OS
|
||||
if suffix == '_darwin.v' || suffix == '_macos.v' {
|
||||
return current_os == 'macos'
|
||||
} else if suffix == '_linux.v' {
|
||||
return current_os == 'linux'
|
||||
} else if suffix == '_windows.v' {
|
||||
return current_os == 'windows'
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Check for "not this OS" files like _nix.v (not windows)
|
||||
if filename.ends_with('_nix.v') {
|
||||
return current_os != 'windows'
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import flag
|
||||
import strings
|
||||
|
||||
const tool_version = '0.0.4'
|
||||
const tool_description = 'Converts a list of arbitrary files into a single v module file.'
|
||||
|
||||
struct Context {
|
||||
mut:
|
||||
files []string
|
||||
prefix string
|
||||
show_help bool
|
||||
module_name string
|
||||
write_file string
|
||||
}
|
||||
|
||||
fn (context Context) header() string {
|
||||
mut header_s := ''
|
||||
header_s += 'module ${context.module_name}\n'
|
||||
header_s += '\n'
|
||||
allfiles := context.files.join(' ')
|
||||
mut options := []string{}
|
||||
if context.prefix.len > 0 {
|
||||
options << '-p ${context.prefix}'
|
||||
}
|
||||
if context.module_name.len > 0 {
|
||||
options << '-m ${context.module_name}'
|
||||
}
|
||||
if context.write_file.len > 0 {
|
||||
options << '-w ${context.write_file}'
|
||||
}
|
||||
soptions := options.join(' ')
|
||||
header_s += '// File generated by:\n'
|
||||
header_s += '// v bin2v ${allfiles} ${soptions}\n'
|
||||
header_s += '// Please, do not edit this file.\n'
|
||||
header_s += '// Your changes may be overwritten.\n'
|
||||
return header_s
|
||||
}
|
||||
|
||||
fn (context Context) footer() string {
|
||||
return ''
|
||||
}
|
||||
|
||||
fn (context Context) file2v(bname string, fbytes []u8, _bn_max int) string {
|
||||
mut sb := strings.new_builder(1000)
|
||||
sb.write_string('const ${bname}_len' + ' = ${fbytes.len}\n')
|
||||
fbyte := fbytes[0]
|
||||
bnmae_line := 'const ${bname}' + ' = [u8(${fbyte}), '
|
||||
sb.write_string(bnmae_line)
|
||||
mut line_len := bnmae_line.len + 3
|
||||
for i := 1; i < fbytes.len; i++ {
|
||||
b := int(fbytes[i]).str()
|
||||
if line_len > 98 {
|
||||
sb.go_back(1)
|
||||
sb.write_string('\n\t')
|
||||
line_len = 8
|
||||
}
|
||||
if i == fbytes.len - 1 {
|
||||
sb.write_string(b)
|
||||
line_len += b.len
|
||||
} else {
|
||||
sb.write_string('${b}, ')
|
||||
line_len += b.len + 2
|
||||
}
|
||||
}
|
||||
sb.write_string(']!\n')
|
||||
return sb.str()
|
||||
}
|
||||
|
||||
fn (context Context) bname_and_bytes(file string) !(string, []u8) {
|
||||
fname := os.file_name(file)
|
||||
fname_escaped := fname.replace_each(['.', '_', '-', '_'])
|
||||
byte_name := '${context.prefix}${fname_escaped}'.to_lower()
|
||||
fbytes := os.read_bytes(file) or { return error('Error: ${err.msg()}') }
|
||||
return byte_name, fbytes
|
||||
}
|
||||
|
||||
fn (context Context) max_bname_len(bnames []string) int {
|
||||
mut max := 0
|
||||
for n in bnames {
|
||||
if n.len > max {
|
||||
max = n.len
|
||||
}
|
||||
}
|
||||
// Add 4 to max due to "_len" suffix
|
||||
return max + 4
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut context := Context{}
|
||||
mut fp := flag.new_flag_parser(os.args[1..])
|
||||
fp.application('v bin2v')
|
||||
fp.version(tool_version)
|
||||
fp.description(tool_description)
|
||||
fp.arguments_description('FILE [FILE]...')
|
||||
context.show_help = fp.bool('help', `h`, false, 'Show this help screen.')
|
||||
context.module_name = fp.string('module', `m`, 'binary', 'Name of the generated module.')
|
||||
context.prefix = fp.string('prefix', `p`, '', 'A prefix put before each resource name.')
|
||||
context.write_file = fp.string('write', `w`, '',
|
||||
'Write directly to a file with the given name.')
|
||||
if context.show_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
files := fp.finalize() or {
|
||||
eprintln('Error: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
real_files := files.filter(it != 'bin2v')
|
||||
if real_files.len == 0 {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
context.files = real_files
|
||||
if context.write_file != '' && os.file_ext(context.write_file) !in ['.vv', '.v'] {
|
||||
context.write_file += '.v'
|
||||
}
|
||||
mut file_byte_map := map[string][]u8{}
|
||||
for file in real_files {
|
||||
bname, fbytes := context.bname_and_bytes(file) or {
|
||||
eprintln(err.msg())
|
||||
exit(1)
|
||||
}
|
||||
file_byte_map[bname] = fbytes
|
||||
}
|
||||
max_bname := context.max_bname_len(file_byte_map.keys())
|
||||
if context.write_file.len > 0 {
|
||||
mut out_file := os.create(context.write_file)!
|
||||
out_file.write_string(context.header())!
|
||||
for bname, fbytes in file_byte_map {
|
||||
out_file.write_string(context.file2v(bname, fbytes, max_bname))!
|
||||
}
|
||||
out_file.write_string(context.footer())!
|
||||
} else {
|
||||
print(context.header())
|
||||
for bname, fbytes in file_byte_map {
|
||||
print(context.file2v(bname, fbytes, max_bname))
|
||||
}
|
||||
print(context.footer())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
module main
|
||||
|
||||
import flag
|
||||
import json
|
||||
import net.http
|
||||
import os
|
||||
import time
|
||||
|
||||
struct CreateBugReportResponse {
|
||||
pub:
|
||||
id string
|
||||
delete_url string
|
||||
delete_token string
|
||||
message string
|
||||
}
|
||||
|
||||
struct SendConfig {
|
||||
report_url string
|
||||
report_file string
|
||||
}
|
||||
|
||||
fn args_without_command() []string {
|
||||
args := os.args#[1..]
|
||||
if args.len > 0 && args[0] == 'bug-report-send' {
|
||||
return args[1..]
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
fn config_from_args() !SendConfig {
|
||||
mut fp := flag.new_flag_parser(args_without_command())
|
||||
fp.application('v bug-report-send')
|
||||
fp.version('0.0.1')
|
||||
fp.description('Send a V compiler bug report JSON payload.')
|
||||
fp.arguments_description('')
|
||||
show_help := fp.bool('help', `h`, false, 'Show this help screen.')
|
||||
report_url := fp.string('url', 0, '', 'Bug report endpoint URL.')
|
||||
report_file := fp.string('file', `f`, '', 'JSON report file to send.')
|
||||
if show_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
remaining := fp.finalize()!
|
||||
if remaining.len > 0 {
|
||||
return error('unexpected arguments: ${remaining.join(' ')}')
|
||||
}
|
||||
if report_url == '' {
|
||||
return error('missing required -url')
|
||||
}
|
||||
if report_file == '' {
|
||||
return error('missing required -file')
|
||||
}
|
||||
return SendConfig{
|
||||
report_url: report_url.trim_space().trim_right('/')
|
||||
report_file: report_file
|
||||
}
|
||||
}
|
||||
|
||||
fn send_bug_report(config SendConfig) !CreateBugReportResponse {
|
||||
report_json := os.read_file(config.report_file)!
|
||||
mut header := http.new_header(key: .content_type, value: 'application/json')
|
||||
header.set(.accept, 'application/json')
|
||||
response := http.fetch(
|
||||
method: .post
|
||||
url: config.report_url
|
||||
data: report_json
|
||||
header: header
|
||||
max_retries: 1
|
||||
read_timeout: 3 * time.second
|
||||
write_timeout: 3 * time.second
|
||||
)!
|
||||
if response.status_code < 200 || response.status_code >= 300 {
|
||||
return error('server responded with HTTP ${response.status_code}')
|
||||
}
|
||||
return json.decode(CreateBugReportResponse, response.body)!
|
||||
}
|
||||
|
||||
fn main() {
|
||||
config := config_from_args() or {
|
||||
eprintln('v bug-report-send: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
response := send_bug_report(config) or {
|
||||
eprintln('v bug-report-send: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
println('Sent C compiler bug report to ${config.report_url}.')
|
||||
if response.id != '' {
|
||||
println('Report id: ${response.id}')
|
||||
}
|
||||
delete_url := if response.delete_url != '' {
|
||||
response.delete_url
|
||||
} else if response.id != '' && response.delete_token != '' {
|
||||
'${config.report_url}/${response.id}?token=${response.delete_token}'
|
||||
} else {
|
||||
''
|
||||
}
|
||||
if delete_url != '' {
|
||||
println('Delete this report from the server with:')
|
||||
println(' curl -X DELETE ${os.quoted_path(delete_url)}')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
module main
|
||||
|
||||
import db.sqlite
|
||||
import flag
|
||||
import json
|
||||
import os
|
||||
import rand
|
||||
import time
|
||||
import vbugreport
|
||||
import veb
|
||||
|
||||
const default_port = 8080
|
||||
const max_report_body_bytes = 256 * 1024
|
||||
|
||||
struct ReportLine {
|
||||
pub:
|
||||
line int
|
||||
text string
|
||||
}
|
||||
|
||||
struct BugReport {
|
||||
pub:
|
||||
kind string
|
||||
v_version string
|
||||
target_os string
|
||||
target_backend string
|
||||
ccompiler string
|
||||
c_error string
|
||||
c_file string
|
||||
c_line int
|
||||
c_context []ReportLine
|
||||
v_file string
|
||||
v_line int
|
||||
v_context []ReportLine
|
||||
}
|
||||
|
||||
struct CreateBugReportResponse {
|
||||
pub:
|
||||
id string
|
||||
delete_url string
|
||||
delete_token string
|
||||
message string
|
||||
}
|
||||
|
||||
struct DeleteBugReportResponse {
|
||||
pub:
|
||||
id string
|
||||
deleted bool
|
||||
message string
|
||||
}
|
||||
|
||||
struct ServerConfig {
|
||||
host string
|
||||
port int
|
||||
db_path string
|
||||
public_url string
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub:
|
||||
public_url string
|
||||
pub mut:
|
||||
db sqlite.DB
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
fn args_without_command() []string {
|
||||
args := os.args#[1..]
|
||||
if args.len > 0 && args[0] == 'bug-report' {
|
||||
return args[1..]
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
fn db_path_from_env() string {
|
||||
db_path := os.getenv('V_BUG_REPORT_DB')
|
||||
if db_path != '' {
|
||||
return db_path
|
||||
}
|
||||
report_dir := os.getenv('V_BUG_REPORT_DIR')
|
||||
if report_dir != '' {
|
||||
return os.join_path(report_dir, 'bug_reports.sqlite')
|
||||
}
|
||||
return os.join_path(os.getwd(), 'bug_reports.sqlite')
|
||||
}
|
||||
|
||||
fn report_host_from_env() string {
|
||||
host := os.getenv('V_BUG_REPORT_HOST')
|
||||
if host != '' {
|
||||
return host
|
||||
}
|
||||
return 'localhost'
|
||||
}
|
||||
|
||||
fn report_port_from_env() int {
|
||||
bug_report_port := os.getenv('V_BUG_REPORT_PORT')
|
||||
if bug_report_port != '' {
|
||||
return bug_report_port.int()
|
||||
}
|
||||
port := os.getenv('PORT')
|
||||
if port != '' {
|
||||
return port.int()
|
||||
}
|
||||
return default_port
|
||||
}
|
||||
|
||||
fn public_url_from_config(flag_url string, host string, port int) string {
|
||||
url := if flag_url != '' { flag_url } else { os.getenv('V_BUG_REPORT_PUBLIC_URL') }
|
||||
if url != '' {
|
||||
return url.trim_space().trim_right('/')
|
||||
}
|
||||
public_host := if host == '' || host == '0.0.0.0' { 'localhost' } else { host }
|
||||
return 'http://${public_host}:${port}/bug-report'
|
||||
}
|
||||
|
||||
fn config_from_args() !ServerConfig {
|
||||
mut fp := flag.new_flag_parser(args_without_command())
|
||||
fp.application('v bug-report')
|
||||
fp.version('0.0.1')
|
||||
fp.description('Run the V compiler bug report receiver.')
|
||||
fp.arguments_description('')
|
||||
show_help := fp.bool('help', `h`, false, 'Show this help screen.')
|
||||
host := fp.string('host', 0, report_host_from_env(), 'Host to listen on.')
|
||||
port := fp.int('port', `p`, report_port_from_env(), 'Port to listen on.')
|
||||
db_path := fp.string('db', 0, db_path_from_env(), 'SQLite database path.')
|
||||
public_url_arg := fp.string('public-url', 0, '', 'Public /bug-report endpoint URL.')
|
||||
if show_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
remaining := fp.finalize()!
|
||||
if remaining.len > 0 {
|
||||
return error('unexpected arguments: ${remaining.join(' ')}')
|
||||
}
|
||||
return ServerConfig{
|
||||
host: host
|
||||
port: port
|
||||
db_path: db_path
|
||||
public_url: public_url_from_config(public_url_arg, host, port)
|
||||
}
|
||||
}
|
||||
|
||||
fn safe_id(id string) bool {
|
||||
if id.len < 8 || id.len > 80 {
|
||||
return false
|
||||
}
|
||||
for ch in id {
|
||||
if !ch.is_letter() && !ch.is_digit() && ch != `-` && ch != `_` {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn new_report_id() string {
|
||||
return rand.uuid_v4()
|
||||
}
|
||||
|
||||
fn (app App) delete_url(id string, token string) string {
|
||||
return '${app.public_url}/${id}?token=${token}'
|
||||
}
|
||||
|
||||
fn delete_token_from_request(ctx Context) string {
|
||||
if token := ctx.query['token'] {
|
||||
return token
|
||||
}
|
||||
return ctx.get_custom_header('X-Delete-Token') or { '' }
|
||||
}
|
||||
|
||||
fn ensure_db_parent(db_path string) ! {
|
||||
if db_path == ':memory:' || db_path.starts_with('file:') {
|
||||
return
|
||||
}
|
||||
parent_dir := os.dir(db_path)
|
||||
if parent_dir != '' && parent_dir != '.' {
|
||||
os.mkdir_all(parent_dir)!
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_db(db_path string) !sqlite.DB {
|
||||
ensure_db_parent(db_path)!
|
||||
mut db := sqlite.connect_full(db_path, [.readwrite, .create, .fullmutex], '')!
|
||||
db.busy_timeout(3000)
|
||||
return db
|
||||
}
|
||||
|
||||
fn init_db(db sqlite.DB) ! {
|
||||
db.exec("create table if not exists bug_reports (
|
||||
id text primary key,
|
||||
delete_token text not null,
|
||||
created_at text not null,
|
||||
remote_ip text not null,
|
||||
user_agent text not null,
|
||||
c_file_name text not null,
|
||||
target_os text not null,
|
||||
ccompiler text not null,
|
||||
error_string text not null,
|
||||
lines text not null,
|
||||
v_lines text not null default ''
|
||||
)")!
|
||||
// Add columns that were introduced after the table already existed, so older
|
||||
// databases pick them up without losing their stored reports.
|
||||
ensure_column(db, 'bug_reports', 'v_lines', "text not null default ''")!
|
||||
db.exec('create index if not exists idx_bug_reports_created_at
|
||||
on bug_reports(created_at)')!
|
||||
}
|
||||
|
||||
// ensure_column adds `column` to `table` when it is not present yet. SQLite has no
|
||||
// `add column if not exists`, so the existing columns are inspected first.
|
||||
fn ensure_column(db sqlite.DB, table string, column string, definition string) ! {
|
||||
rows := db.exec('pragma table_info(${table})')!
|
||||
for row in rows {
|
||||
// pragma table_info columns are: cid, name, type, notnull, dflt_value, pk
|
||||
if row.vals.len > 1 && row.vals[1] == column {
|
||||
return
|
||||
}
|
||||
}
|
||||
db.exec('alter table ${table} add column ${column} ${definition}')!
|
||||
}
|
||||
|
||||
// create stores a compiler bug report and returns its deletion token.
|
||||
@['/bug-report'; post]
|
||||
pub fn (mut app App) create(mut ctx Context) veb.Result {
|
||||
if ctx.req.data.len == 0 {
|
||||
return ctx.request_error('empty report body')
|
||||
}
|
||||
if ctx.req.data.len > max_report_body_bytes {
|
||||
ctx.res.set_status(.bad_request)
|
||||
return ctx.text('report body is too large')
|
||||
}
|
||||
report := json.decode(BugReport, ctx.req.data) or {
|
||||
return ctx.request_error('invalid report JSON: ${err}')
|
||||
}
|
||||
if report.kind != 'v-c-compiler-error' {
|
||||
return ctx.request_error('unsupported report kind')
|
||||
}
|
||||
stored_report := vbugreport.new_stored_c_error_report(report.c_file, report.target_os,
|
||||
report.ccompiler, report.c_error, report.c_context.map(it.text),
|
||||
report.v_context.map(it.text))
|
||||
id := new_report_id()
|
||||
delete_token := rand.uuid_v4()
|
||||
app.db.exec_param_many('insert into bug_reports (
|
||||
id, delete_token, created_at, remote_ip, user_agent,
|
||||
c_file_name, target_os, ccompiler, error_string, lines, v_lines
|
||||
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
||||
id,
|
||||
delete_token,
|
||||
time.utc().format_rfc3339(),
|
||||
ctx.ip(),
|
||||
ctx.user_agent(),
|
||||
stored_report.c_file_name,
|
||||
stored_report.target_os,
|
||||
stored_report.ccompiler,
|
||||
stored_report.error_string,
|
||||
stored_report.lines,
|
||||
stored_report.v_lines,
|
||||
]) or { return ctx.server_error('could not store report') }
|
||||
return ctx.json(CreateBugReportResponse{
|
||||
id: id
|
||||
delete_url: app.delete_url(id, delete_token)
|
||||
delete_token: delete_token
|
||||
message: 'created'
|
||||
})
|
||||
}
|
||||
|
||||
// delete removes a compiler bug report when the deletion token matches.
|
||||
@['/bug-report/:id'; delete]
|
||||
pub fn (mut app App) delete(mut ctx Context, id string) veb.Result {
|
||||
if !safe_id(id) {
|
||||
return ctx.request_error('invalid report id')
|
||||
}
|
||||
rows := app.db.exec_param('select delete_token from bug_reports where id = ?', id) or {
|
||||
return ctx.server_error('could not read report')
|
||||
}
|
||||
if rows.len == 0 {
|
||||
return ctx.not_found()
|
||||
}
|
||||
if delete_token_from_request(ctx) != rows[0].vals[0] {
|
||||
ctx.res.set_status(.forbidden)
|
||||
return ctx.text('invalid delete token')
|
||||
}
|
||||
app.db.exec_param('delete from bug_reports where id = ?', id) or {
|
||||
return ctx.server_error('could not delete report')
|
||||
}
|
||||
return ctx.json(DeleteBugReportResponse{
|
||||
id: id
|
||||
deleted: true
|
||||
message: 'deleted'
|
||||
})
|
||||
}
|
||||
|
||||
// healthz returns a basic readiness response for local checks.
|
||||
@['/healthz'; get]
|
||||
pub fn (mut app App) healthz(mut ctx Context) veb.Result {
|
||||
return ctx.text('ok')
|
||||
}
|
||||
|
||||
fn main() {
|
||||
config := config_from_args() or {
|
||||
eprintln('v bug-report: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
mut db := connect_db(config.db_path) or { panic(err) }
|
||||
init_db(db) or { panic(err) }
|
||||
mut app := &App{
|
||||
public_url: config.public_url
|
||||
db: db
|
||||
}
|
||||
veb.run_at[App, Context](mut app, host: config.host, port: config.port, family: .ip) or {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import os
|
||||
import term
|
||||
import readline
|
||||
import vbugreport
|
||||
|
||||
fn elog(msg string) {
|
||||
eprintln(term.ecolorize(term.gray, msg))
|
||||
}
|
||||
|
||||
fn olog(msg string) {
|
||||
println(term.colorize(term.green, msg))
|
||||
}
|
||||
|
||||
fn vversion() string {
|
||||
vexe := os.getenv('VEXE')
|
||||
return os.execute('${os.quoted_path(vexe)} version').output.trim_space()
|
||||
}
|
||||
|
||||
// get output from `v doctor`
|
||||
fn get_vdoctor_output(is_verbose bool) string {
|
||||
vexe := os.getenv('VEXE')
|
||||
verbose_flag := if is_verbose { '-v' } else { '' }
|
||||
result := os.execute('${os.quoted_path(vexe)} ${verbose_flag} doctor')
|
||||
if result.exit_code != 0 {
|
||||
elog('> unable to get `v doctor` output: ${result.output}')
|
||||
return ''
|
||||
}
|
||||
return result.output
|
||||
}
|
||||
|
||||
fn runv(label string, user_cmd string) os.Result {
|
||||
mut result := os.Result{}
|
||||
elog('> ${label} using: ${term.ecolorize(term.magenta, user_cmd)}')
|
||||
result = os.execute(user_cmd)
|
||||
print(result.output)
|
||||
return result
|
||||
}
|
||||
|
||||
// get output from `./v -g -o vdbg cmd/v && ./vdbg -user_args run file.v`
|
||||
fn get_v_build_output(is_verbose bool, is_yes bool, file_path string, user_args string, generated_file string) string {
|
||||
mut result := os.Result{}
|
||||
mut vexe := os.getenv('VEXE')
|
||||
|
||||
// prepare a V compiler with -g to have better backtraces if possible
|
||||
wd := os.getwd()
|
||||
vroot := @VMODROOT
|
||||
os.chdir(vroot) or {}
|
||||
verbose_flag := if is_verbose { '-v' } else { '' }
|
||||
vdbg_path := $if windows { '${vroot}/vdbg.exe' } $else { '${vroot}/vdbg' }
|
||||
vdbg_compilation_cmd := '${os.quoted_path(vexe)} ${verbose_flag} -g -o ${os.quoted_path(vdbg_path)} cmd/v'
|
||||
result = runv('Prepare vdbg', vdbg_compilation_cmd)
|
||||
os.chdir(wd) or {}
|
||||
|
||||
if result.exit_code == 0 {
|
||||
vexe = vdbg_path
|
||||
} else {
|
||||
elog('> unable to compile V in debug mode: ${result.output}\ncommand: ${vdbg_compilation_cmd}\n')
|
||||
}
|
||||
|
||||
result = runv('Compile',
|
||||
'${os.quoted_path(vexe)} ${verbose_flag} ${user_args} ${os.quoted_path(file_path)}')
|
||||
defer {
|
||||
os.rm(vdbg_path) or {
|
||||
if is_verbose {
|
||||
elog('> unable to delete `vdbg`: ${err}')
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.exit_code == 0 {
|
||||
real_generated_file := os.real_path(generated_file)
|
||||
defer(fn) {
|
||||
os.rm(generated_file) or {
|
||||
if is_verbose {
|
||||
elog('> unable to delete generated file: ${err}')
|
||||
}
|
||||
}
|
||||
}
|
||||
run := is_yes
|
||||
|| ask('It looks like the compilation went well, do you want to run the file?')
|
||||
if run {
|
||||
result = runv('Run', real_generated_file)
|
||||
if result.exit_code == 0 && !is_yes {
|
||||
elog('> The file ran correctly as well.')
|
||||
confirm_or_exit('Are you sure you want to continue?')
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.output
|
||||
}
|
||||
|
||||
fn ask(msg string) bool {
|
||||
prompt := os.input_opt(term.colorize(term.bright_white, '${msg} [Y/n] ')) or { 'y' }
|
||||
return prompt == '' || prompt[0].ascii_str().to_lower() != 'n'
|
||||
}
|
||||
|
||||
fn confirm_or_exit(msg string) {
|
||||
if !ask(msg) {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
unbuffer_stdout()
|
||||
mut compiler_args := []string{}
|
||||
mut file_path := ''
|
||||
is_verbose := '-v' in os.args
|
||||
is_yes := '-y' in os.args
|
||||
|
||||
for arg in os.args[1..] {
|
||||
if arg == 'bug' {
|
||||
continue
|
||||
}
|
||||
if arg.ends_with('.v') || arg.ends_with('.vsh') || arg.ends_with('.vv') {
|
||||
if file_path != '' {
|
||||
elog('> v bug: only one V file can be submitted')
|
||||
exit(1)
|
||||
}
|
||||
file_path = arg
|
||||
} else {
|
||||
if arg !in ['-y', '-v'] {
|
||||
compiler_args << arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if file_path == '' {
|
||||
elog('> v bug: no v file listed to report')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
os.unsetenv('VCOLORS')
|
||||
// collect error information
|
||||
// output from `v doctor`
|
||||
vdoctor_output := get_vdoctor_output(is_verbose)
|
||||
// file content
|
||||
file_content := os.read_file(file_path) or {
|
||||
elog('> unable to get file "${file_path}" content: ${err}')
|
||||
''
|
||||
}
|
||||
|
||||
user_args := compiler_args.join(' ')
|
||||
mut generated_file := file_path.all_before_last('.')
|
||||
if os.user_os() == 'windows' {
|
||||
generated_file += '.exe'
|
||||
}
|
||||
build_output := get_v_build_output(is_verbose, is_yes, file_path, user_args, generated_file)
|
||||
|
||||
// ask the user if he wants to submit even after an error
|
||||
if !is_yes && (vdoctor_output == '' || file_content == '' || build_output == '') {
|
||||
elog('> Error while retrieving the information.')
|
||||
confirm_or_exit('Do you want to continue?')
|
||||
}
|
||||
|
||||
expected_result := readline.read_line('What did you expect to see? ') or {
|
||||
// Ctrl-C was pressed
|
||||
elog('\nCanceled')
|
||||
exit(1)
|
||||
}
|
||||
// open prefilled issue creation page, or print link as a fallback
|
||||
|
||||
if !is_yes && vdoctor_output.contains('behind V master') {
|
||||
olog('> It looks like your installation of V is outdated.')
|
||||
olog('> We advise you to run `v up` before submitting an issue.')
|
||||
confirm_or_exit('Are you sure you want to continue?')
|
||||
}
|
||||
|
||||
report := vbugreport.new_bug_report(file_path, generated_file, user_args, expected_result,
|
||||
vversion(), vdoctor_output, file_content, build_output)
|
||||
delivery := vbugreport.prepare_bug_report_delivery(report, file_path)
|
||||
if delivery.mode == .local_report {
|
||||
os.write_file(delivery.local_report_path, delivery.local_report_body) or {
|
||||
elog('> unable to write the bug report file `${delivery.local_report_path}`: ${err}')
|
||||
}
|
||||
elog('> The full bug report is too large to prefill on GitHub.')
|
||||
elog('> The full report was saved to `${delivery.local_report_path}`.')
|
||||
elog('> Open the bug form below, then paste the saved markdown sections or attach the file:')
|
||||
}
|
||||
os.open_uri(delivery.uri) or {
|
||||
if is_verbose {
|
||||
elog(err.str())
|
||||
}
|
||||
olog(delivery.uri)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import testing
|
||||
|
||||
const vroot = os.dir(os.real_path(os.getenv_opt('VEXE') or { @VEXE }))
|
||||
|
||||
// build as a project folder
|
||||
const efolders = [
|
||||
'examples/viewer',
|
||||
'examples/fasthttp',
|
||||
'examples/veb_orm_jwt',
|
||||
'examples/veb_fullstack',
|
||||
]
|
||||
|
||||
pub fn normalised_vroot_path(path string) string {
|
||||
return os.real_path(os.join_path_single(vroot, path)).replace('\\', '/')
|
||||
}
|
||||
|
||||
fn main() {
|
||||
args_string := os.args[1..].join(' ')
|
||||
params := args_string.all_before('build-examples')
|
||||
mut skip_prefixes := efolders.map(normalised_vroot_path(it))
|
||||
res := testing.v_build_failing_skipped(params, 'examples', skip_prefixes, fn (mut session testing.TestSession) {
|
||||
for x in efolders {
|
||||
pathsegments := x.split_any('/')
|
||||
fpath := os.real_path(os.join_path(vroot, ...pathsegments))
|
||||
session.skip_files = session.skip_files.filter(it != fpath)
|
||||
session.add(fpath)
|
||||
}
|
||||
})
|
||||
if res {
|
||||
exit(1)
|
||||
}
|
||||
if testing.v_build_failing_skipped(params + '-live', os.join_path_single('examples',
|
||||
'hot_reload'), skip_prefixes, fn (mut session testing.TestSession) {})
|
||||
{
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import testing
|
||||
import v.util
|
||||
|
||||
// Note: tools like vdoc are compiled in their own subfolder
|
||||
// => cmd/tools/vdoc/vdoc.exe
|
||||
// Usually, they have several top level .v files in the subfolder,
|
||||
// that cannot be compiled separately, but instead, the whole folder,
|
||||
// should be compiled (v folder).
|
||||
// To implement that, these folders are initially skipped, then added
|
||||
// as a whole *after the testing.prepare_test_session call*.
|
||||
const tools_in_subfolders = ['vast', 'vcreate', 'vdoc', 'vpm', 'vsqlite', 'vsymlink', 'vvet',
|
||||
'vwhere', 'vcover']
|
||||
|
||||
// v2 is temporarily disabled, so tools that depend on it are skipped too.
|
||||
const temporarily_disabled_tool_subfolders = ['vast2']
|
||||
|
||||
// non_packaged_tools are tools that should not be packaged with
|
||||
// prebuild versions of V, to keep the size smaller.
|
||||
// They are mainly useful for the V project itself, not to end users.
|
||||
const non_packaged_tools = ['gen1m', 'gen_vc', 'fast', 'wyhash']
|
||||
|
||||
fn main() {
|
||||
if os.getenv('VTEST_SANDBOXED_PACKAGING') == '' {
|
||||
util.ensure_modules_for_all_tools_are_installed('-v' in os.args)
|
||||
}
|
||||
args_string := os.args[1..].join(' ')
|
||||
vexe := os.getenv('VEXE')
|
||||
vroot := os.dir(vexe)
|
||||
os.chdir(vroot)!
|
||||
folder := os.join_path('cmd', 'tools')
|
||||
tfolder := os.join_path(vroot, 'cmd', 'tools')
|
||||
main_label := 'Building ${folder} ...'
|
||||
finish_label := 'building ${folder}'
|
||||
|
||||
mut skips := []string{}
|
||||
for stool in tools_in_subfolders {
|
||||
skips << os.join_path(tfolder, stool).replace('\\', '/')
|
||||
}
|
||||
for stool in temporarily_disabled_tool_subfolders {
|
||||
skips << os.join_path(tfolder, stool).replace('\\', '/')
|
||||
}
|
||||
buildopts := args_string.all_before('build-tools')
|
||||
mut session := testing.prepare_test_session(buildopts, folder, skips, main_label)
|
||||
session.rm_binaries = false
|
||||
session.build_tools = true
|
||||
for stool in tools_in_subfolders {
|
||||
session.add(os.join_path(tfolder, stool))
|
||||
}
|
||||
// eprintln('> session.files: ${session.files}')
|
||||
// eprintln('> session.skip_files: ${session.skip_files}')
|
||||
session.test()
|
||||
eprintln(session.benchmark.total_message(finish_label))
|
||||
if session.failed_cmds.len > 0 {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
mut executables := os.ls(session.vtmp_dir)!
|
||||
executables.sort()
|
||||
for texe in executables {
|
||||
tname := texe.replace(os.file_ext(texe), '')
|
||||
if tname in non_packaged_tools {
|
||||
continue
|
||||
}
|
||||
//
|
||||
tpath := os.join_path(session.vtmp_dir, texe)
|
||||
if texe.ends_with('_builder') || texe.ends_with('_builder.exe') {
|
||||
os.mv_by_cp(tpath, os.join_path(tfolder, 'builders', texe)) or { panic(err) }
|
||||
continue
|
||||
}
|
||||
if tname in tools_in_subfolders {
|
||||
os.mv_by_cp(tpath, os.join_path(tfolder, tname, texe)) or { panic(err) }
|
||||
continue
|
||||
}
|
||||
if os.is_dir(tpath) {
|
||||
continue
|
||||
}
|
||||
target_path := os.join_path(tfolder, texe)
|
||||
os.mv_by_cp(tpath, target_path) or {
|
||||
emsg := err.msg()
|
||||
if !emsg.contains('vbuild-tools') && !emsg.contains('vtest-all') {
|
||||
eprintln('error while moving ${tpath} to ${target_path}: ${emsg}')
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module main
|
||||
|
||||
import testing
|
||||
|
||||
fn main() {
|
||||
if testing.building_any_v_binaries_failed() {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) 2019-2024 Subhomoy Haldar. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
module main
|
||||
|
||||
import flag
|
||||
import os
|
||||
import regex
|
||||
import semver
|
||||
|
||||
const tool_name = os.file_name(os.executable())
|
||||
const tool_version = '0.1.0'
|
||||
const tool_description = '\n Bump the semantic version of the v.mod and/or specified files.
|
||||
|
||||
The first instance of a version number is replaced with the new version.
|
||||
Additionally, the line affected must contain the word "version" in any
|
||||
form of capitalization. For instance, the following lines will be
|
||||
recognized by the heuristic:
|
||||
|
||||
tool_version = \'1.2.1\'
|
||||
version: \'0.2.42\'
|
||||
VERSION = "1.23.8"
|
||||
|
||||
If certain lines need to be skipped, use the --skip option. For instance,
|
||||
the following command will skip lines containing "tool-version":
|
||||
|
||||
v bump --patch --skip "tool-version" [files...]
|
||||
|
||||
Examples:
|
||||
Bump the patch version in v.mod if it exists
|
||||
v bump --patch
|
||||
Bump the major version in v.mod and vls.v
|
||||
v bump --major v.mod vls.v
|
||||
Upgrade the minor version in sample.v only
|
||||
v bump --minor sample.v
|
||||
'
|
||||
|
||||
const semver_query = r'((0)|([1-9]\d*)\.){2}(0)|([1-9]\d*)(\-[\w\d\.\-_]+)?(\+[\w\d\.\-_]+)?'
|
||||
|
||||
struct Options {
|
||||
show_help bool
|
||||
major bool
|
||||
minor bool
|
||||
patch bool
|
||||
skip string
|
||||
}
|
||||
|
||||
type ReplacementFunction = fn (re regex.RE, input string, start int, end int) string
|
||||
|
||||
fn replace_with_increased_patch_version(_re regex.RE, input string, start int, end int) string {
|
||||
version := semver.from(input[start..end]) or { return input }
|
||||
return version.increment(.patch).str()
|
||||
}
|
||||
|
||||
fn replace_with_increased_minor_version(_re regex.RE, input string, start int, end int) string {
|
||||
version := semver.from(input[start..end]) or { return input }
|
||||
return version.increment(.minor).str()
|
||||
}
|
||||
|
||||
fn replace_with_increased_major_version(_re regex.RE, input string, start int, end int) string {
|
||||
version := semver.from(input[start..end]) or { return input }
|
||||
return version.increment(.major).str()
|
||||
}
|
||||
|
||||
fn get_replacement_function(options Options) ReplacementFunction {
|
||||
if options.patch {
|
||||
return replace_with_increased_patch_version
|
||||
} else if options.minor {
|
||||
return replace_with_increased_minor_version
|
||||
} else if options.major {
|
||||
return replace_with_increased_major_version
|
||||
}
|
||||
return replace_with_increased_patch_version
|
||||
}
|
||||
|
||||
fn process_file(input_file string, options Options) ! {
|
||||
lines := os.read_lines(input_file) or { return error('Failed to read file: ${input_file}') }
|
||||
|
||||
mut re := regex.regex_opt(semver_query) or { return error('Could not create a RegEx parser.') }
|
||||
|
||||
repl_fn := get_replacement_function(options)
|
||||
|
||||
mut new_lines := []string{cap: lines.len}
|
||||
mut replacement_complete := false
|
||||
|
||||
for line in lines {
|
||||
// Copy over the remaining lines normally if the replacement is complete
|
||||
if replacement_complete {
|
||||
new_lines << line
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if replacement is necessary
|
||||
updated_line := if line.to_lower().contains('version') && !(options.skip != ''
|
||||
&& line.contains(options.skip)) {
|
||||
replacement_complete = true
|
||||
re.replace_by_fn(line, repl_fn)
|
||||
} else {
|
||||
line
|
||||
}
|
||||
new_lines << updated_line
|
||||
}
|
||||
|
||||
// Add a trailing newline
|
||||
new_lines << ''
|
||||
|
||||
backup_file := input_file + '.cache'
|
||||
|
||||
// Remove the backup file if it exists.
|
||||
os.rm(backup_file) or {}
|
||||
|
||||
// Rename the original to the backup.
|
||||
os.mv(input_file, backup_file) or { return error('Failed to move file: ${input_file}') }
|
||||
|
||||
// Process the old file and write it back to the original.
|
||||
os.write_file(input_file, new_lines.join_lines()) or {
|
||||
return error('Failed to write file: ${input_file}')
|
||||
}
|
||||
|
||||
// Remove the backup file.
|
||||
os.rm(backup_file) or {}
|
||||
|
||||
if replacement_complete {
|
||||
println('Bumped version in ${input_file}')
|
||||
} else {
|
||||
println('No changes made in ${input_file}')
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if os.args.len < 2 {
|
||||
eprintln('Usage: ${tool_name} [options] [file1 file2 ...]
|
||||
${tool_description}
|
||||
Try ${tool_name} -h for more help...')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
|
||||
fp.application(tool_name)
|
||||
fp.version(tool_version)
|
||||
fp.description(tool_description)
|
||||
fp.arguments_description('[file1 file2 ...]')
|
||||
fp.skip_executable()
|
||||
|
||||
options := Options{
|
||||
show_help: fp.bool('help', `h`, false, 'Show this help text.')
|
||||
patch: fp.bool('patch', `p`, false, 'Bump the patch version.')
|
||||
minor: fp.bool('minor', `n`, false, 'Bump the minor version.')
|
||||
major: fp.bool('major', `m`, false, 'Bump the major version.')
|
||||
skip: fp.string('skip', `s`, '', 'Skip lines matching this substring.').trim_space()
|
||||
}
|
||||
|
||||
remaining := fp.finalize() or {
|
||||
eprintln(fp.usage())
|
||||
exit(1)
|
||||
}
|
||||
|
||||
if options.show_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
|
||||
validate_options(options) or {
|
||||
eprintln(fp.usage())
|
||||
exit(1)
|
||||
}
|
||||
|
||||
files := remaining[1..]
|
||||
|
||||
if files.len == 0 {
|
||||
if !os.exists('v.mod') {
|
||||
eprintln('v.mod does not exist. You can create one using "v init".')
|
||||
exit(1)
|
||||
}
|
||||
process_file('v.mod', options) or {
|
||||
eprintln('Failed to process v.mod: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
for input_file in files {
|
||||
if !os.exists(input_file) {
|
||||
eprintln('File not found: ${input_file}')
|
||||
exit(1)
|
||||
}
|
||||
process_file(input_file, options) or {
|
||||
eprintln('Failed to process ${input_file}: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_options(options Options) ! {
|
||||
if options.patch && options.major {
|
||||
return error('Cannot specify both --patch and --major.')
|
||||
}
|
||||
|
||||
if options.patch && options.minor {
|
||||
return error('Cannot specify both --patch and --minor.')
|
||||
}
|
||||
|
||||
if options.major && options.minor {
|
||||
return error('Cannot specify both --major and --minor.')
|
||||
}
|
||||
|
||||
if !(options.patch || options.major || options.minor) {
|
||||
return error('Must specify one of --patch, --major, or --minor.')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import os
|
||||
|
||||
const vexe = @VEXE
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'vbump')
|
||||
|
||||
fn testsuite_begin() {
|
||||
os.mkdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
struct BumpTestCase {
|
||||
file_name string
|
||||
contents string
|
||||
line int
|
||||
expected_patch string
|
||||
expected_minor string
|
||||
expected_major string
|
||||
}
|
||||
|
||||
const test_cases = [
|
||||
BumpTestCase{
|
||||
file_name: 'v.mod'
|
||||
contents: "Module {
|
||||
name: 'Sample'
|
||||
description: 'Sample project'
|
||||
version: '1.2.6'
|
||||
license: 'MIT'
|
||||
dependencies: []
|
||||
}
|
||||
|
||||
"
|
||||
line: 3
|
||||
expected_patch: " version: '1.2.7'"
|
||||
expected_minor: " version: '1.3.0'"
|
||||
expected_major: " version: '2.0.0'"
|
||||
},
|
||||
BumpTestCase{
|
||||
file_name: 'random_versions.vv'
|
||||
contents: "
|
||||
1.1.2
|
||||
1.2.5
|
||||
3.21.73
|
||||
version = '1.5.1'
|
||||
|
||||
"
|
||||
line: 4
|
||||
expected_patch: "version = '1.5.2'"
|
||||
expected_minor: "version = '1.6.0'"
|
||||
expected_major: "version = '2.0.0'"
|
||||
},
|
||||
BumpTestCase{
|
||||
file_name: 'sample_tool.v'
|
||||
contents: "// Module comment and copyright information
|
||||
import os
|
||||
import flag
|
||||
|
||||
const tool_name = os.file_name(os.executable())
|
||||
const tool_version = '0.1.33'
|
||||
|
||||
fn main() {
|
||||
// stuff
|
||||
}
|
||||
"
|
||||
line: 5
|
||||
expected_patch: "const tool_version = '0.1.34'"
|
||||
expected_minor: "const tool_version = '0.2.0'"
|
||||
expected_major: "const tool_version = '1.0.0'"
|
||||
},
|
||||
]
|
||||
|
||||
fn run_individual_test(case BumpTestCase) ! {
|
||||
test_file := os.join_path_single(tfolder, case.file_name)
|
||||
|
||||
os.rm(test_file) or {}
|
||||
os.write_file(test_file, case.contents)!
|
||||
|
||||
os.execute_or_exit('${os.quoted_path(vexe)} bump --patch ${os.quoted_path(test_file)}')
|
||||
patch_lines := os.read_lines(test_file)!
|
||||
assert patch_lines[case.line] == case.expected_patch
|
||||
|
||||
os.execute_or_exit('${os.quoted_path(vexe)} bump --minor ${os.quoted_path(test_file)}')
|
||||
minor_lines := os.read_lines(test_file)!
|
||||
assert minor_lines[case.line] == case.expected_minor
|
||||
|
||||
os.execute_or_exit('${os.quoted_path(vexe)} bump --major ${os.quoted_path(test_file)}')
|
||||
major_lines := os.read_lines(test_file)!
|
||||
assert major_lines[case.line] == case.expected_major
|
||||
|
||||
os.rm(test_file)!
|
||||
}
|
||||
|
||||
fn test_all_bump_cases() {
|
||||
for case in test_cases {
|
||||
run_individual_test(case) or { panic(err) }
|
||||
}
|
||||
}
|
||||
|
||||
struct SkipTestCase {
|
||||
file_name string
|
||||
contents string
|
||||
skip string
|
||||
line int
|
||||
expected_patch string
|
||||
expected_minor string
|
||||
expected_major string
|
||||
}
|
||||
|
||||
const skip_test_cases = [
|
||||
SkipTestCase{
|
||||
file_name: 'CITATION.cff'
|
||||
contents: 'abstract: A sample CLI tool made in V that prints geometric shapes to the screen.
|
||||
authors:
|
||||
- alias: hungrybluedev
|
||||
family-names: Haldar
|
||||
given-names: Subhomoy
|
||||
cff-version: 1.2.0
|
||||
date-released: 2023-04-20
|
||||
license: MIT
|
||||
message: Please cite this software using these information.
|
||||
repository-code: https://github.com/hungrybluedev/geo
|
||||
title: geo
|
||||
url: https://github.com/hungrybluedev/geo
|
||||
version: 0.2.4
|
||||
'
|
||||
line: 12
|
||||
skip: 'cff-version'
|
||||
expected_patch: 'version: 0.2.5'
|
||||
expected_minor: 'version: 0.3.0'
|
||||
expected_major: 'version: 1.0.0'
|
||||
},
|
||||
]
|
||||
|
||||
fn run_skip_test(case SkipTestCase) ! {
|
||||
test_file := os.join_path_single(tfolder, case.file_name)
|
||||
|
||||
os.rm(test_file) or {}
|
||||
os.write_file(test_file, case.contents)!
|
||||
|
||||
os.execute_or_exit('${os.quoted_path(vexe)} bump --patch --skip="${case.skip}" ${os.quoted_path(test_file)}')
|
||||
patch_lines := os.read_lines(test_file)!
|
||||
assert patch_lines[case.line] == case.expected_patch
|
||||
|
||||
os.execute_or_exit('${os.quoted_path(vexe)} bump --minor --skip="${case.skip}" ${os.quoted_path(test_file)}')
|
||||
minor_lines := os.read_lines(test_file)!
|
||||
assert minor_lines[case.line] == case.expected_minor
|
||||
|
||||
os.execute_or_exit('${os.quoted_path(vexe)} bump --major --skip="${case.skip}" ${os.quoted_path(test_file)}')
|
||||
major_lines := os.read_lines(test_file)!
|
||||
assert major_lines[case.line] == case.expected_major
|
||||
|
||||
os.rm(test_file)!
|
||||
}
|
||||
|
||||
fn test_all_skip_bump_cases() ! {
|
||||
for case in skip_test_cases {
|
||||
run_skip_test(case) or { panic(err) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,930 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module main
|
||||
|
||||
import os
|
||||
import os.cmdline
|
||||
import rand
|
||||
import term
|
||||
import v.help
|
||||
import regex
|
||||
|
||||
const too_long_line_length_example = 120
|
||||
const too_long_line_length_codeblock = 120
|
||||
const too_long_line_length_table = 160
|
||||
const too_long_line_length_link = 250
|
||||
const too_long_line_length_other = 100
|
||||
const term_colors = term.can_show_color_on_stderr()
|
||||
const hide_warnings = '-hide-warnings' in os.args || '-w' in os.args
|
||||
const show_progress = os.getenv('GITHUB_JOB') == '' && '-silent' !in os.args
|
||||
const non_option_args = cmdline.only_non_options(os.args[2..])
|
||||
const is_verbose = os.getenv('VERBOSE') != ''
|
||||
const vcheckfolder = os.join_path(os.vtmp_dir(), 'vcheck_${os.getpid()}')
|
||||
const should_autofix = os.getenv('VAUTOFIX') != '' || '-fix' in os.args
|
||||
const vexe = @VEXE
|
||||
|
||||
struct CheckResult {
|
||||
pub mut:
|
||||
files int
|
||||
lines int
|
||||
examples int
|
||||
oks int
|
||||
warnings int
|
||||
ferrors int
|
||||
errors int
|
||||
}
|
||||
|
||||
struct VCheckIgnoreRule {
|
||||
base_dir string
|
||||
pattern string
|
||||
}
|
||||
|
||||
struct VCheckIgnoreContext {
|
||||
repo_root string
|
||||
}
|
||||
|
||||
struct VCheckIgnoreMatch {
|
||||
ignore_file string
|
||||
pattern string
|
||||
}
|
||||
|
||||
struct MDPathScanResult {
|
||||
files []string
|
||||
skipped int
|
||||
}
|
||||
|
||||
fn (v1 CheckResult) + (v2 CheckResult) CheckResult {
|
||||
return CheckResult{
|
||||
files: v1.files + v2.files
|
||||
lines: v1.lines + v2.lines
|
||||
examples: v1.examples + v2.examples
|
||||
oks: v1.oks + v2.oks
|
||||
warnings: v1.warnings + v2.warnings
|
||||
ferrors: v1.ferrors + v2.ferrors
|
||||
errors: v1.errors + v2.errors
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
unbuffer_stdout()
|
||||
if non_option_args.len == 0 || '-help' in os.args {
|
||||
help.print_and_exit('check-md')
|
||||
}
|
||||
if '-all' in os.args {
|
||||
println('´-all´ flag is deprecated. Please use ´v check-md .´ instead.')
|
||||
exit(1)
|
||||
}
|
||||
mut skip_line_length_check := '-skip-line-length-check' in os.args
|
||||
mut files_paths := non_option_args.clone()
|
||||
mut res := CheckResult{}
|
||||
if term_colors {
|
||||
os.setenv('VCOLORS', 'always', true)
|
||||
}
|
||||
os.mkdir_all(vcheckfolder, mode: 0o700) or {} // keep directory private
|
||||
defer {
|
||||
os.rmdir_all(vcheckfolder) or {}
|
||||
}
|
||||
mut all_mdfiles := []MDFile{}
|
||||
mut skipped_mdfiles := 0
|
||||
for i := 0; i < files_paths.len; i++ {
|
||||
file_path := files_paths[i]
|
||||
if os.is_dir(file_path) {
|
||||
scan_result := md_file_paths(file_path)
|
||||
files_paths << scan_result.files
|
||||
skipped_mdfiles += scan_result.skipped
|
||||
continue
|
||||
}
|
||||
real_path := os.real_path(file_path)
|
||||
lines := os.read_lines(real_path) or {
|
||||
println('"${file_path}" does not exist')
|
||||
res.warnings++
|
||||
continue
|
||||
}
|
||||
all_mdfiles << MDFile{
|
||||
skip_line_length_check: skip_line_length_check
|
||||
path: file_path
|
||||
lines: lines
|
||||
}
|
||||
}
|
||||
println('> Found: ${all_mdfiles.len} .md files. Skipped by .vcheckignore: ${skipped_mdfiles}.')
|
||||
if is_verbose {
|
||||
for idx, mdfile in all_mdfiles {
|
||||
println('> file ${idx + 1} is ${mdfile.path}')
|
||||
}
|
||||
}
|
||||
if show_progress {
|
||||
// this is intended to be replaced by the progress lines
|
||||
println('')
|
||||
}
|
||||
for idx, mut mdfile in all_mdfiles {
|
||||
mdfile.idx = idx
|
||||
mdfile.nfiles = all_mdfiles.len
|
||||
res += mdfile.check()
|
||||
}
|
||||
if res.errors == 0 && show_progress {
|
||||
clear_previous_line()
|
||||
}
|
||||
println('Checked .md files: ${res.files} | Ex.: ${res.examples} | Lines: ${res.lines} | OKs: ${res.oks} | Warnings: ${res.warnings} | Errors: ${res.errors} | Fmt errors: ${res.ferrors}')
|
||||
if res.ferrors > 0 && !should_autofix {
|
||||
println('Note: you can use `VAUTOFIX=1 v check-md file.md`, or `v check-md -fix file.md`,')
|
||||
println(' to fix the V formatting errors in the markdown code blocks, when possible.')
|
||||
println(' Run the command 2 times, to verify that all formatting errors were fixed.')
|
||||
println('Note: `v help check-md` shows a list of ```v fence keywords (for partial code).')
|
||||
}
|
||||
if res.errors > 0 {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn md_file_paths(dir string) MDPathScanResult {
|
||||
mut files_to_check := []string{}
|
||||
mut skipped := 0
|
||||
vcheckignore := collect_vcheckignore_context(dir)
|
||||
md_files := os.walk_ext(dir, '.md')
|
||||
for file in md_files {
|
||||
nfile := file.replace('\\', '/')
|
||||
if nfile.contains_any_substr(['/thirdparty/', 'CHANGELOG', '/testdata/']) {
|
||||
continue
|
||||
}
|
||||
if skip_match := vcheckignore.skip_match(file) {
|
||||
if is_verbose {
|
||||
println('SKIP: ${vcheckignore.repo_relative_path(file)} (from ${vcheckignore.repo_relative_path(skip_match.ignore_file)}: ${skip_match.pattern})')
|
||||
}
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
files_to_check << file
|
||||
}
|
||||
return MDPathScanResult{
|
||||
files: files_to_check
|
||||
skipped: skipped
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_vcheckignore_context(cwd string) VCheckIgnoreContext {
|
||||
repo_root := find_repo_root(cwd)
|
||||
return VCheckIgnoreContext{
|
||||
repo_root: repo_root
|
||||
}
|
||||
}
|
||||
|
||||
fn find_repo_root(cwd string) string {
|
||||
mut dir := os.real_path(cwd)
|
||||
for {
|
||||
if os.exists(os.join_path(dir, '.git')) {
|
||||
return dir
|
||||
}
|
||||
parent := os.dir(dir)
|
||||
if parent == dir || parent == '' {
|
||||
return dir
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
fn (ctx VCheckIgnoreContext) skip_match(file_path string) ?VCheckIgnoreMatch {
|
||||
file := os.real_path(file_path).replace('\\', '/')
|
||||
mut dir := os.dir(file)
|
||||
repo_root := ctx.repo_root.replace('\\', '/')
|
||||
for {
|
||||
ignore_path := os.join_path(dir, '.vcheckignore')
|
||||
if os.is_file(ignore_path) {
|
||||
lines := os.read_lines(ignore_path) or { []string{} }
|
||||
for line in lines {
|
||||
pattern := normalize_vcheckignore_line(line)
|
||||
if pattern == '' || pattern.starts_with('#') {
|
||||
continue
|
||||
}
|
||||
if matches_vcheckignore_rule(file, VCheckIgnoreRule{
|
||||
base_dir: dir
|
||||
pattern: pattern
|
||||
})
|
||||
{
|
||||
return VCheckIgnoreMatch{
|
||||
ignore_file: ignore_path
|
||||
pattern: pattern
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if dir.replace('\\', '/') == repo_root {
|
||||
break
|
||||
}
|
||||
parent := os.dir(dir)
|
||||
if parent == dir || parent == '' {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
fn normalize_vcheckignore_line(line string) string {
|
||||
trimmed := line.trim_space()
|
||||
if trimmed == '' {
|
||||
return ''
|
||||
}
|
||||
if comment_idx := trimmed.index('#') {
|
||||
return trimmed[..comment_idx].trim_space()
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
fn (ctx VCheckIgnoreContext) repo_relative_path(file_path string) string {
|
||||
file := os.real_path(file_path).replace('\\', '/')
|
||||
root := ctx.repo_root.replace('\\', '/')
|
||||
root_prefix := root + '/'
|
||||
if file.starts_with(root_prefix) {
|
||||
return file.all_after(root_prefix)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fn matches_vcheckignore_rule(file string, rule VCheckIgnoreRule) bool {
|
||||
base := rule.base_dir.replace('\\', '/')
|
||||
base_prefix := base + '/'
|
||||
if !file.starts_with(base_prefix) {
|
||||
return false
|
||||
}
|
||||
relative_file := file.all_after(base_prefix)
|
||||
mut pattern := rule.pattern.replace('\\', '/')
|
||||
if pattern.starts_with('!') {
|
||||
return false
|
||||
}
|
||||
mut anchored := false
|
||||
if pattern.starts_with('/') {
|
||||
anchored = true
|
||||
pattern = pattern.trim_left('/')
|
||||
}
|
||||
if pattern.ends_with('/') {
|
||||
pattern = pattern.trim_right('/')
|
||||
return matches_vcheckignore_directory_pattern(relative_file, pattern, anchored)
|
||||
}
|
||||
if anchored {
|
||||
return relative_file.match_glob(pattern)
|
||||
}
|
||||
if pattern.contains('/') {
|
||||
return relative_file.match_glob(pattern)
|
||||
}
|
||||
return os.file_name(relative_file).match_glob(pattern)
|
||||
}
|
||||
|
||||
fn matches_vcheckignore_directory_pattern(relative_file string, pattern string, anchored bool) bool {
|
||||
mut relative_dir := os.dir(relative_file).replace('\\', '/')
|
||||
if relative_dir == '.' || relative_dir == '' {
|
||||
return false
|
||||
}
|
||||
if anchored {
|
||||
return relative_dir.match_glob(pattern) || relative_dir.match_glob(pattern + '/*')
|
||||
}
|
||||
mut candidate := relative_dir
|
||||
for {
|
||||
if candidate.match_glob(pattern) || candidate.match_glob(pattern + '/*') {
|
||||
return true
|
||||
}
|
||||
if slash_idx := candidate.index('/') {
|
||||
candidate = candidate[slash_idx + 1..]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn wprintln(s string) {
|
||||
if !hide_warnings {
|
||||
println(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn ftext(s string, cb fn (string) string) string {
|
||||
if term_colors {
|
||||
return cb(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
fn btext(s string) string {
|
||||
return ftext(s, term.bold)
|
||||
}
|
||||
|
||||
fn mtext(s string) string {
|
||||
return ftext(s, term.magenta)
|
||||
}
|
||||
|
||||
fn rtext(s string) string {
|
||||
return ftext(s, term.red)
|
||||
}
|
||||
|
||||
fn wline(file_path string, lnumber int, column int, message string) string {
|
||||
return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(mtext(' warn:')) +
|
||||
rtext(' ${message}')
|
||||
}
|
||||
|
||||
fn eline(file_path string, lnumber int, column int, message string) string {
|
||||
return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(rtext(' error: ${message}'))
|
||||
}
|
||||
|
||||
const default_command = 'compile'
|
||||
|
||||
struct VCodeExample {
|
||||
mut:
|
||||
text []string
|
||||
command string
|
||||
sline int
|
||||
eline int
|
||||
}
|
||||
|
||||
enum MDFileParserState {
|
||||
markdown
|
||||
vexample
|
||||
codeblock
|
||||
}
|
||||
|
||||
struct MDFile {
|
||||
path string
|
||||
skip_line_length_check bool
|
||||
mut:
|
||||
idx int
|
||||
nfiles int
|
||||
lines []string
|
||||
examples []VCodeExample
|
||||
current VCodeExample
|
||||
state MDFileParserState = .markdown
|
||||
|
||||
oks int
|
||||
warnings int
|
||||
errors int // compilation errors + formatting errors
|
||||
ferrors int // purely formatting errors
|
||||
}
|
||||
|
||||
fn (mut f MDFile) progress(message string) {
|
||||
if show_progress {
|
||||
clear_previous_line()
|
||||
println('${message} | File ${f.idx + 1:3}/${f.nfiles:-3}: ${f.path}')
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckResultContext {
|
||||
path string
|
||||
line_number int
|
||||
line string
|
||||
}
|
||||
|
||||
fn (mut f MDFile) wcheck(actual int, limit int, ctx CheckResultContext, msg_template string) {
|
||||
if actual > limit {
|
||||
final := msg_template.replace('@', limit.str()) + ', actual: ' + actual.str()
|
||||
wprintln(wline(ctx.path, ctx.line_number, ctx.line.len, final))
|
||||
wprintln(ctx.line)
|
||||
wprintln(ftext('-'.repeat(limit) + '^', term.gray))
|
||||
f.warnings++
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut f MDFile) echeck(actual int, limit int, ctx CheckResultContext, msg_template string) {
|
||||
if actual > limit {
|
||||
final := msg_template.replace('@', limit.str()) + ', actual: ' + actual.str()
|
||||
eprintln(eline(ctx.path, ctx.line_number, ctx.line.len, final))
|
||||
eprintln(ctx.line)
|
||||
eprintln(ftext('-'.repeat(limit) + '^', term.gray))
|
||||
f.errors++
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut f MDFile) check() CheckResult {
|
||||
mut anchor_data := AnchorData{}
|
||||
for j, line in f.lines {
|
||||
// f.progress('line: ${j}')
|
||||
if !f.skip_line_length_check {
|
||||
ctx := CheckResultContext{f.path, j, line}
|
||||
if f.state == .vexample {
|
||||
f.wcheck(line.len, too_long_line_length_example, ctx,
|
||||
'example lines must be less than @ characters')
|
||||
} else if f.state == .codeblock {
|
||||
f.wcheck(line.len, too_long_line_length_codeblock, ctx,
|
||||
'code lines must be less than @ characters')
|
||||
} else if line.starts_with('|') {
|
||||
f.wcheck(line.len, too_long_line_length_table, ctx,
|
||||
'table lines must be less than @ characters')
|
||||
} else if line.contains('http') {
|
||||
// vfmt off
|
||||
f.wcheck(line.all_after('https').len, too_long_line_length_link, ctx, 'link lines must be less than @ characters')
|
||||
// vfmt on
|
||||
} else {
|
||||
f.echeck(line.len, too_long_line_length_other, ctx,
|
||||
'must be less than @ characters')
|
||||
}
|
||||
}
|
||||
if f.state == .markdown {
|
||||
anchor_data.add_links(j, line)
|
||||
anchor_data.add_link_targets(j, line)
|
||||
}
|
||||
|
||||
f.parse_line(j, line)
|
||||
}
|
||||
f.check_link_target_match(anchor_data)
|
||||
f.check_examples()
|
||||
return CheckResult{
|
||||
files: 1
|
||||
lines: f.lines.len
|
||||
examples: f.examples.len
|
||||
oks: f.oks
|
||||
warnings: f.warnings
|
||||
errors: f.errors
|
||||
ferrors: f.ferrors
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut f MDFile) parse_line(lnumber int, line string) {
|
||||
if line.starts_with('```v') {
|
||||
if f.state == .markdown {
|
||||
f.state = .vexample
|
||||
mut command := line.replace('```v', '').trim_space()
|
||||
if command == '' {
|
||||
command = default_command
|
||||
} else if command == 'nofmt' {
|
||||
command += ' ${default_command}'
|
||||
}
|
||||
f.current = VCodeExample{
|
||||
sline: lnumber
|
||||
command: command
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if line.starts_with('```') {
|
||||
match f.state {
|
||||
.vexample {
|
||||
f.state = .markdown
|
||||
f.current.eline = lnumber
|
||||
f.examples << f.current
|
||||
f.current = VCodeExample{}
|
||||
return
|
||||
}
|
||||
.codeblock {
|
||||
f.state = .markdown
|
||||
return
|
||||
}
|
||||
.markdown {
|
||||
f.state = .codeblock
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.state == .vexample {
|
||||
f.current.text << line
|
||||
}
|
||||
}
|
||||
|
||||
struct Headline {
|
||||
line int
|
||||
label string
|
||||
level int
|
||||
}
|
||||
|
||||
struct Anchor {
|
||||
line int
|
||||
}
|
||||
|
||||
type AnchorTarget = Anchor | Headline
|
||||
|
||||
struct AnchorLink {
|
||||
line int
|
||||
label string
|
||||
}
|
||||
|
||||
struct AnchorData {
|
||||
mut:
|
||||
links map[string][]AnchorLink
|
||||
anchors map[string][]AnchorTarget
|
||||
}
|
||||
|
||||
fn (mut ad AnchorData) add_links(line_number int, line string) {
|
||||
query := r'\[(?P<label>[^\]]+)\]\(\s*#(?P<link>[a-z0-9\-\_\x7f-\uffff]+)\)'
|
||||
mut re := regex.regex_opt(query) or { panic(err) }
|
||||
res := re.find_all_str(line)
|
||||
|
||||
for elem in res {
|
||||
re.match_string(elem)
|
||||
link := re.get_group_by_name(elem, 'link')
|
||||
if link !in ad.links {
|
||||
ad.links[link] = []AnchorLink{}
|
||||
}
|
||||
ad.links[link] << AnchorLink{
|
||||
line: line_number
|
||||
label: re.get_group_by_name(elem, 'label')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut ad AnchorData) add_link_targets(line_number int, line string) {
|
||||
if line.trim_space().starts_with('#') {
|
||||
if headline_start_pos := line.index(' ') {
|
||||
headline := line.substr(headline_start_pos + 1, line.len)
|
||||
link := create_ref_link(headline)
|
||||
if link !in ad.anchors {
|
||||
ad.anchors[link] = []AnchorTarget{}
|
||||
}
|
||||
ad.anchors[link] << Headline{
|
||||
line: line_number
|
||||
label: headline
|
||||
level: headline_start_pos
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := '<a\\s*id=["\'](?P<link>[a-z0-9\\-\\_\\x7f-\\uffff]+)["\']\\s*/>'
|
||||
mut re := regex.regex_opt(query) or { panic(err) }
|
||||
res := re.find_all_str(line)
|
||||
|
||||
for elem in res {
|
||||
re.match_string(elem)
|
||||
link := re.get_group_by_name(elem, 'link')
|
||||
if link !in ad.anchors {
|
||||
ad.anchors[link] = []AnchorTarget{}
|
||||
}
|
||||
ad.anchors[link] << Anchor{
|
||||
line: line_number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut f MDFile) check_link_target_match(ad AnchorData) {
|
||||
mut checked_headlines := []string{}
|
||||
mut found_error_warning := false
|
||||
for link, linkdata in ad.links {
|
||||
if link in ad.anchors {
|
||||
checked_headlines << link
|
||||
if ad.anchors[link].len > 1 {
|
||||
found_error_warning = true
|
||||
f.errors++
|
||||
for anchordata in ad.anchors[link] {
|
||||
eprintln(eline(f.path, anchordata.line, 0,
|
||||
'multiple link targets of existing link (#${link})'))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
found_error_warning = true
|
||||
f.errors++
|
||||
for brokenlink in linkdata {
|
||||
eprintln(eline(f.path, brokenlink.line, 0,
|
||||
'no link target found for existing link [${brokenlink.label}](#${link})'))
|
||||
}
|
||||
}
|
||||
}
|
||||
for link, anchor_lists in ad.anchors {
|
||||
if link !in checked_headlines {
|
||||
if anchor_lists.len > 1 {
|
||||
for anchor in anchor_lists {
|
||||
line := match anchor {
|
||||
Headline {
|
||||
anchor.line
|
||||
}
|
||||
Anchor {
|
||||
anchor.line
|
||||
}
|
||||
}
|
||||
|
||||
wprintln(wline(f.path, line, 0,
|
||||
'multiple link target for non existing link (#${link})'))
|
||||
found_error_warning = true
|
||||
f.warnings++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if found_error_warning {
|
||||
eprintln('') // fix suppressed last error output
|
||||
}
|
||||
}
|
||||
|
||||
// based on a reference sample md doc
|
||||
// https://github.com/aheissenberger/vlang-markdown-module/blob/master/test.md
|
||||
fn create_ref_link(s string) string {
|
||||
mut result := ''
|
||||
for c in s.trim_space() {
|
||||
result += match c {
|
||||
`a`...`z`, `0`...`9` {
|
||||
c.ascii_str()
|
||||
}
|
||||
`A`...`Z` {
|
||||
c.ascii_str().to_lower()
|
||||
}
|
||||
` `, `-` {
|
||||
'-'
|
||||
}
|
||||
`_` {
|
||||
'_'
|
||||
}
|
||||
else {
|
||||
if c > 127 { c.ascii_str() } else { '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn (mut f MDFile) debug() {
|
||||
for e in f.examples {
|
||||
eprintln('f.path: ${f.path} | example: ${e}')
|
||||
}
|
||||
}
|
||||
|
||||
fn cmdexecute(cmd string) int {
|
||||
verbose_println(cmd)
|
||||
res := os.execute(cmd)
|
||||
if res.exit_code < 0 {
|
||||
return 1
|
||||
}
|
||||
if res.exit_code != 0 {
|
||||
eprint(res.output)
|
||||
}
|
||||
return res.exit_code
|
||||
}
|
||||
|
||||
fn silent_cmdexecute(cmd string) int {
|
||||
verbose_println(cmd)
|
||||
res := os.execute(cmd)
|
||||
return res.exit_code
|
||||
}
|
||||
|
||||
fn get_fmt_exit_code(vfile string, vexe string) int {
|
||||
return silent_cmdexecute('${os.quoted_path(vexe)} fmt -verify ${os.quoted_path(vfile)}')
|
||||
}
|
||||
|
||||
fn (mut f MDFile) check_examples() {
|
||||
recheck_all_examples: for eidx, e in f.examples {
|
||||
if e.command == 'ignore' {
|
||||
continue
|
||||
}
|
||||
if e.command == 'wip' {
|
||||
continue
|
||||
}
|
||||
fname := os.base(f.path).replace('.md', '_md')
|
||||
uid := rand.ulid()
|
||||
cfile := os.join_path(vcheckfolder, '${uid}.c')
|
||||
vfile := os.join_path(vcheckfolder,
|
||||
'check_${fname}_example_${e.sline}__${e.eline}__${uid}.v')
|
||||
efile := os.join_path(vcheckfolder,
|
||||
'check_${fname}_example_${e.sline}__${e.eline}__${uid}.exe')
|
||||
mut should_cleanup_vfile := true
|
||||
// eprintln('>>> checking example ${vfile} ...')
|
||||
vcontent := e.text.join('\n') + '\n'
|
||||
os.write_file(vfile, vcontent) or { panic(err) }
|
||||
mut acommands := e.command.split(' ')
|
||||
nofmt := 'nofmt' in acommands
|
||||
for command in acommands {
|
||||
f.progress('OK: ${f.oks:3}, W: ${f.warnings:2}, E: ${f.errors:2}, F: ${f.ferrors:2}, ex. ${
|
||||
eidx + 1:3}/${f.examples.len:-3}, from line ${e.sline:4} to line ${e.eline:-4} of ${f.lines.len:-4}, command: ${command:12s}')
|
||||
fmt_res := if nofmt { 0 } else { get_fmt_exit_code(vfile, vexe) }
|
||||
f.ferrors += fmt_res
|
||||
match command {
|
||||
'compile' {
|
||||
res :=
|
||||
cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(efile)} ${os.quoted_path(vfile)}')
|
||||
if res != 0 || fmt_res != 0 {
|
||||
if res != 0 {
|
||||
eprintln(eline(f.path, e.sline, 0, 'example failed to compile'))
|
||||
}
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'cgen' {
|
||||
res :=
|
||||
cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
|
||||
if res != 0 || fmt_res != 0 {
|
||||
if res != 0 {
|
||||
eprintln(eline(f.path, e.sline, 0, 'example failed to generate C code'))
|
||||
}
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'globals' {
|
||||
res :=
|
||||
cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -enable-globals -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
|
||||
if res != 0 || fmt_res != 0 {
|
||||
if res != 0 {
|
||||
eprintln(eline(f.path, e.sline, 0,
|
||||
'`example failed to compile with -enable-globals'))
|
||||
}
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'live' {
|
||||
res :=
|
||||
cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -live -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
|
||||
if res != 0 || fmt_res != 0 {
|
||||
if res != 0 {
|
||||
eprintln(eline(f.path, e.sline, 0,
|
||||
'example failed to compile with -live'))
|
||||
}
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'shared' {
|
||||
res :=
|
||||
cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -shared -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
|
||||
if res != 0 || fmt_res != 0 {
|
||||
if res != 0 {
|
||||
eprintln(eline(f.path, e.sline, 0,
|
||||
'module example failed to compile with -shared'))
|
||||
}
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'failcompile' {
|
||||
res :=
|
||||
silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
|
||||
if res == 0 || fmt_res != 0 {
|
||||
if res == 0 {
|
||||
eprintln(eline(f.path, e.sline, 0, '`failcompile` example compiled'))
|
||||
}
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'oksyntax' {
|
||||
res :=
|
||||
cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}')
|
||||
if res != 0 || fmt_res != 0 {
|
||||
if res != 0 {
|
||||
eprintln(eline(f.path, e.sline, 0,
|
||||
'`oksyntax` example with invalid syntax'))
|
||||
}
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'okfmt' {
|
||||
if fmt_res != 0 {
|
||||
f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
|
||||
unsafe {
|
||||
goto recheck_all_examples
|
||||
}
|
||||
}
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'badsyntax' {
|
||||
res :=
|
||||
silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}')
|
||||
if res == 0 {
|
||||
eprintln(eline(f.path, e.sline, 0, '`badsyntax` example can be parsed fine'))
|
||||
eprintln(vcontent)
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
continue
|
||||
}
|
||||
f.oks++
|
||||
}
|
||||
'nofmt' {}
|
||||
// mark the example as playable inside docs
|
||||
'play' {}
|
||||
// same as play, but run example as a test
|
||||
'play-test' {}
|
||||
// when ```vmod
|
||||
'mod' {}
|
||||
else {
|
||||
eprintln(eline(f.path, e.sline, 0,
|
||||
'unrecognized command: "${command}", use one of: wip/ignore/compile/failcompile/okfmt/nofmt/oksyntax/badsyntax/cgen/globals/live/shared'))
|
||||
should_cleanup_vfile = false
|
||||
f.errors++
|
||||
}
|
||||
}
|
||||
}
|
||||
os.rm(cfile) or {}
|
||||
os.rm(efile) or {}
|
||||
if should_cleanup_vfile {
|
||||
os.rm(vfile) or { panic(err) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verbose_println(message string) {
|
||||
if is_verbose {
|
||||
println(message)
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_previous_line() {
|
||||
if is_verbose {
|
||||
return
|
||||
}
|
||||
term.clear_previous_line()
|
||||
}
|
||||
|
||||
fn (mut f MDFile) report_not_formatted_example_if_needed(e VCodeExample, fmt_res int, vfile string) ! {
|
||||
if fmt_res == 0 {
|
||||
return
|
||||
}
|
||||
eprintln(eline(f.path, e.sline, 0, 'example is not formatted'))
|
||||
if !should_autofix {
|
||||
return
|
||||
}
|
||||
f.autofix_example(e, vfile) or {
|
||||
if err is ExampleWasRewritten {
|
||||
eprintln('>> f.path: ${f.path} | example from ${e.sline} to ${e.eline} was re-formatted by vfmt')
|
||||
return err
|
||||
}
|
||||
eprintln('>> f.path: ${f.path} | encountered error while autofixing the example: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
struct ExampleWasRewritten {
|
||||
Error
|
||||
}
|
||||
|
||||
fn (mut f MDFile) autofix_example(e VCodeExample, vfile string) ! {
|
||||
eprintln('>>> AUTOFIXING f.path: ${f.path} | e.sline: ${e.sline} | vfile: ${vfile}')
|
||||
res := cmdexecute('${os.quoted_path(vexe)} fmt -w ${os.quoted_path(vfile)}')
|
||||
if res != 0 {
|
||||
return error('could not autoformat the example')
|
||||
}
|
||||
formatted_content_lines := os.read_lines(vfile) or { return }
|
||||
mut new_lines := []string{}
|
||||
new_lines << f.lines#[0..e.sline + 1]
|
||||
new_lines << formatted_content_lines
|
||||
new_lines << f.lines#[e.eline..]
|
||||
f.update_examples(new_lines)!
|
||||
os.rm(vfile) or {}
|
||||
f.examples = f.examples.filter(it.sline >= e.sline)
|
||||
return ExampleWasRewritten{}
|
||||
}
|
||||
|
||||
fn (mut f MDFile) update_examples(new_lines []string) ! {
|
||||
os.write_file(f.path, new_lines.join('\n'))!
|
||||
f.lines = new_lines
|
||||
f.examples = []
|
||||
f.current = VCodeExample{}
|
||||
f.state = .markdown
|
||||
for j, line in f.lines {
|
||||
f.parse_line(j, line)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
import os
|
||||
|
||||
const vexe = @VEXE
|
||||
const git_exe = os.find_abs_path_of_executable('git') or { '' }
|
||||
|
||||
fn test_check_md_respects_vcheckignore() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
mut repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_test_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'README.md'), '# Root\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'cwd_skip.md'), '# CWD skip\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'skip.md'), '# Skip me\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'skip2.md'), '# Skip me too\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'notes', 'keep2.md'), '# Keep 2\n')!
|
||||
|
||||
write_text_file(os.join_path(repo_dir, '.vcheckignore'), 'docs/skip.md\ndocs/sub/*.md\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), 'cwd_skip.md\n')!
|
||||
|
||||
check_cmd := '${os.quoted_path(vexe)} check-md -hide-warnings -silent .'
|
||||
|
||||
root_res := run_in_dir(repo_dir, check_cmd, true)!
|
||||
assert root_res.exit_code == 0, root_res.output
|
||||
assert root_res.output.contains('SKIP: docs/cwd_skip.md'), root_res.output
|
||||
assert root_res.output.contains('SKIP: docs/skip.md'), root_res.output
|
||||
assert root_res.output.contains('SKIP: docs/sub/skip2.md'), root_res.output
|
||||
assert root_res.output.contains('from docs/.vcheckignore: cwd_skip.md'), root_res.output
|
||||
assert root_res.output.contains('from .vcheckignore: docs/skip.md'), root_res.output
|
||||
assert root_res.output.contains('from .vcheckignore: docs/sub/*.md'), root_res.output
|
||||
assert root_res.output.contains('> Found: 2 .md files.'), root_res.output
|
||||
assert root_res.output.contains('Skipped by .vcheckignore: 3.'), root_res.output
|
||||
assert root_res.output.contains('Checked .md files: 2 |'), root_res.output
|
||||
root_non_verbose_res := run_in_dir(repo_dir, check_cmd, false)!
|
||||
assert root_non_verbose_res.exit_code == 0, root_non_verbose_res.output
|
||||
assert !root_non_verbose_res.output.contains('SKIP:'), root_non_verbose_res.output
|
||||
|
||||
docs_res := run_in_dir(os.join_path(repo_dir, 'docs'), check_cmd, true)!
|
||||
assert docs_res.exit_code == 0, docs_res.output
|
||||
assert docs_res.output.contains('SKIP: docs/cwd_skip.md'), docs_res.output
|
||||
assert docs_res.output.contains('SKIP: docs/skip.md'), docs_res.output
|
||||
assert docs_res.output.contains('SKIP: docs/sub/skip2.md'), docs_res.output
|
||||
assert docs_res.output.contains('from docs/.vcheckignore: cwd_skip.md'), docs_res.output
|
||||
assert docs_res.output.contains('> Found: 0 .md files.'), docs_res.output
|
||||
assert docs_res.output.contains('Skipped by .vcheckignore: 3.'), docs_res.output
|
||||
assert docs_res.output.contains('Checked .md files: 0 |'), docs_res.output
|
||||
|
||||
sub_res := run_in_dir(os.join_path(repo_dir, 'docs', 'sub'), check_cmd, true)!
|
||||
assert sub_res.exit_code == 0, sub_res.output
|
||||
assert sub_res.output.contains('SKIP: docs/sub/skip2.md'), sub_res.output
|
||||
assert !sub_res.output.contains('SKIP: docs/cwd_skip.md'), sub_res.output
|
||||
assert sub_res.output.contains('from .vcheckignore: docs/sub/*.md'), sub_res.output
|
||||
assert sub_res.output.contains('> Found: 0 .md files.'), sub_res.output
|
||||
assert sub_res.output.contains('Skipped by .vcheckignore: 1.'), sub_res.output
|
||||
assert sub_res.output.contains('Checked .md files: 0 |'), sub_res.output
|
||||
}
|
||||
|
||||
fn test_check_md_respects_vcheckignore_glob_in_scanned_dir() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_glob_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'ignored1.md'), '# ignored\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'ignored2.md'), '# ignored\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'ignored3.md'), '# ignored\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'keep1.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'keep2.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'keep3.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'doc', 'plans', '.vcheckignore'), 'ignored*.md\n')!
|
||||
|
||||
res := run_in_dir(repo_dir,
|
||||
'${os.quoted_path(vexe)} check-md -hide-warnings -silent doc/plans', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.contains('SKIP: doc/plans/ignored1.md'), res.output
|
||||
assert res.output.contains('SKIP: doc/plans/ignored2.md'), res.output
|
||||
assert res.output.contains('SKIP: doc/plans/ignored3.md'), res.output
|
||||
assert res.output.contains('from doc/plans/.vcheckignore: ignored*.md'), res.output
|
||||
assert res.output.contains('> Found: 3 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 3.'), res.output
|
||||
assert res.output.contains('Checked .md files: 3 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_respects_vcheckignore_anchored_pattern() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_anchored_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'root_only.md'), '# ignored by /root_only.md\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'root_only.md'), '# should be kept\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), '/root_only.md\n')!
|
||||
|
||||
res :=
|
||||
run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.contains('SKIP: docs/root_only.md'), res.output
|
||||
assert res.output.contains('from docs/.vcheckignore: /root_only.md'), res.output
|
||||
assert !res.output.contains('SKIP: docs/sub/root_only.md'), res.output
|
||||
assert res.output.contains('> Found: 2 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 1.'), res.output
|
||||
assert res.output.contains('Checked .md files: 2 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_respects_vcheckignore_anchored_directory_pattern() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_anchored_dir_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'ignored.md'), '# ignored by /sub/\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'nested', 'sub', 'keep.md'), '# kept\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), '/sub/\n')!
|
||||
|
||||
res :=
|
||||
run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.contains('SKIP: docs/sub/ignored.md'), res.output
|
||||
assert res.output.contains('from docs/.vcheckignore: /sub/'), res.output
|
||||
assert !res.output.contains('SKIP: docs/nested/sub/keep.md'), res.output
|
||||
assert res.output.contains('> Found: 2 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 1.'), res.output
|
||||
assert res.output.contains('Checked .md files: 2 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_respects_vcheckignore_non_anchored_directory_pattern() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_non_anchored_dir_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'ignored1.md'), '# ignored 1\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'nested', 'sub', 'ignored2.md'), '# ignored 2\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), 'sub/\n')!
|
||||
|
||||
res :=
|
||||
run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.contains('SKIP: docs/sub/ignored1.md'), res.output
|
||||
assert res.output.contains('SKIP: docs/nested/sub/ignored2.md'), res.output
|
||||
assert res.output.contains('from docs/.vcheckignore: sub/'), res.output
|
||||
assert res.output.contains('> Found: 1 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output
|
||||
assert res.output.contains('Checked .md files: 1 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_respects_vcheckignore_anchored_directory_glob_pattern() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_anchored_dir_glob_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'docs-a', 'sub', 'ignored1.md'), '# ignored 1\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs-b', 'sub', 'ignored2.md'), '# ignored 2\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'other', 'keep.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, '.vcheckignore'), '/docs-*/\n')!
|
||||
|
||||
res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent .', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.contains('SKIP: docs-a/sub/ignored1.md'), res.output
|
||||
assert res.output.contains('SKIP: docs-b/sub/ignored2.md'), res.output
|
||||
assert res.output.contains('from .vcheckignore: /docs-*/'), res.output
|
||||
assert !res.output.contains('SKIP: other/keep.md'), res.output
|
||||
assert res.output.contains('> Found: 1 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output
|
||||
assert res.output.contains('Checked .md files: 1 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_respects_vcheckignore_comments_and_blank_lines() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_comments_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'ignored.md'), '# ignored\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'ignored2.md'), '# ignored2\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'),
|
||||
'# comment\n\nignored.md # inline comment\nignored2.md\n')!
|
||||
|
||||
res :=
|
||||
run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.contains('SKIP: docs/ignored.md'), res.output
|
||||
assert res.output.contains('SKIP: docs/ignored2.md'), res.output
|
||||
assert res.output.contains('from docs/.vcheckignore: ignored.md'), res.output
|
||||
assert res.output.contains('from docs/.vcheckignore: ignored2.md'), res.output
|
||||
assert !res.output.contains('SKIP: docs/keep.md'), res.output
|
||||
assert res.output.contains('> Found: 1 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output
|
||||
assert res.output.contains('Checked .md files: 1 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_file_argument_does_not_use_vcheckignore_directory_filtering() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_file_argument_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'ignored.md'), '# ignored by dir scan\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), 'ignored.md\n')!
|
||||
|
||||
res := run_in_dir(repo_dir,
|
||||
'${os.quoted_path(vexe)} check-md -hide-warnings -silent docs/ignored.md', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert !res.output.contains('SKIP: docs/ignored.md'), res.output
|
||||
assert res.output.contains('> Found: 1 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 0.'), res.output
|
||||
assert res.output.contains('Checked .md files: 1 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_uses_scanned_dir_repo_root_for_vcheckignore() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
base_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_repo_root_${os.getpid()}')
|
||||
repo_a := os.join_path(base_dir, 'repo_a')
|
||||
repo_b := os.join_path(base_dir, 'repo_b')
|
||||
os.rmdir_all(base_dir) or {}
|
||||
os.mkdir_all(os.join_path(repo_b, 'docs'))!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(base_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_a)}')
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_b)}')
|
||||
|
||||
write_text_file(os.join_path(base_dir, '.vcheckignore'), 'outside*.md\n')!
|
||||
write_text_file(os.join_path(repo_b, 'docs', 'outside1.md'),
|
||||
'# outside but should not be skipped\n')!
|
||||
write_text_file(os.join_path(repo_b, 'docs', 'keep1.md'), '# keep\n')!
|
||||
|
||||
res := run_in_dir(repo_a, '${os.quoted_path(vexe)} check-md -hide-warnings -silent ${os.quoted_path(os.join_path(repo_b,
|
||||
'docs'))}', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert !res.output.contains('SKIP: '), res.output
|
||||
assert res.output.contains('> Found: 2 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 0.'), res.output
|
||||
assert res.output.contains('Checked .md files: 2 |'), res.output
|
||||
}
|
||||
|
||||
fn test_check_md_multiple_directories_accumulate_skipped_count() {
|
||||
if git_exe == '' {
|
||||
eprintln('git is required for this test; skipping')
|
||||
return
|
||||
}
|
||||
original_wd := os.getwd()
|
||||
repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_multi_dirs_${os.getpid()}')
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
os.mkdir_all(repo_dir)!
|
||||
defer {
|
||||
os.chdir(original_wd) or {}
|
||||
os.rmdir_all(repo_dir) or {}
|
||||
}
|
||||
os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}')
|
||||
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'ignored.md'), '# ignored\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'notes', 'ignored.md'), '# ignored\n')!
|
||||
write_text_file(os.join_path(repo_dir, 'notes', 'keep.md'), '# keep\n')!
|
||||
write_text_file(os.join_path(repo_dir, '.vcheckignore'), 'docs/ignored.md\nnotes/ignored.md\n')!
|
||||
|
||||
res := run_in_dir(repo_dir,
|
||||
'${os.quoted_path(vexe)} check-md -hide-warnings -silent docs notes', true)!
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.contains('SKIP: docs/ignored.md'), res.output
|
||||
assert res.output.contains('SKIP: notes/ignored.md'), res.output
|
||||
assert res.output.contains('> Found: 2 .md files.'), res.output
|
||||
assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output
|
||||
assert res.output.contains('Checked .md files: 2 |'), res.output
|
||||
}
|
||||
|
||||
fn run_in_dir(path string, cmd string, verbose bool) !os.Result {
|
||||
original_wd := os.getwd()
|
||||
os.chdir(path)!
|
||||
if verbose {
|
||||
os.setenv('VERBOSE', '1', true)
|
||||
} else {
|
||||
os.unsetenv('VERBOSE')
|
||||
}
|
||||
res := os.execute(cmd)
|
||||
os.unsetenv('VERBOSE')
|
||||
os.chdir(original_wd)!
|
||||
return res
|
||||
}
|
||||
|
||||
fn write_text_file(path string, content string) ! {
|
||||
os.mkdir_all(os.dir(path))!
|
||||
os.write_file(path, content)!
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
//
|
||||
// Utility functions helping integrate with various shell auto-completion systems.
|
||||
// The install process and communication is inspired from that of [kitty](https://sw.kovidgoyal.net/kitty/#completion-for-kitty)
|
||||
// This method avoids writing and maintaining external files on the user's file system.
|
||||
// The user will be responsible for adding a small line to their .*rc - that will ensure *live* (i.e. not-static)
|
||||
// auto-completion features.
|
||||
//
|
||||
// # bash
|
||||
// To install auto-completion for V in bash, simply add this code to your `~/.bashrc`:
|
||||
// `source /dev/stdin <<<"$(v complete setup bash)"`
|
||||
// On more recent versions of bash (>3.2) this should suffice:
|
||||
// `source <(v complete setup bash)`
|
||||
//
|
||||
// # fish
|
||||
// For versions of fish <3.0.0, add the following to your `~/.config/fish/config.fish`
|
||||
// `v complete setup fish | source`
|
||||
// Later versions of fish source completions by default.
|
||||
//
|
||||
// # zsh
|
||||
// To install auto-completion for V in zsh - please add the following to your `~/.zshrc`:
|
||||
// ```
|
||||
// autoload -Uz compinit
|
||||
// compinit
|
||||
// # Completion for v
|
||||
// v complete setup zsh | source /dev/stdin
|
||||
// ```
|
||||
// Please note that you should let v load the zsh completions after the call to compinit
|
||||
//
|
||||
// # powershell
|
||||
// To install auto-complete for V in PowerShell, simply do this
|
||||
// `v complete setup powershell >> $PROFILE`
|
||||
// and reload profile
|
||||
// `& $PROFILE`
|
||||
// If `$PROFILE` didn't exist yet, create it before
|
||||
// `New-Item -Type File -Force $PROFILE`
|
||||
//
|
||||
module main
|
||||
|
||||
import os
|
||||
|
||||
const auto_complete_shells = ['bash', 'fish', 'zsh', 'powershell'] // list of supported shells
|
||||
|
||||
const vexe = os.getenv('VEXE')
|
||||
const help_text = "Usage:
|
||||
v complete [options] [SUBCMD] QUERY...
|
||||
|
||||
Description:
|
||||
Tool for bridging auto completion between various shells and v
|
||||
|
||||
Supported shells:
|
||||
bash, fish, zsh, powershell
|
||||
|
||||
Examples:
|
||||
Echo auto-detected shell install script to STDOUT
|
||||
v complete
|
||||
Echo specific shell install script to STDOUT
|
||||
v complete setup bash
|
||||
Auto complete input `v tes`*USER PUSHES TAB* (in Bash compatible format).
|
||||
This is not meant for manual invocation - it's called by the relevant
|
||||
shell via the script installed with `v complete` or `v complete setup SHELL`.
|
||||
v complete bash v tes
|
||||
|
||||
Options:
|
||||
-h, --help Show this help text.
|
||||
|
||||
SUBCMD:
|
||||
setup : setup [SHELL] - returns the code for completion setup for SHELL
|
||||
bash : [QUERY] - returns Bash compatible completion code with completions computed from QUERY
|
||||
fish : [QUERY] - returns Fish compatible completion code with completions computed from QUERY
|
||||
zsh : [QUERY] - returns ZSH compatible completion code with completions computed from QUERY
|
||||
powershell: [QUERY] - returns PowerShell compatible completion code with completions computed from QUERY"
|
||||
|
||||
// Snooped from cmd/v/v.v, vlib/v/pref/pref.c.v
|
||||
const auto_complete_commands = [
|
||||
// simple_cmd
|
||||
'ast',
|
||||
'doc',
|
||||
'vet',
|
||||
// tools in one .v file or folder (typically has a "v" prefix)
|
||||
'bin2v',
|
||||
'bug',
|
||||
'build-examples',
|
||||
'build-tools',
|
||||
'build-vbinaries',
|
||||
'bump',
|
||||
'check-md',
|
||||
'complete',
|
||||
'compress',
|
||||
'cover',
|
||||
'create',
|
||||
'doctor',
|
||||
'download',
|
||||
'fmt',
|
||||
'gret',
|
||||
'git-fmt-hook',
|
||||
'ls',
|
||||
'quest',
|
||||
'retry',
|
||||
'reduce',
|
||||
'repl',
|
||||
'repeat',
|
||||
'self',
|
||||
'setup-freetype',
|
||||
'scan',
|
||||
'shader',
|
||||
'symlink',
|
||||
'test-all',
|
||||
'test-cleancode',
|
||||
'test-fmt',
|
||||
'test-parser',
|
||||
'test-self',
|
||||
'test',
|
||||
'tracev',
|
||||
'up',
|
||||
'watch',
|
||||
'where',
|
||||
'wipe-cache',
|
||||
// commands
|
||||
'help',
|
||||
'new',
|
||||
'init',
|
||||
'translate',
|
||||
'self',
|
||||
'search',
|
||||
'install',
|
||||
'link',
|
||||
'update',
|
||||
'upgrade',
|
||||
'outdated',
|
||||
'list',
|
||||
'remove',
|
||||
'unlink',
|
||||
'vlib-docs',
|
||||
'get',
|
||||
'version',
|
||||
'run',
|
||||
'build',
|
||||
'build-module',
|
||||
'missdoc',
|
||||
]
|
||||
// Entries in the flag arrays below should be entered as is:
|
||||
// * Short flags, e.g.: "-v", should be entered: '-v'
|
||||
// * Long flags, e.g.: "--version", should be entered: '--version'
|
||||
// * Single-dash flags, e.g.: "-version", should be entered: '-version'
|
||||
const auto_complete_flags = [
|
||||
'-wasm-validate',
|
||||
'-wasm-stack-top',
|
||||
'-apk',
|
||||
'-arch',
|
||||
'-assert',
|
||||
'-show-timings',
|
||||
'-show-asserts',
|
||||
'-check-syntax',
|
||||
'-check',
|
||||
'-?',
|
||||
'-h',
|
||||
'-help',
|
||||
'--help',
|
||||
'-q',
|
||||
'-v',
|
||||
'-V',
|
||||
'--version',
|
||||
'-version',
|
||||
'-progress',
|
||||
'-Wimpure-v',
|
||||
'-Wfatal-errors',
|
||||
'-silent',
|
||||
'-skip-running',
|
||||
'-cstrict',
|
||||
'-nofloat',
|
||||
'-fast-math',
|
||||
'-e',
|
||||
'-subsystem',
|
||||
'-gc',
|
||||
'-g',
|
||||
'-cg',
|
||||
'-debug-tcc',
|
||||
'-sourcemap',
|
||||
'-warn-about-allocs',
|
||||
'-sourcemap-src-included',
|
||||
'-sourcemap-inline',
|
||||
'-repl',
|
||||
'-live',
|
||||
'-sharedlive',
|
||||
'-shared',
|
||||
'--enable-globals',
|
||||
'-enable-globals',
|
||||
'-autofree',
|
||||
'-print_autofree_vars',
|
||||
'-print_autofree_vars_in_fn',
|
||||
'-trace-calls',
|
||||
'-trace-fns',
|
||||
'-manualfree',
|
||||
'-skip-unused',
|
||||
'-no-skip-unused',
|
||||
'-compress',
|
||||
'-freestanding',
|
||||
'-no-retry-compilation',
|
||||
'-musl',
|
||||
'-glibc',
|
||||
'-no-bounds-checking',
|
||||
'-no-builtin',
|
||||
'-no-preludes',
|
||||
'-no-relaxed-gcc14',
|
||||
'-prof',
|
||||
'-profile',
|
||||
'-cov',
|
||||
'-coverage',
|
||||
'-profile-fns',
|
||||
'-profile-no-inline',
|
||||
'-prod',
|
||||
'-sanitize',
|
||||
'-simulator',
|
||||
'-stats',
|
||||
'-obf',
|
||||
'-obfuscate',
|
||||
'-hide-auto-str',
|
||||
'-translated',
|
||||
'-translated-go',
|
||||
'-m32',
|
||||
'-m64',
|
||||
'-color',
|
||||
'-nocolor',
|
||||
'-showcc',
|
||||
'-show-c-output',
|
||||
'-show-callgraph',
|
||||
'-show-depgraph',
|
||||
'-run-only',
|
||||
'-exclude',
|
||||
'-test-runner',
|
||||
'-dump-c-flags',
|
||||
'-dump-modules',
|
||||
'-dump-files',
|
||||
'-dump-defines',
|
||||
'-experimental',
|
||||
'-usecache',
|
||||
'-use-os-system-to-run',
|
||||
'-macosx-version-min',
|
||||
'-nocache',
|
||||
'-prealloc',
|
||||
'-no-parallel',
|
||||
'-parallel-cc',
|
||||
'-native',
|
||||
'-W',
|
||||
'-w',
|
||||
'-N',
|
||||
'-n',
|
||||
'-no-rsp',
|
||||
'-no-std',
|
||||
'-keepc',
|
||||
'-watch',
|
||||
'-print-v-files',
|
||||
'-print-watched-files',
|
||||
'-http',
|
||||
'-cross',
|
||||
'-os',
|
||||
'-printfn',
|
||||
'-cflags',
|
||||
'-ldflags',
|
||||
'-d',
|
||||
'-define',
|
||||
'-message-limit',
|
||||
'-thread-stack-size',
|
||||
'-cc',
|
||||
'-c++',
|
||||
'-checker-match-exhaustive-cutoff-limit',
|
||||
'-o',
|
||||
'-output',
|
||||
'-is_o',
|
||||
'-b',
|
||||
'-backend',
|
||||
'-es5',
|
||||
'-path',
|
||||
'-bare-builtin-dir',
|
||||
'-custom-prelude',
|
||||
'-raw-vsh-tmp-prefix',
|
||||
'-cmain',
|
||||
'-line-info',
|
||||
'-check-unused-fn-args',
|
||||
'-check-return',
|
||||
'-use-coroutines',
|
||||
]
|
||||
const auto_complete_flags_cover = [
|
||||
'--help',
|
||||
'-h',
|
||||
'--verbose',
|
||||
'-v',
|
||||
'--hotspots',
|
||||
'-H',
|
||||
'--percentages',
|
||||
'-P',
|
||||
'--lcov',
|
||||
'--show_test_files',
|
||||
'-S',
|
||||
'--absolute',
|
||||
'-A',
|
||||
'--filter',
|
||||
'-f',
|
||||
]
|
||||
const auto_complete_flags_doc = [
|
||||
'-all',
|
||||
'-f',
|
||||
'-h',
|
||||
'-help',
|
||||
'-m',
|
||||
'-o',
|
||||
'-readme',
|
||||
'-v',
|
||||
'-filename',
|
||||
'-pos',
|
||||
'-no-timestamp',
|
||||
'-inline-assets',
|
||||
'-theme-dir',
|
||||
'-open',
|
||||
'-p',
|
||||
'-s',
|
||||
'-l',
|
||||
]
|
||||
const auto_complete_flags_download = [
|
||||
'--help',
|
||||
'-h',
|
||||
'--target-folder',
|
||||
'-t',
|
||||
'--sha1',
|
||||
'-1',
|
||||
'--sha256',
|
||||
'-2',
|
||||
'--continue',
|
||||
'-c',
|
||||
'--retries',
|
||||
'-r',
|
||||
'--delay',
|
||||
'-d',
|
||||
]
|
||||
const auto_complete_flags_fmt = [
|
||||
'-c',
|
||||
'-diff',
|
||||
'-l',
|
||||
'-w',
|
||||
'-debug',
|
||||
'-verify',
|
||||
]
|
||||
const auto_complete_flags_bin2v = [
|
||||
'-h',
|
||||
'--help',
|
||||
'-m',
|
||||
'--module',
|
||||
'-p',
|
||||
'--prefix',
|
||||
'-w',
|
||||
'--write',
|
||||
]
|
||||
const auto_complete_flags_retry = [
|
||||
'--help',
|
||||
'-h',
|
||||
'--timeout',
|
||||
'-t',
|
||||
'--delay',
|
||||
'-d',
|
||||
'--retries',
|
||||
'-r',
|
||||
]
|
||||
const auto_complete_flags_shader = [
|
||||
'--help',
|
||||
'-h',
|
||||
'--force-update',
|
||||
'-u',
|
||||
'--verbose',
|
||||
'-v',
|
||||
'--slang',
|
||||
'-l',
|
||||
'--output',
|
||||
'-o',
|
||||
]
|
||||
const auto_complete_flags_missdoc = [
|
||||
'--help',
|
||||
'-h',
|
||||
'--tags',
|
||||
'-t',
|
||||
'--deprecated',
|
||||
'-d',
|
||||
'--private',
|
||||
'-p',
|
||||
'--no-line-numbers',
|
||||
'-n',
|
||||
'--exclude',
|
||||
'-e',
|
||||
'--relative-paths',
|
||||
'-r',
|
||||
'--js',
|
||||
'--verify',
|
||||
'--diff',
|
||||
]
|
||||
const auto_complete_flags_bump = [
|
||||
'--patch',
|
||||
'--minor',
|
||||
'--major',
|
||||
]
|
||||
const auto_complete_flags_self = [
|
||||
'-prod',
|
||||
]
|
||||
const auto_complete_flags_where = [
|
||||
'-h',
|
||||
'-f',
|
||||
'-v',
|
||||
]
|
||||
const auto_complete_flags_reduce = [
|
||||
'-e',
|
||||
'--error_msg',
|
||||
'-c',
|
||||
'--command',
|
||||
'-w',
|
||||
'--fmt',
|
||||
'--version',
|
||||
]
|
||||
const auto_complete_flags_repeat = [
|
||||
'--help',
|
||||
'-h',
|
||||
'--runs',
|
||||
'-r',
|
||||
'--series',
|
||||
'-s',
|
||||
'--warmup',
|
||||
'-w',
|
||||
'--newline',
|
||||
'-n',
|
||||
'--output',
|
||||
'-O',
|
||||
'--max_time',
|
||||
'-m',
|
||||
'--fail_percent',
|
||||
'-f',
|
||||
'--template',
|
||||
'-t',
|
||||
'--parameter',
|
||||
'-p',
|
||||
'--nmins',
|
||||
'-i',
|
||||
'--nmaxs',
|
||||
'-a',
|
||||
'--ignore',
|
||||
'-e',
|
||||
]
|
||||
const auto_complete_compilers = [
|
||||
'cc',
|
||||
'gcc',
|
||||
'tcc',
|
||||
'tinyc',
|
||||
'clang',
|
||||
'mingw',
|
||||
'msvc',
|
||||
]
|
||||
|
||||
// auto_complete prints auto completion results back to the calling shell's completion system.
|
||||
// auto_complete acts as communication bridge between the calling shell and V's completions.
|
||||
fn auto_complete(args []string) {
|
||||
if args.len <= 1 || args[0] != 'complete' {
|
||||
if args.len == 1 {
|
||||
shell_path := os.getenv('SHELL')
|
||||
if shell_path != '' {
|
||||
shell_name := os.file_name(shell_path).to_lower()
|
||||
if shell_name in auto_complete_shells {
|
||||
println(setup_for_shell(shell_name))
|
||||
exit(0)
|
||||
}
|
||||
eprintln('Unknown shell ${shell_name}. Supported shells are: ${auto_complete_shells}')
|
||||
exit(1)
|
||||
}
|
||||
eprintln('auto completion require arguments to work.')
|
||||
} else {
|
||||
eprintln('auto completion failed for "${args}".')
|
||||
}
|
||||
exit(1)
|
||||
}
|
||||
sub := args[1]
|
||||
sub_args := args[1..]
|
||||
match sub {
|
||||
'setup' {
|
||||
if sub_args.len <= 1 || sub_args[1] !in auto_complete_shells {
|
||||
eprintln('please specify a shell to setup auto completion for (${auto_complete_shells}).')
|
||||
exit(1)
|
||||
}
|
||||
shell := sub_args[1]
|
||||
println(setup_for_shell(shell))
|
||||
}
|
||||
'bash' {
|
||||
if sub_args.len <= 1 {
|
||||
exit(0)
|
||||
}
|
||||
mut lines := []string{}
|
||||
list := auto_complete_request(sub_args[1..])
|
||||
for entry in list {
|
||||
lines << "COMPREPLY+=('${entry}')"
|
||||
}
|
||||
println(lines.join('\n'))
|
||||
}
|
||||
'fish', 'powershell' {
|
||||
if sub_args.len <= 1 {
|
||||
exit(0)
|
||||
}
|
||||
mut lines := []string{}
|
||||
list := auto_complete_request(sub_args[1..])
|
||||
for entry in list {
|
||||
lines << '${entry}'
|
||||
}
|
||||
println(lines.join('\n'))
|
||||
}
|
||||
'zsh' {
|
||||
if sub_args.len <= 1 {
|
||||
exit(0)
|
||||
}
|
||||
mut lines := []string{}
|
||||
mut dirs := []string{}
|
||||
mut files := []string{}
|
||||
list := auto_complete_request(sub_args[1..])
|
||||
for entry in list {
|
||||
match true {
|
||||
os.is_dir(entry) { dirs << entry }
|
||||
os.is_file(entry) { files << entry }
|
||||
else { lines << entry }
|
||||
}
|
||||
}
|
||||
println('compadd -q -- ${lines.join(' ')}')
|
||||
println('compadd -J "dirs" -X "directory" -d -- ${dirs.join(' ')}')
|
||||
println('compadd -J "files" -X "file" -f -- ${files.join(' ')}')
|
||||
}
|
||||
'-h', '--help' {
|
||||
println(help_text)
|
||||
}
|
||||
else {}
|
||||
}
|
||||
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// append_separator_if_dir returns the input `path` with an appended
|
||||
// `/` or `\`, depending on the platform, when `path` is a directory.
|
||||
fn append_separator_if_dir(path string) string {
|
||||
if os.is_dir(path) && !path.ends_with(os.path_separator) {
|
||||
return path + os.path_separator
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// nearest_path_or_root returns the nearest valid path searching
|
||||
// backwards from `path`.
|
||||
fn nearest_path_or_root(path string) string {
|
||||
mut fixed_path := path
|
||||
if !os.is_dir(fixed_path) {
|
||||
fixed_path = path.all_before_last(os.path_separator)
|
||||
if fixed_path == '' {
|
||||
fixed_path = '/'
|
||||
}
|
||||
}
|
||||
return fixed_path
|
||||
}
|
||||
|
||||
// auto_complete_request returns a list of completions resolved from a full argument list.
|
||||
fn auto_complete_request(args []string) []string {
|
||||
// Using space will ensure a uniform input in cases where the shell
|
||||
// returns the completion input as a string (['v','run'] vs. ['v run']).
|
||||
split_by := ' '
|
||||
request := args.join(split_by)
|
||||
mut do_home_expand := false
|
||||
mut list := []string{}
|
||||
// new_part := request.ends_with('\n\n')
|
||||
mut parts := request.trim_right(' ').split(split_by)
|
||||
if parts.len <= 1 { // 'v <tab>' -> top level commands.
|
||||
for command in auto_complete_commands {
|
||||
list << command
|
||||
}
|
||||
} else {
|
||||
mut part := parts.last().trim(' ')
|
||||
mut parent_command := ''
|
||||
for i := parts.len - 1; i >= 0; i-- {
|
||||
if parts[i].starts_with('-') {
|
||||
continue
|
||||
}
|
||||
parent_command = parts[i]
|
||||
break
|
||||
}
|
||||
if part.starts_with('-') { // 'v [subcmd] -<tab>' or 'v [subcmd] --<tab>'-> flags.
|
||||
get_flags := fn (base []string, flag string) []string {
|
||||
mut results := []string{}
|
||||
for entry in base {
|
||||
if entry.starts_with(flag) {
|
||||
results << entry
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
match parent_command {
|
||||
'bin2v' { // 'v bin2v -<tab>'
|
||||
list = get_flags(auto_complete_flags_bin2v, part)
|
||||
}
|
||||
'bump' { // 'v bump -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_bump, part)
|
||||
}
|
||||
'build' { // 'v build -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags, part)
|
||||
}
|
||||
'cover' { // 'v cover -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_cover, part)
|
||||
}
|
||||
'doc' { // 'v doc -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_doc, part)
|
||||
}
|
||||
'download' { // 'v download -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_download, part)
|
||||
}
|
||||
'fmt' { // 'v fmt -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_fmt, part)
|
||||
}
|
||||
'missdoc' { // 'v missdoc -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_missdoc, part)
|
||||
}
|
||||
'reduce' { // 'v reduce -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_reduce, part)
|
||||
}
|
||||
'retry' { // 'v retry -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_retry, part)
|
||||
}
|
||||
'repeat' { // 'v repeat -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_repeat, part)
|
||||
}
|
||||
'self' { // 'v self -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_self, part)
|
||||
}
|
||||
'shader' { // 'v shader -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_shader, part)
|
||||
}
|
||||
'where' { // 'v where -<tab>' -> flags.
|
||||
list = get_flags(auto_complete_flags_where, part)
|
||||
}
|
||||
else {
|
||||
for flag in auto_complete_flags {
|
||||
if flag == part {
|
||||
if flag == '-cc' { // 'v -cc <tab>' -> list of available compilers.
|
||||
for compiler in auto_complete_compilers {
|
||||
path := os.find_abs_path_of_executable(compiler) or { '' }
|
||||
if path != '' {
|
||||
list << compiler
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if flag.starts_with(part) { // 'v -<char(s)><tab>' -> flags matching "<char(s)>".
|
||||
list << flag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the list if the result is identical to the part examined
|
||||
// (the flag must have already been completed)
|
||||
if list.len == 1 && part == list[0] {
|
||||
list.clear()
|
||||
}
|
||||
} else {
|
||||
match part {
|
||||
'help' { // 'v help <tab>' -> top level commands except "help".
|
||||
list = auto_complete_commands.filter(it != part && it != 'complete')
|
||||
}
|
||||
else {
|
||||
// 'v <char(s)><tab>' -> commands matching "<char(s)>".
|
||||
// Don't include if part matches a full command - instead go to path completion below.
|
||||
for command in auto_complete_commands {
|
||||
if part != command && command.starts_with(part) {
|
||||
list << command
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Nothing of value was found.
|
||||
// Mimic shell dir and file completion
|
||||
if list.len == 0 {
|
||||
mut ls_path := '.'
|
||||
mut collect_all := part in auto_complete_commands
|
||||
mut path_complete := false
|
||||
do_home_expand = part.starts_with('~')
|
||||
if do_home_expand {
|
||||
add_sep := if part == '~' { os.path_separator } else { '' }
|
||||
part = part.replace_once('~', os.home_dir().trim_right(os.path_separator)) + add_sep
|
||||
}
|
||||
is_abs_path :=
|
||||
part.starts_with(os.path_separator) // TODO: Windows support for drive prefixes
|
||||
if part.ends_with(os.path_separator) || part == '.' || part == '..' {
|
||||
// 'v <command>(.*/$|.|..)<tab>' -> output full directory list
|
||||
ls_path = '.' + os.path_separator + part
|
||||
if is_abs_path {
|
||||
ls_path = nearest_path_or_root(part)
|
||||
}
|
||||
collect_all = true
|
||||
} else if !collect_all && part.contains(os.path_separator) && os.is_dir(os.dir(part)) {
|
||||
// 'v <command>(.*/.* && os.is_dir)<tab>' -> output completion friendly directory list
|
||||
if is_abs_path {
|
||||
ls_path = nearest_path_or_root(part)
|
||||
} else {
|
||||
ls_path = os.dir(part)
|
||||
}
|
||||
path_complete = true
|
||||
}
|
||||
|
||||
entries := os.ls(ls_path) or { return list }
|
||||
mut last := part.all_after_last(os.path_separator)
|
||||
if is_abs_path && os.is_dir(part) {
|
||||
last = ''
|
||||
}
|
||||
if path_complete {
|
||||
path := part.all_before_last(os.path_separator)
|
||||
for entry in entries {
|
||||
if entry.starts_with(last) {
|
||||
list << append_separator_if_dir(os.join_path(path, entry))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle special case, where there is only one file in the directory
|
||||
// being completed - if it can be resolved we return that since
|
||||
// handling it in the generalized logic below will result in
|
||||
// more complexity.
|
||||
if entries.len == 1 && os.is_file(os.join_path(ls_path, entries[0])) {
|
||||
mut keep_input_path_format := ls_path
|
||||
if !part.starts_with('./') && ls_path.starts_with('./') {
|
||||
keep_input_path_format = keep_input_path_format.all_after('./')
|
||||
}
|
||||
return [os.join_path(keep_input_path_format, entries[0])]
|
||||
}
|
||||
for entry in entries {
|
||||
if collect_all || entry.starts_with(last) {
|
||||
list << append_separator_if_dir(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if do_home_expand {
|
||||
return list.map(it.replace_once(os.home_dir().trim_right(os.path_separator), '~'))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fn setup_for_shell(shell string) string {
|
||||
mut setup := ''
|
||||
match shell {
|
||||
'bash' {
|
||||
setup = '
|
||||
_v_completions() {
|
||||
local src
|
||||
local limit
|
||||
# Send all words up to the word the cursor is currently on
|
||||
let limit=1+\$COMP_CWORD
|
||||
src=\$(${vexe} complete bash \$(printf "%s\\n" \${COMP_WORDS[@]: 0:\$limit}))
|
||||
if [[ \$? == 0 ]]; then
|
||||
eval \${src}
|
||||
#echo \${src}
|
||||
fi
|
||||
}
|
||||
|
||||
complete -o nospace -F _v_completions v
|
||||
'
|
||||
}
|
||||
'fish' {
|
||||
setup = '
|
||||
function __v_completions
|
||||
# Send all words up to the one before the cursor
|
||||
${vexe} complete fish (commandline -cop)
|
||||
end
|
||||
complete -f -c v -a "(__v_completions)"
|
||||
'
|
||||
}
|
||||
'zsh' {
|
||||
setup = '
|
||||
#compdef v
|
||||
_v() {
|
||||
local src
|
||||
# Send all words up to the word the cursor is currently on
|
||||
src=\$(${vexe} complete zsh \$(printf "%s\\n" \${(@)words[1,\$CURRENT]}))
|
||||
if [[ \$? == 0 ]]; then
|
||||
eval \${src}
|
||||
#echo \${src}
|
||||
fi
|
||||
}
|
||||
compdef _v v
|
||||
'
|
||||
}
|
||||
'powershell' {
|
||||
setup = '
|
||||
Register-ArgumentCompleter -Native -CommandName v -ScriptBlock {
|
||||
param(\$commandName, \$wordToComplete, \$cursorPosition)
|
||||
${vexe} complete powershell "\$wordToComplete" | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(\$_, \$_, \'ParameterValue\', \$_)
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
else {}
|
||||
}
|
||||
|
||||
return setup
|
||||
}
|
||||
|
||||
fn main() {
|
||||
args := os.args[1..]
|
||||
// println('"${args}"')
|
||||
auto_complete(args)
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import os
|
||||
|
||||
const vexe = @VEXE
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'vcomplete_test')
|
||||
|
||||
enum Shell {
|
||||
bash
|
||||
fish
|
||||
zsh
|
||||
powershell
|
||||
}
|
||||
|
||||
fn string_to_shell(shell string) !Shell {
|
||||
return match shell {
|
||||
'bash' {
|
||||
.bash
|
||||
}
|
||||
'fish' {
|
||||
.fish
|
||||
}
|
||||
'zsh' {
|
||||
.zsh
|
||||
}
|
||||
'powershell' {
|
||||
.powershell
|
||||
}
|
||||
else {
|
||||
error('${shell} is not supported')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testsuite_begin() {
|
||||
os.mkdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn detect_shell() Shell {
|
||||
forced_shell := os.getenv('VTEST_VCOMPLETE_SHELL')
|
||||
if forced_shell != '' {
|
||||
return string_to_shell(forced_shell) or { panic(err) }
|
||||
}
|
||||
$if linux || macos {
|
||||
return .bash
|
||||
}
|
||||
$if windows {
|
||||
return .powershell
|
||||
}
|
||||
return .bash
|
||||
}
|
||||
|
||||
struct CompleteTestCase {
|
||||
shell Shell = detect_shell()
|
||||
structure []string
|
||||
completes map[string][]string
|
||||
}
|
||||
|
||||
const test_cases = [
|
||||
CompleteTestCase{
|
||||
// Tests the completion of "v complet<tab>" *completeception*
|
||||
structure: [] // no test folders
|
||||
completes: {
|
||||
'v complet': ['complete']
|
||||
}
|
||||
},
|
||||
CompleteTestCase{
|
||||
// Tests the completion of "v ru<tab>"
|
||||
structure: [] // no test folders
|
||||
completes: {
|
||||
'v ru': ['run']
|
||||
}
|
||||
},
|
||||
CompleteTestCase{
|
||||
// Tests with one sub-folder in test root
|
||||
// vfmt off
|
||||
structure: [
|
||||
os.join_path('examples', 'ex1'), // Empty dir
|
||||
os.join_path('examples', 'ex2'), // Two files
|
||||
os.join_path('examples', 'ex2', 'ex2_0.v'),
|
||||
os.join_path('examples', 'ex2', 'ex2_1.v'),
|
||||
os.join_path('examples', 'ex3'), // One file
|
||||
os.join_path('examples', 'ex3', 'ex3.v')
|
||||
]
|
||||
// vfmt on
|
||||
completes: {
|
||||
'v run e': ['examples/']
|
||||
'v run': ['examples/']
|
||||
'v run ': ['examples/']
|
||||
'v run examples/': ['ex1', 'ex2', 'ex3']
|
||||
'v run examples/ex': ['examples/ex1/', 'examples/ex2/', 'examples/ex3/']
|
||||
'v run examples/ex1': ['examples/ex1/']
|
||||
'v run examples/ex1/': []
|
||||
'v run examples/ex2/': ['ex2_0.v', 'ex2_1.v']
|
||||
'v run examples/ex3/': ['examples/ex3/ex3.v']
|
||||
'v run examples/ex3/ex3.v': ['examples/ex3/ex3.v']
|
||||
'v examp': ['examples/']
|
||||
'v examples/f': []
|
||||
'v examples/': ['ex1', 'ex2', 'ex3']
|
||||
'v examples/ex': ['examples/ex1/', 'examples/ex2/', 'examples/ex3/']
|
||||
'v examples/ex1': ['examples/ex1/']
|
||||
'v examples/ex1/': []
|
||||
'v examples/ex2/': ['ex2_0.v', 'ex2_1.v']
|
||||
'v examples/ex3/': ['examples/ex3/ex3.v']
|
||||
'v examples/ex3/ex3.v': ['examples/ex3/ex3.v']
|
||||
}
|
||||
},
|
||||
CompleteTestCase{
|
||||
// Tests with two sub-folder in test root
|
||||
// vfmt off
|
||||
structure: [
|
||||
os.join_path('sub0', 'ex1'), // Empty dir
|
||||
os.join_path('sub0', 'ex2'), // Two files
|
||||
os.join_path('sub0', 'ex2', 'ex2_0.v'),
|
||||
os.join_path('sub0', 'ex2', 'ex2_1.v'),
|
||||
os.join_path('sub0', 'ex3'), // One file
|
||||
os.join_path('sub0', 'ex3', 'ex3.v'),
|
||||
os.join_path('sub1', 'vex1'),
|
||||
os.join_path('sub1', 'vex2'),
|
||||
os.join_path('sub1', 'vex3'),
|
||||
os.join_path('sub1', 'vex3', 'vex3.v')
|
||||
]
|
||||
// vfmt on
|
||||
completes: {
|
||||
// sub0
|
||||
'v run ./': ['sub0/', 'sub1/']
|
||||
'v run ./s': ['./sub0/', './sub1/']
|
||||
'v run': ['sub0/', 'sub1/']
|
||||
'v run ': ['sub0/', 'sub1/']
|
||||
'v run sub0/': ['ex1', 'ex2', 'ex3']
|
||||
'v run sub0/ex': ['sub0/ex1/', 'sub0/ex2/', 'sub0/ex3/']
|
||||
'v run sub0/ex1': ['sub0/ex1/']
|
||||
'v run sub0/ex1/': []
|
||||
'v run sub0/ex2/': ['ex2_0.v', 'ex2_1.v']
|
||||
'v run sub0/ex3/': ['sub0/ex3/ex3.v']
|
||||
'v run sub0/ex3/ex3.v': ['sub0/ex3/ex3.v']
|
||||
'v su': ['sub0/', 'sub1/']
|
||||
'v sub': ['sub0/', 'sub1/']
|
||||
'v sub0/f': []
|
||||
'v sub0/': ['ex1', 'ex2', 'ex3']
|
||||
'v sub0/ex': ['sub0/ex1/', 'sub0/ex2/', 'sub0/ex3/']
|
||||
'v sub0/ex1': ['sub0/ex1/']
|
||||
'v sub0/ex1/': []
|
||||
'v sub0/ex2/': ['ex2_0.v', 'ex2_1.v']
|
||||
'v sub0/ex3/': ['sub0/ex3/ex3.v']
|
||||
'v sub0/ex3/ex3.v': ['sub0/ex3/ex3.v']
|
||||
// sub1
|
||||
'v run sub1/': ['vex1', 'vex2', 'vex3']
|
||||
'v sub1/vex1/': []
|
||||
'v sub1/vex2/': []
|
||||
'v sub1/vex3/': ['sub1/vex3/vex3.v']
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
fn run_individual_test(case CompleteTestCase) ! {
|
||||
// Clean plate
|
||||
os.rmdir_all(tfolder) or {}
|
||||
os.mkdir_all(tfolder) or {}
|
||||
// Work relative to the test dir
|
||||
os.chdir(tfolder) or { panic(err) }
|
||||
for relative_path in case.structure {
|
||||
path := os.join_path(tfolder, relative_path)
|
||||
rel_parts := relative_path.split(os.path_separator)
|
||||
if rel_parts.last().contains('.') {
|
||||
// Make the containing dir
|
||||
os.mkdir_all(os.dir(path)) or { panic(err) }
|
||||
// Create empty file
|
||||
mut f := os.create(path) or { panic(err) }
|
||||
f.close()
|
||||
} else {
|
||||
os.mkdir_all(path) or { panic(err) }
|
||||
}
|
||||
}
|
||||
for complete, expected in case.completes {
|
||||
mut pre_strip := ''
|
||||
mut post_strip := ''
|
||||
mut complete_command := '${vexe} complete'
|
||||
match case.shell {
|
||||
.bash {
|
||||
pre_strip = "COMPREPLY+=('"
|
||||
post_strip = "')"
|
||||
complete_command += ' bash'
|
||||
}
|
||||
.fish {
|
||||
complete_command += ' fish'
|
||||
}
|
||||
.zsh {
|
||||
pre_strip = 'compadd -U -S"" -- \''
|
||||
post_strip = "';"
|
||||
complete_command += ' zsh'
|
||||
}
|
||||
.powershell {
|
||||
complete_command += ' powershell'
|
||||
}
|
||||
}
|
||||
|
||||
mut normalized_complete := complete
|
||||
if case.shell == .powershell {
|
||||
normalized_complete = normalized_complete.replace('/', '\\')
|
||||
}
|
||||
complete_command += ' ${normalized_complete}'
|
||||
res := os.execute('${complete_command}')
|
||||
mut lines := res.output.split('\n')
|
||||
for mut line in lines {
|
||||
if case.shell == .powershell {
|
||||
line = line.replace('\r', '')
|
||||
line = line.all_after('.\\')
|
||||
}
|
||||
line = line.trim_string_left(pre_strip).trim_string_right(post_strip)
|
||||
}
|
||||
lines = lines.filter(it != '')
|
||||
lines.sort()
|
||||
mut sorted_expected := expected.clone()
|
||||
if case.shell == .powershell {
|
||||
sorted_expected = sorted_expected.map(fn (path string) string {
|
||||
mut normalized_path := path.replace('/', '\\')
|
||||
normalized_path = normalized_path.all_after('.\\')
|
||||
return normalized_path
|
||||
})
|
||||
}
|
||||
sorted_expected.sort()
|
||||
// eprintln('${complete} ${lines} vs ${sorted_expected}') // kept for easier debugging
|
||||
|
||||
assert lines.len == sorted_expected.len
|
||||
for i, line in lines {
|
||||
assert line == sorted_expected[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_all_complete_cases() {
|
||||
for case in test_cases {
|
||||
run_individual_test(case)!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
module main
|
||||
|
||||
import compress.zlib
|
||||
import os
|
||||
|
||||
enum CompressionType {
|
||||
zlib
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if os.args.len != 5 {
|
||||
eprintln('v compress <type> <in> <out>')
|
||||
eprintln('supported types: zlib')
|
||||
exit(1)
|
||||
}
|
||||
compression_type := match os.args[2] {
|
||||
'zlib' {
|
||||
CompressionType.zlib
|
||||
}
|
||||
else {
|
||||
eprintln('unsupported type: ${os.args[1]}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
path := os.args[3]
|
||||
content := os.read_bytes(path) or {
|
||||
eprintln('unable to read "${path}": ${err}')
|
||||
exit(1)
|
||||
}
|
||||
compressed := match compression_type {
|
||||
.zlib {
|
||||
zlib.compress(content) or {
|
||||
eprintln('compression error: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out_path := os.args[4]
|
||||
|
||||
os.write_file_array(out_path, compressed) or {
|
||||
eprintln('failed to write "${out_path}": ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// vtest retry: 2
|
||||
import os
|
||||
|
||||
const vexe = @VEXE
|
||||
const vroot = os.dir(vexe)
|
||||
const tfolder = os.join_path(os.vtmp_dir(), 'cover_test')
|
||||
|
||||
const t1 = np(os.join_path(tfolder, 't1'))
|
||||
const t2 = np(os.join_path(tfolder, 't2'))
|
||||
const t3 = np(os.join_path(tfolder, 't3'))
|
||||
|
||||
fn testsuite_begin() {
|
||||
os.setenv('VCOLORS', 'never', true)
|
||||
os.chdir(vroot)!
|
||||
os.rmdir_all(tfolder) or {}
|
||||
os.mkdir(tfolder) or {}
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(tfolder) or {}
|
||||
}
|
||||
|
||||
fn test_help() {
|
||||
res := execute('${os.quoted_path(vexe)} cover -h')
|
||||
assert res.exit_code == 0
|
||||
assert res.output.contains('Usage: v cover')
|
||||
assert res.output.contains('Description: Analyze & make reports')
|
||||
assert res.output.contains('Options:')
|
||||
assert res.output.contains('-h, --help Show this help text.')
|
||||
assert res.output.contains('-v, --verbose Be more verbose while processing the coverages.')
|
||||
assert res.output.contains('-H, --hotspots Show most frequently executed covered lines.')
|
||||
assert res.output.contains('-P, --percentages Show coverage percentage per file.')
|
||||
assert res.output.contains('--lcov <string> Write an LCOV line coverage report')
|
||||
assert res.output.contains('-S, --show_test_files Show `_test.v` files as well (normally filtered).')
|
||||
assert res.output.contains('-A, --absolute Use absolute paths for all files')
|
||||
}
|
||||
|
||||
fn np(path string) string {
|
||||
return path.replace('\\', '/')
|
||||
}
|
||||
|
||||
fn test_simple() {
|
||||
assert !os.exists(t1), t1
|
||||
assert !os.exists(t2), t2
|
||||
assert !os.exists(t3), t3
|
||||
|
||||
r1 :=
|
||||
execute('${os.quoted_path(vexe)} -no-skip-unused -coverage ${os.quoted_path(t1)} cmd/tools/vcover/testdata/simple/t1_test.v')
|
||||
assert r1.exit_code == 0, r1.str()
|
||||
assert r1.output.trim_space() == '10', r1.str()
|
||||
assert os.exists(t1), t1
|
||||
cmd := '${os.quoted_path(vexe)} cover ${os.quoted_path(t1)} --filter vcover/testdata/simple/'
|
||||
filter1 := execute(cmd)
|
||||
assert filter1.exit_code == 0, filter1.output
|
||||
assert filter1.output.contains('cmd/tools/vcover/testdata/simple/simple.v'), filter1.output
|
||||
assert filter1.output.trim_space().ends_with('| 4 | 9 | 44.44%'), filter1.output
|
||||
lcov_file := np(os.join_path(tfolder, 'coverage', 'simple.lcov'))
|
||||
os.rm(lcov_file) or {}
|
||||
lcov :=
|
||||
execute('${os.quoted_path(vexe)} cover ${os.quoted_path(t1)} --filter vcover/testdata/simple/ --lcov ${os.quoted_path(lcov_file)} -P false')
|
||||
assert lcov.exit_code == 0, lcov.str()
|
||||
assert os.exists(lcov_file), lcov_file
|
||||
lcov_content := os.read_file(lcov_file) or { panic(err) }
|
||||
assert lcov_content.contains('SF:${np(os.join_path(vroot,
|
||||
'cmd/tools/vcover/testdata/simple/simple.v'))}')
|
||||
assert lcov_content.contains('DA:4,1')
|
||||
assert lcov_content.contains('DA:12,0')
|
||||
assert lcov_content.contains('LF:9')
|
||||
assert lcov_content.contains('LH:4')
|
||||
assert lcov_content.contains('end_of_record')
|
||||
hfilter1 :=
|
||||
execute('${os.quoted_path(vexe)} cover ${os.quoted_path(t1)} --filter vcover/testdata/simple/ -H -P false')
|
||||
assert hfilter1.exit_code == 0, hfilter1.output
|
||||
assert !hfilter1.output.contains('%'), hfilter1.output
|
||||
houtput1 := hfilter1.output.trim_space().split_into_lines()
|
||||
zeros1 := houtput1.filter(it.starts_with('0 '))
|
||||
nzeros1 := houtput1.filter(!it.starts_with('0 '))
|
||||
assert zeros1.len > 0
|
||||
assert zeros1.any(it.contains('simple.v:12')), zeros1.str()
|
||||
assert zeros1.any(it.contains('simple.v:14')), zeros1.str()
|
||||
assert zeros1.any(it.contains('simple.v:17')), zeros1.str()
|
||||
assert zeros1.any(it.contains('simple.v:18')), zeros1.str()
|
||||
assert zeros1.any(it.contains('simple.v:19')), zeros1.str()
|
||||
assert nzeros1.len > 0
|
||||
assert nzeros1.any(it.contains('simple.v:4')), nzeros1.str()
|
||||
assert nzeros1.any(it.contains('simple.v:6')), nzeros1.str()
|
||||
assert nzeros1.any(it.contains('simple.v:8')), nzeros1.str()
|
||||
assert nzeros1.any(it.contains('simple.v:25')), nzeros1.str()
|
||||
|
||||
r2 :=
|
||||
execute('${os.quoted_path(vexe)} -no-skip-unused -coverage ${os.quoted_path(t2)} cmd/tools/vcover/testdata/simple/t2_test.v')
|
||||
assert r2.exit_code == 0, r2.str()
|
||||
assert r2.output.trim_space() == '24', r2.str()
|
||||
assert os.exists(t2), t2
|
||||
filter2 :=
|
||||
execute('${os.quoted_path(vexe)} cover ${os.quoted_path(t2)} --filter vcover/testdata/simple')
|
||||
assert filter2.exit_code == 0, filter2.output
|
||||
assert filter2.output.contains('cmd/tools/vcover/testdata/simple/simple.v')
|
||||
assert filter2.output.trim_space().ends_with('| 6 | 9 | 66.67%'), filter2.output
|
||||
hfilter2 :=
|
||||
execute('${os.quoted_path(vexe)} cover ${os.quoted_path(t2)} --filter testdata/simple -H -P false')
|
||||
assert hfilter2.exit_code == 0, hfilter2.output
|
||||
assert !hfilter2.output.contains('%'), hfilter2.output
|
||||
houtput2 := hfilter2.output.trim_space().split_into_lines()
|
||||
zeros2 := houtput2.filter(it.starts_with('0 '))
|
||||
nzeros2 := houtput2.filter(!it.starts_with('0 '))
|
||||
assert zeros2.len > 0
|
||||
assert zeros2.any(it.contains('simple.v:4')), zeros2.str()
|
||||
assert zeros2.any(it.contains('simple.v:6')), zeros2.str()
|
||||
assert zeros2.any(it.contains('simple.v:8')), zeros2.str()
|
||||
assert nzeros2.len > 0
|
||||
assert nzeros2.any(it.contains('simple.v:17')), nzeros2.str()
|
||||
assert nzeros2.any(it.contains('simple.v:18')), nzeros2.str()
|
||||
assert nzeros2.any(it.contains('simple.v:19')), nzeros2.str()
|
||||
assert nzeros2.any(it.contains('simple.v:25')), nzeros2.str()
|
||||
|
||||
// Run both tests. The coverage should be combined and == 100%
|
||||
r3 :=
|
||||
execute('${os.quoted_path(vexe)} -no-skip-unused -coverage ${os.quoted_path(t3)} test cmd/tools/vcover/testdata/simple/')
|
||||
assert r3.exit_code == 0, r3.str()
|
||||
assert r3.output.trim_space().contains('Summary for all V _test.v files: '), r3.str()
|
||||
assert os.exists(t3), t3
|
||||
filter3 := execute('${os.quoted_path(vexe)} cover ${os.quoted_path(t3)} --filter simple/')
|
||||
assert filter3.exit_code == 0, filter3.str()
|
||||
assert filter3.output.contains('cmd/tools/vcover/testdata/simple/simple.v'), filter3.str()
|
||||
assert filter3.output.trim_space().match_glob('*cmd/tools/vcover/testdata/simple/simple.v *| 9 | 9 | 100.00%'), filter3.str()
|
||||
}
|
||||
|
||||
fn execute(cmd string) os.Result {
|
||||
eprintln('Executing: ${cmd}')
|
||||
return os.execute(cmd)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2024 Felipe Pena and Delyan Angelov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module main
|
||||
|
||||
// vcounter_*.csv files contain counter lines in a CSV format. They can get quite large,
|
||||
// for big programs, since they contain all non zero coverage counters.
|
||||
// Their names are timestamp (rand.ulid + clock_gettime) based, to minimise the chance that parallel runs
|
||||
// will overwrite each other, but without the overhead of additional synchronisation/locks.
|
||||
struct CounterLine {
|
||||
mut:
|
||||
file string // retrieved based on the loaded meta
|
||||
line int // retrieved based on the loaded meta
|
||||
|
||||
meta string // A filename in the sibling meta/ folder, should exist, to match the value from this field. The filename is a hash of both the path and the used build options, to facilitate merging coverage data from different builds/programs
|
||||
point int // The index of a source point. Note that it is not a line number, but an index in the meta data file, keyed by the field `meta` above.
|
||||
hits u64 // How many times the coverage point was executed. Only counters that are != 0 are recorded.
|
||||
}
|
||||
|
||||
// Source metadata files in meta/*.txt, contain JSON encoded fields (mappings from v source files to point line numbers).
|
||||
// Their names are a result of a hashing function, applied over both the source file name, and the build options.
|
||||
// This has several benefits:
|
||||
// a) it makes sure, that the resulting path is normalised
|
||||
// b) the meta data is deduplicated between runs that use the same source files
|
||||
// c) coverage data from different runs can be merged by simply reusing the same -coverage folder,
|
||||
// or by copy/pasting all files from 1 run, to the folder of another.
|
||||
struct MetaData {
|
||||
file string // V source file path
|
||||
fhash string // fhash is the name of the meta file
|
||||
v_version string // the V version, used to generate the coverage meta data file
|
||||
build_options string // the build options for the program
|
||||
npoints int // the number of stored coverage points
|
||||
points []int // the line numbers corresponding to each point
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright (c) 2024 Felipe Pena and Delyan Angelov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module main
|
||||
|
||||
import os
|
||||
import log
|
||||
import flag
|
||||
import json
|
||||
import arrays
|
||||
import encoding.csv
|
||||
|
||||
// program options, storage etc
|
||||
struct Context {
|
||||
mut:
|
||||
show_help bool
|
||||
show_hotspots bool
|
||||
show_percentages bool
|
||||
show_test_files bool
|
||||
use_absolute_paths bool
|
||||
be_verbose bool
|
||||
lcov_output string
|
||||
filter string
|
||||
working_folder string
|
||||
|
||||
targets []string
|
||||
meta map[string]MetaData // aggregated meta data, read from all .json files
|
||||
all_lines_per_file map[string][]int // aggregated by load_meta
|
||||
|
||||
counters map[string]u64 // incremented by process_target, based on each .csv file
|
||||
lines_per_file map[string]map[int]int // incremented by process_target, based on each .csv file
|
||||
processed_points u64
|
||||
}
|
||||
|
||||
const metadata_extension = '.json'
|
||||
const vcounter_glob_pattern = 'vcounters_*.csv'
|
||||
|
||||
fn (mut ctx Context) load_meta(folder string) {
|
||||
for omfile in os.walk_ext(folder, metadata_extension) {
|
||||
mfile := omfile.replace('\\', '/')
|
||||
content := os.read_file(mfile) or { '' }
|
||||
meta := os.file_name(mfile.replace(metadata_extension, ''))
|
||||
data := json.decode(MetaData, content) or {
|
||||
log.error('${@METHOD} failed to load ${mfile}')
|
||||
continue
|
||||
}
|
||||
ctx.meta[meta] = data
|
||||
mut lines_per_file := ctx.all_lines_per_file[data.file]
|
||||
lines_per_file << data.points
|
||||
ctx.all_lines_per_file[data.file] = arrays.distinct(lines_per_file)
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut ctx Context) post_process_all_metas() {
|
||||
ctx.verbose('${@METHOD}')
|
||||
for _, m in ctx.meta {
|
||||
lines_per_file := ctx.all_lines_per_file[m.file]
|
||||
for line in lines_per_file {
|
||||
ctx.counters['${m.file}:${line}:'] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut ctx Context) post_process_all_targets() {
|
||||
ctx.verbose('${@METHOD}')
|
||||
ctx.verbose('ctx.processed_points: ${ctx.processed_points}')
|
||||
}
|
||||
|
||||
fn (ctx &Context) verbose(msg string) {
|
||||
if ctx.be_verbose {
|
||||
log.info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut ctx Context) process_target(tfile string) ! {
|
||||
ctx.verbose('${@METHOD} ${tfile}')
|
||||
mut reader := csv.new_reader_from_file(tfile)!
|
||||
header := reader.read()!
|
||||
if header != ['meta', 'point', 'hits'] {
|
||||
return error('invalid header in .csv file')
|
||||
}
|
||||
for {
|
||||
row := reader.read() or { break }
|
||||
mut cline := CounterLine{
|
||||
meta: row[0]
|
||||
point: row[1].int()
|
||||
hits: row[2].u64()
|
||||
}
|
||||
m := ctx.meta[cline.meta] or {
|
||||
ctx.verbose('> skipping invalid meta: ${cline.meta} in file: ${cline.file}, csvfile: ${tfile}')
|
||||
continue
|
||||
}
|
||||
cline.file = m.file
|
||||
cline.line = m.points[cline.point] or {
|
||||
ctx.verbose('> skipping invalid point: ${cline.point} in file: ${cline.file}, meta: ${cline.meta}, csvfile: ${tfile}')
|
||||
continue
|
||||
}
|
||||
ctx.counters['${cline.file}:${cline.line}:'] += cline.hits
|
||||
mut lines := ctx.lines_per_file[cline.file].move()
|
||||
lines[cline.line]++
|
||||
ctx.lines_per_file[cline.file] = lines.move()
|
||||
// dump( ctx.lines_per_file[cline.meta][cline.point] )
|
||||
ctx.processed_points++
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut ctx Context) show_report() ! {
|
||||
filters := ctx.filter.split(',').filter(it != '')
|
||||
if ctx.show_hotspots {
|
||||
mut locations := []string{cap: ctx.counters.len}
|
||||
for location, _ in ctx.counters {
|
||||
if !ctx.matches_filters(location, filters) {
|
||||
continue
|
||||
}
|
||||
locations << location
|
||||
}
|
||||
locations.sort()
|
||||
for location in locations {
|
||||
hits := ctx.counters[location]
|
||||
mut final_path := normalize_path(location)
|
||||
if !ctx.use_absolute_paths {
|
||||
final_path = location.all_after_first('${ctx.working_folder}/')
|
||||
}
|
||||
println('${hits:-8} ${final_path}')
|
||||
}
|
||||
}
|
||||
if ctx.show_percentages {
|
||||
for file in ctx.sorted_hit_files(filters) {
|
||||
total_lines := ctx.all_lines_per_file[file].len
|
||||
executed_points := ctx.lines_per_file[file].len
|
||||
coverage_percent := 100.0 * f64(executed_points) / f64(total_lines)
|
||||
mut final_path := normalize_path(file)
|
||||
if !ctx.use_absolute_paths {
|
||||
final_path = file.all_after_first('${ctx.working_folder}/')
|
||||
}
|
||||
println('${final_path:-80s} | ${executed_points:6} | ${total_lines:6} | ${coverage_percent:6.2f}%')
|
||||
}
|
||||
}
|
||||
if ctx.lcov_output != '' {
|
||||
ctx.write_lcov_report(filters)!
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_path(path string) string {
|
||||
return path.replace(os.path_separator, '/')
|
||||
}
|
||||
|
||||
fn (ctx &Context) matches_filters(path string, filters []string) bool {
|
||||
if filters.len == 0 {
|
||||
return true
|
||||
}
|
||||
return filters.any(path.contains(it))
|
||||
}
|
||||
|
||||
fn (ctx &Context) should_include_file(file string, filters []string) bool {
|
||||
if !ctx.show_test_files && (file.ends_with('_test.v') || file.ends_with('_test.c.v')) {
|
||||
return false
|
||||
}
|
||||
return ctx.matches_filters(file, filters)
|
||||
}
|
||||
|
||||
fn (ctx &Context) sorted_hit_files(filters []string) []string {
|
||||
mut files := []string{}
|
||||
for file, _ in ctx.lines_per_file {
|
||||
if !ctx.should_include_file(file, filters) {
|
||||
continue
|
||||
}
|
||||
files << file
|
||||
}
|
||||
files.sort()
|
||||
return files
|
||||
}
|
||||
|
||||
fn (ctx &Context) sorted_files(filters []string) []string {
|
||||
mut files := []string{}
|
||||
for file, _ in ctx.all_lines_per_file {
|
||||
if !ctx.should_include_file(file, filters) {
|
||||
continue
|
||||
}
|
||||
files << file
|
||||
}
|
||||
files.sort()
|
||||
return files
|
||||
}
|
||||
|
||||
fn (ctx &Context) write_lcov_report(filters []string) ! {
|
||||
output_path := os.real_path(ctx.lcov_output)
|
||||
output_dir := os.dir(output_path)
|
||||
if output_dir != '' && !os.exists(output_dir) {
|
||||
os.mkdir_all(output_dir)!
|
||||
}
|
||||
mut output := []string{}
|
||||
for file in ctx.sorted_files(filters) {
|
||||
mut lines := ctx.all_lines_per_file[file].clone()
|
||||
lines.sort()
|
||||
hit_lines := ctx.lines_per_file[file].len
|
||||
output << 'TN:'
|
||||
output << 'SF:${normalize_path(file)}'
|
||||
for line in lines {
|
||||
hits := ctx.counters['${file}:${line}:']
|
||||
output << 'DA:${line},${hits}'
|
||||
}
|
||||
output << 'LF:${lines.len}'
|
||||
output << 'LH:${hit_lines}'
|
||||
output << 'end_of_record'
|
||||
}
|
||||
os.write_file(output_path, output.join_lines())!
|
||||
ctx.verbose('Wrote LCOV report to ${output_path}')
|
||||
}
|
||||
|
||||
fn main() {
|
||||
log.use_stdout()
|
||||
mut ctx := Context{}
|
||||
ctx.working_folder = normalize_path(os.real_path(os.getwd()))
|
||||
mut fp := flag.new_flag_parser(os.args#[1..])
|
||||
fp.application('v cover')
|
||||
fp.version('0.0.2')
|
||||
fp.description('Analyze & make reports, based on cover files, produced by running programs and tests, compiled with `-coverage folder/`')
|
||||
fp.arguments_description('[folder1/ file2 ...]')
|
||||
fp.skip_executable()
|
||||
ctx.show_help = fp.bool('help', `h`, false, 'Show this help text.')
|
||||
ctx.be_verbose = fp.bool('verbose', `v`, false,
|
||||
'Be more verbose while processing the coverages.')
|
||||
ctx.show_hotspots = fp.bool('hotspots', `H`, false,
|
||||
'Show most frequently executed covered lines.')
|
||||
ctx.show_percentages = fp.bool('percentages', `P`, true, 'Show coverage percentage per file.')
|
||||
ctx.lcov_output = fp.string('lcov', 0, '',
|
||||
'Write an LCOV line coverage report to the specified file path.')
|
||||
ctx.show_test_files = fp.bool('show_test_files', `S`, false,
|
||||
'Show `_test.v` files as well (normally filtered).')
|
||||
ctx.use_absolute_paths = fp.bool('absolute', `A`, false,
|
||||
'Use absolute paths for all files, no matter the current folder. By default, files inside the current folder, are shown with a relative path.')
|
||||
ctx.filter = fp.string('filter', `f`, '', 'Filter only the matching source path patterns.')
|
||||
if ctx.show_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
targets := fp.finalize() or {
|
||||
log.error(fp.usage())
|
||||
exit(1)
|
||||
}
|
||||
ctx.verbose('Targets: ${targets}')
|
||||
for t in targets {
|
||||
if !os.exists(t) {
|
||||
log.error('Skipping ${t}, since it does not exist')
|
||||
continue
|
||||
}
|
||||
if os.is_dir(t) {
|
||||
found_counter_files := os.walk_ext(t, '.csv')
|
||||
if found_counter_files.len == 0 {
|
||||
log.error('Skipping ${t}, since there are 0 ${vcounter_glob_pattern} files in it')
|
||||
continue
|
||||
}
|
||||
for counterfile in found_counter_files {
|
||||
ctx.targets << counterfile
|
||||
ctx.load_meta(t)
|
||||
}
|
||||
} else {
|
||||
ctx.targets << t
|
||||
ctx.load_meta(os.dir(t))
|
||||
}
|
||||
}
|
||||
ctx.post_process_all_metas()
|
||||
ctx.verbose('Final ctx.targets.len: ${ctx.targets.len}')
|
||||
ctx.verbose('Final ctx.meta.len: ${ctx.meta.len}')
|
||||
ctx.verbose('Final ctx.filter: ${ctx.filter}')
|
||||
if ctx.targets.len == 0 {
|
||||
log.error('0 cover targets')
|
||||
exit(1)
|
||||
}
|
||||
for t in ctx.targets {
|
||||
ctx.process_target(t)!
|
||||
}
|
||||
ctx.post_process_all_targets()
|
||||
ctx.show_report()!
|
||||
}
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
module example1
|
||||
|
||||
fn abc01() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
fn abc02() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc03() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc04() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc05() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc06() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc07() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc08() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc09() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc10() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc11() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc12() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc13() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc14() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc15() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc16() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc17() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc18() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc19() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc20() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc21() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc22() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc23() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc24() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc25() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc26() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc27() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc28() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc29() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc30() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc31() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc32() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc33() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc34() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc35() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc36() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc37() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc38() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn abc39() int {
|
||||
if true {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module example1
|
||||
|
||||
fn test_abc() {
|
||||
abc01()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
module example1
|
||||
|
||||
fn test_abc() {
|
||||
abc10()
|
||||
abc11()
|
||||
abc12()
|
||||
abc13()
|
||||
abc14()
|
||||
abc15()
|
||||
abc16()
|
||||
abc17()
|
||||
abc18()
|
||||
abc19()
|
||||
abc20()
|
||||
abc21()
|
||||
abc22()
|
||||
abc23()
|
||||
abc24()
|
||||
abc25()
|
||||
abc26()
|
||||
abc27()
|
||||
abc28()
|
||||
abc29()
|
||||
abc30()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module example1
|
||||
|
||||
fn test_abc() {
|
||||
abc20()
|
||||
abc21()
|
||||
abc22()
|
||||
abc23()
|
||||
abc24()
|
||||
abc25()
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,30 @@
|
||||
module example2
|
||||
|
||||
pub fn condition() int {
|
||||
mut res := 0
|
||||
$if condition1 ? {
|
||||
res += 1
|
||||
}
|
||||
$if condition2 ? {
|
||||
res += 2
|
||||
}
|
||||
$if condition3 ? {
|
||||
res += 4
|
||||
}
|
||||
$if condition4 ? {
|
||||
res += 8
|
||||
}
|
||||
$if condition5 ? {
|
||||
res += 16
|
||||
}
|
||||
$if condition6 ? {
|
||||
res += 32
|
||||
}
|
||||
$if condition7 ? {
|
||||
res += 64
|
||||
}
|
||||
$if condition8 ? {
|
||||
return 128
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import example2
|
||||
|
||||
fn test_condition() {
|
||||
println(example2.condition())
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
module example2
|
||||
|
||||
import os
|
||||
|
||||
pub fn runtime_condition() int {
|
||||
mut res := 0
|
||||
branch := os.getenv('CONDITION')
|
||||
if branch == '' {
|
||||
return res
|
||||
}
|
||||
if branch.contains('1') {
|
||||
res += 1
|
||||
}
|
||||
if branch.contains('2') {
|
||||
res += 2
|
||||
}
|
||||
if branch.contains('3') {
|
||||
res += 4
|
||||
}
|
||||
if branch.contains('4') {
|
||||
res += 8
|
||||
}
|
||||
if branch.contains('5') {
|
||||
res += 16
|
||||
}
|
||||
if branch.contains('6') {
|
||||
res += 32
|
||||
}
|
||||
if branch.contains('7') {
|
||||
res += 64
|
||||
}
|
||||
if branch.contains('8') {
|
||||
res += 128
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import example2
|
||||
|
||||
fn test_runtime_condition() {
|
||||
println(example2.runtime_condition())
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,26 @@
|
||||
module simple
|
||||
|
||||
pub fn sum() int {
|
||||
mut res := 0
|
||||
for i in 1 .. 5 {
|
||||
res += i
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
pub fn mul() int {
|
||||
mut res := 1
|
||||
for i in 1 .. 5 {
|
||||
res *= i
|
||||
}
|
||||
// the lines here are just to introduce an asymmetry in the reported coverage lines
|
||||
c := res * 2
|
||||
_ := c * 10
|
||||
return res
|
||||
}
|
||||
|
||||
pub const a_const = f()
|
||||
|
||||
fn f() int {
|
||||
return 50 // this should be executed always
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import simple
|
||||
|
||||
fn test_sum() {
|
||||
s := simple.sum()
|
||||
assert s == 10
|
||||
println(s)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import simple
|
||||
|
||||
fn test_mul() {
|
||||
m := simple.mul()
|
||||
assert m == 24
|
||||
println(m)
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
|
||||
fn (mut c Create) set_bin_project_files() {
|
||||
base := if c.new_dir { c.name } else { '' }
|
||||
c.files << ProjectFiles{
|
||||
path: os.join_path(base, 'main.v')
|
||||
content: "module main
|
||||
|
||||
fn main() {
|
||||
println('Hello World!')
|
||||
}
|
||||
"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
|
||||
fn (mut c Create) set_lib_project_files() {
|
||||
base := if c.new_dir { c.name } else { '' }
|
||||
c.files << ProjectFiles{
|
||||
path: os.join_path(base, c.name + '.v')
|
||||
content: 'module ${c.name}
|
||||
|
||||
// square calculates the second power of `x`
|
||||
pub fn square(x int) int {
|
||||
return x * x
|
||||
}
|
||||
'
|
||||
}
|
||||
c.files << ProjectFiles{
|
||||
path: os.join_path(base, 'tests', 'square_test.v')
|
||||
content: 'import ${c.name}
|
||||
|
||||
fn test_square() {
|
||||
assert ${c.name}.square(2) == 4
|
||||
}
|
||||
'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
|
||||
fn (mut c Create) set_web_project_files() {
|
||||
base := if c.new_dir { c.name } else { '' }
|
||||
c.files << ProjectFiles{
|
||||
path: os.join_path(base, 'assets', 'main.css')
|
||||
content: "html, body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: #f4efe6;
|
||||
color: #1f2933;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
max-width: 560px;
|
||||
padding: 32px;
|
||||
border: 1px solid #d8ccb8;
|
||||
border-radius: 16px;
|
||||
background: #fffaf1;
|
||||
box-shadow: 0 18px 50px rgba(69, 52, 35, 0.08);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #8b5e34;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 40px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #52606d;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
"
|
||||
}
|
||||
c.files << ProjectFiles{
|
||||
path: os.join_path(base, 'templates', 'index.html')
|
||||
content: "<!doctype html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<title>@title</title>
|
||||
@css '/assets/main.css'
|
||||
</head>
|
||||
<body>
|
||||
<main class='card'>
|
||||
<p class='eyebrow'>veb starter</p>
|
||||
<h1>@title</h1>
|
||||
<p>@message</p>
|
||||
<p class='hint'>Try <code>GET /health</code> for a plain-text response.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
}
|
||||
c.files << ProjectFiles{
|
||||
path: os.join_path(base, 'main.v')
|
||||
content: "module main
|
||||
|
||||
import os
|
||||
import veb
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
veb.StaticHandler
|
||||
}
|
||||
|
||||
pub fn (app &App) index() veb.Result {
|
||||
title := '${c.name}'
|
||||
message := 'Your new V web app is powered by veb.'
|
||||
return \$veb.html()
|
||||
}
|
||||
|
||||
@['/health'; get]
|
||||
pub fn (app &App) health(mut ctx Context) veb.Result {
|
||||
return ctx.text('ok')
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Keep asset and template paths stable for `v run .`.
|
||||
os.chdir(os.dir(@FILE))!
|
||||
mut app := &App{}
|
||||
app.handle_static('assets', false)!
|
||||
veb.run[App, Context](mut app, 8080)
|
||||
}
|
||||
"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env expect
|
||||
|
||||
set timeout 10
|
||||
|
||||
# Pass v_root as arg, since we chdir into a temp directory during testing and create a project there.
|
||||
set v_root [lindex $argv 0]
|
||||
|
||||
spawn $v_root/v init
|
||||
|
||||
expect "Input your project description: " { send "\r" } timeout { exit 1 }
|
||||
expect "Input your project version: (0.0.0) " { send "\r" } timeout { exit 1 }
|
||||
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
|
||||
# The completion message is verified in `vcreate_init_test.v`.
|
||||
|
||||
expect eof
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env expect
|
||||
|
||||
set timeout 10
|
||||
|
||||
# Pass v_root as arg, since we chdir into a temp directory during testing and create a project there.
|
||||
set v_root [lindex $argv 0]
|
||||
set project_dir_name [lindex $argv 1]
|
||||
set corrected_mod_name [lindex $argv 2]
|
||||
|
||||
spawn $v_root/v init
|
||||
|
||||
expect "Input your project description: " { send "\r" } timeout { exit 1 }
|
||||
expect "Input your project version: (0.0.0) " { send "\r" } timeout { exit 1 }
|
||||
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
|
||||
expect "The directory name `$project_dir_name` is invalid as a module name. The module name in `v.mod` was set to `$corrected_mod_name`" {} timeout { exit 1 }
|
||||
expect "Created binary (application) project `$corrected_mod_name`" {} timeout {} timeout { exit 1 }
|
||||
|
||||
expect eof
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env expect
|
||||
|
||||
set timeout 10
|
||||
|
||||
# Pass v_root as arg, since we chdir into a temp directory during testing and create a project there.
|
||||
set v_root [lindex $argv 0]
|
||||
set model [lindex $argv 1]
|
||||
|
||||
spawn $v_root/v init $model
|
||||
|
||||
expect "Input your project description: " { send "My Awesome V Application.\r" } timeout { exit 1 }
|
||||
expect "Input your project version: (0.0.0) " { send "0.0.1\r" } timeout { exit 1 }
|
||||
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
|
||||
expect "Initialising ..." {} timeout { exit 1 }
|
||||
# The completion message is verified in `vcreate_init_test.v`.
|
||||
|
||||
expect eof
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env expect
|
||||
|
||||
set timeout 10
|
||||
|
||||
# Pass v_root as arg, since we chdir into a temp directory during testing and create a project there.
|
||||
set v_root [lindex $argv 0]
|
||||
set model [lindex $argv 1]
|
||||
set project_name [lindex $argv 2]
|
||||
|
||||
spawn $v_root/v new $model $project_name
|
||||
|
||||
expect "Input your project description: " { send "My Awesome V Project.\r" } timeout { exit 1 }
|
||||
expect "Input your project version: (0.0.0) " { send "0.0.1\r" } timeout { exit 1 }
|
||||
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
|
||||
expect "Initialising ..." {} timeout { exit 1 }
|
||||
expect "Created library project `$project_name`" {} timeout { exit 1 }
|
||||
|
||||
expect eof
|
||||
Loaded 100 of 11957 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user