Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+221
View File
@@ -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'
}
+19
View File
@@ -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
}
+55
View File
@@ -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}')
}
+11
View File
@@ -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()
}
+7
View File
@@ -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
+594
View File
@@ -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}'
}
+109
View File
@@ -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')
}
+13
View File
@@ -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)
}
}
+17
View File
@@ -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.
')
}
}
+11
View File
@@ -0,0 +1,11 @@
fast
index.html
table.html
v.c
v2
fast.log
website/
fast.vlang.io/
docs.vlang.io/
+332
View File
@@ -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(['<', '&lt;', '>', '&gt;'])
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
}
+111
View File
@@ -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()
}
}
+67
View File
@@ -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;
}
})();
+55
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
</table>
<script src="main.js"></script>
</body>
</html>
+36
View File
@@ -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'>&nbsp;&nbsp;&nbsp;&nbsp;Timestamp&nbsp;&nbsp;&nbsp;&nbsp;</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
}
+18
View File
@@ -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
+144
View File
@@ -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()
}
+16
View File
@@ -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") } ')
}
+365
View File
@@ -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.')
}
+69
View File
@@ -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}')
}
}
+74
View File
@@ -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` .')
}
+48
View File
@@ -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
}
+105
View File
@@ -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
}
+103
View File
@@ -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
}
+76
View File
@@ -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)
}
+325
View File
@@ -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'
}
}
+199
View File
@@ -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)
}
}
+222
View File
@@ -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()
}
+81
View File
@@ -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')
}
+1
View File
@@ -0,0 +1 @@
println(arguments()#[1..])
+98
View File
@@ -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)
}
+70
View File
@@ -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)
}
}
+62
View File
@@ -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)
}
}
+65
View File
@@ -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()
}
+1
View File
@@ -0,0 +1 @@
demo.json
+129
View File
@@ -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
+156
View File
@@ -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
}
+143
View File
@@ -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())
}
}
+102
View File
@@ -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)}')
}
}
+315
View File
@@ -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)
}
}
+184
View File
@@ -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)
}
}
+40
View File
@@ -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)
}
}
+89
View File
@@ -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
}
}
}
+9
View File
@@ -0,0 +1,9 @@
module main
import testing
fn main() {
if testing.building_any_v_binaries_failed() {
exit(1)
}
}
+209
View File
@@ -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.')
}
}
+161
View File
@@ -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) }
}
}
+930
View File
@@ -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)
}
}
+367
View File
@@ -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)!
}
+810
View File
@@ -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)
}
+238
View File
@@ -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)!
}
}
+46
View File
@@ -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)
}
}
+132
View File
@@ -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)
}
+34
View File
@@ -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
}
+276
View File
@@ -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
View File
@@ -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()
}
View File
Whitespace-only changes.
+30
View File
@@ -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())
}
View File
Whitespace-only changes.
+26
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
import simple
fn test_sum() {
s := simple.sum()
assert s == 10
println(s)
}
+7
View File
@@ -0,0 +1,7 @@
import simple
fn test_mul() {
m := simple.mul()
assert m == 24
println(m)
}
View File
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