tools: support // vtest build: !do_not_test ?, // vtest build: !windows && tinyc to skip files during testing on specific platforms, without having to keep centralised skip lists (#23900)

This commit is contained in:
Delyan Angelov 2025-03-11 21:57:47 +02:00 committed by GitHub
parent 5439ff9cde
commit 35b1cff2d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 473 additions and 5 deletions

View file

@ -12,6 +12,7 @@ import v.util.vtest
import runtime import runtime
import rand import rand
import strings import strings
import v.build_constraint
pub const max_header_len = get_max_header_len() pub const max_header_len = get_max_header_len()
@ -98,6 +99,8 @@ pub mut:
hash string // used as part of the name of the temporary directory created for tests, to ease cleanup hash string // used as part of the name of the temporary directory created for tests, to ease cleanup
exec_mode ActionMode = .compile // .compile_and_run only for `v test` exec_mode ActionMode = .compile // .compile_and_run only for `v test`
build_environment build_constraint.Environment // see the documentation in v.build_constraint
} }
pub fn (mut ts TestSession) add_failed_cmd(cmd string) { pub fn (mut ts TestSession) add_failed_cmd(cmd string) {
@ -443,6 +446,9 @@ pub fn (mut ts TestSession) test() {
printing_thread := spawn ts.print_messages() printing_thread := spawn ts.print_messages()
pool_of_test_runners.set_shared_context(ts) pool_of_test_runners.set_shared_context(ts)
ts.reporter.worker_threads_start(remaining_files, mut ts) ts.reporter.worker_threads_start(remaining_files, mut ts)
ts.build_environment = get_build_environment()
// all the testing happens here: // all the testing happens here:
pool_of_test_runners.work_on_pointers(unsafe { remaining_files.pointers() }) pool_of_test_runners.work_on_pointers(unsafe { remaining_files.pointers() })
@ -568,9 +574,23 @@ fn worker_trunner(mut p pool.PoolProcessor, idx int, thread_id int) voidptr {
} else { } else {
os.quoted_path(generated_binary_fpath) os.quoted_path(generated_binary_fpath)
} }
mut details := get_test_details(file)
mut should_be_built := true
if details.vbuild != '' {
should_be_built = ts.build_environment.eval(details.vbuild) or {
eprintln('${file}:${details.vbuild_line}:17: error during parsing the `// v test build` expression `${details.vbuild}`: ${err}')
false
}
$if trace_should_be_built ? {
eprintln('${file} has specific build constraint: `${details.vbuild}` => should_be_built: `${should_be_built}`')
eprintln('> env facts: ${ts.build_environment.facts}')
eprintln('> env defines: ${ts.build_environment.defines}')
}
}
ts.benchmark.step() ts.benchmark.step()
tls_bench.step() tls_bench.step()
if !ts.build_tools && abs_path in ts.skip_files { if !ts.build_tools && (!should_be_built || abs_path in ts.skip_files) {
ts.benchmark.skip() ts.benchmark.skip()
tls_bench.skip() tls_bench.skip()
if !hide_skips { if !hide_skips {
@ -599,7 +619,6 @@ fn worker_trunner(mut p pool.PoolProcessor, idx int, thread_id int) voidptr {
ts.append_message_with_duration(.cmd_end, '', cmd_duration, mtc) ts.append_message_with_duration(.cmd_end, '', cmd_duration, mtc)
if status != 0 { if status != 0 {
details := get_test_details(file)
os.setenv('VTEST_RETRY_MAX', '${details.retry}', true) os.setenv('VTEST_RETRY_MAX', '${details.retry}', true)
for retry := 1; retry <= details.retry; retry++ { for retry := 1; retry <= details.retry; retry++ {
if !details.hide_retries { if !details.hide_retries {
@ -686,7 +705,6 @@ fn worker_trunner(mut p pool.PoolProcessor, idx int, thread_id int) voidptr {
println(r.output.split_into_lines().filter(it.contains(' assert')).join('\n')) println(r.output.split_into_lines().filter(it.contains(' assert')).join('\n'))
} }
if r.exit_code != 0 { if r.exit_code != 0 {
mut details := get_test_details(file)
mut trimmed_output := r.output.trim_space() mut trimmed_output := r.output.trim_space()
if trimmed_output.len == 0 { if trimmed_output.len == 0 {
// retry running at least 1 more time, to avoid CI false positives as much as possible // retry running at least 1 more time, to avoid CI false positives as much as possible
@ -895,19 +913,25 @@ pub mut:
retry int retry int
flaky bool // when flaky tests fail, the whole run is still considered successful, unless VTEST_FAIL_FLAKY is 1 flaky bool // when flaky tests fail, the whole run is still considered successful, unless VTEST_FAIL_FLAKY is 1
// //
hide_retries bool // when true, all retry tries are silent; used by `vlib/v/tests/retry_test.v` hide_retries bool // when true, all retry tries are silent; used by `vlib/v/tests/retry_test.v`
vbuild string // could be `!(windows && tinyc)`
vbuild_line int // for more precise error reporting, if the `vbuild` expression is incorrect
} }
pub fn get_test_details(file string) TestDetails { pub fn get_test_details(file string) TestDetails {
mut res := TestDetails{} mut res := TestDetails{}
lines := os.read_lines(file) or { [] } lines := os.read_lines(file) or { [] }
for line in lines { for idx, line in lines {
if line.starts_with('// vtest retry:') { if line.starts_with('// vtest retry:') {
res.retry = line.all_after(':').trim_space().int() res.retry = line.all_after(':').trim_space().int()
} }
if line.starts_with('// vtest flaky:') { if line.starts_with('// vtest flaky:') {
res.flaky = line.all_after(':').trim_space().bool() res.flaky = line.all_after(':').trim_space().bool()
} }
if line.starts_with('// vtest build:') {
res.vbuild = line.all_after(':').trim_space()
res.vbuild_line = idx + 1
}
if line.starts_with('// vtest hide_retries') { if line.starts_with('// vtest hide_retries') {
res.hide_retries = true res.hide_retries = true
} }
@ -949,3 +973,9 @@ fn get_max_header_len() int {
} }
return cols return cols
} }
fn get_build_environment() &build_constraint.Environment {
facts := os.getenv('VBUILD_FACTS').split_any(',')
defines := os.getenv('VBUILD_DEFINES').split_any(',')
return build_constraint.new_environment(facts, defines)
}

View file

@ -98,6 +98,9 @@ fn main() {
exit(1) exit(1)
} }
timers.show('v parsing CLI args') timers.show('v parsing CLI args')
setup_vbuild_env_vars(prefs)
// Start calling the correct functions/external tools // Start calling the correct functions/external tools
// Note for future contributors: Please add new subcommands in the `match` block below. // Note for future contributors: Please add new subcommands in the `match` block below.
if command in external_tools { if command in external_tools {
@ -206,3 +209,28 @@ fn rebuild(prefs &pref.Preferences) {
} }
} }
} }
@[manualfree]
fn setup_vbuild_env_vars(prefs &pref.Preferences) {
mut facts := []string{cap: 10}
facts << prefs.os.lower()
facts << prefs.ccompiler_type.str()
facts << prefs.arch.str()
if prefs.is_prod {
facts << 'prod'
}
github_job := os.getenv('GITHUB_JOB')
if github_job != '' {
facts << github_job
}
sfacts := facts.join(',')
os.setenv('VBUILD_FACTS', sfacts, true)
sdefines := prefs.compile_defines_all.join(',')
os.setenv('VBUILD_DEFINES', sdefines, true)
unsafe { sdefines.free() }
unsafe { sfacts.free() }
unsafe { github_job.free() }
unsafe { facts.free() }
}

View file

@ -1,3 +1,4 @@
// vtest build: !do_not_test ?
module big module big
fn test_add_digit_array_01() { fn test_add_digit_array_01() {

View file

@ -0,0 +1,23 @@
module build_constraint
// ast:
struct BExpr {
expr BOr
}
struct BOr {
exprs []BAnd
}
struct BAnd {
exprs []BUnary
}
type BUnary = BNot | BExpr | BFact | BDefine
struct BNot {
expr BUnary
}
type BFact = string
type BDefine = string

View file

@ -0,0 +1,102 @@
import v.build_constraint
const benv = build_constraint.new_environment(['linux', 'tinyc'], ['abc', 'def'])
fn test_eval_fact() {
assert benv.is_fact('tinyc')
assert benv.is_fact('linux')
assert !benv.is_fact('macos')
assert !benv.is_fact('windows')
}
fn test_eval_define() {
assert benv.is_define('abc')
assert benv.is_define('def')
assert !benv.is_define('xyz')
}
fn test_eval_platforms_and_compilers() {
assert benv.eval('tinyc')!
assert benv.eval(' tinyc')!
assert benv.eval('tinyc ')!
assert benv.eval(' tinyc ')!
assert !benv.eval('gcc')!
assert !benv.eval('clang')!
assert !benv.eval('msvc')!
assert benv.eval('linux')!
assert benv.eval(' linux')!
assert benv.eval('linux ')!
assert benv.eval(' linux ')!
assert !benv.eval('windows')!
assert !benv.eval('macos')!
assert !benv.eval('freebsd')!
}
fn test_eval_defines() {
assert benv.eval('abc?')!
assert benv.eval(' abc?')!
assert benv.eval('abc? ')!
assert benv.eval(' abc? ')!
assert benv.eval('abc ?')!
assert benv.eval(' abc ?')!
assert benv.eval('abc ? ')!
assert benv.eval(' abc ? ')!
assert benv.eval('def?')!
}
fn test_eval_not() {
assert benv.eval('!gcc')!
assert benv.eval('!clang')!
assert benv.eval('!msvc')!
assert !benv.eval('!tinyc')!
assert !benv.eval(' !tinyc')!
assert !benv.eval('!tinyc ')!
assert !benv.eval(' !tinyc ')!
assert benv.eval('!xyz?')!
}
fn test_eval_and() {
assert benv.eval('linux && tinyc')!
assert !benv.eval('macos && tinyc')!
assert !benv.eval('windows && tinyc')!
assert !benv.eval('linux && gcc')!
//
assert benv.eval('linux && tinyc && abc?')!
assert benv.eval('linux && tinyc && def?')!
assert !benv.eval('linux && tinyc && xyz?')!
//
assert benv.eval('linux && !gcc')!
assert benv.eval('linux && !clang')!
assert benv.eval('!gcc && !windows')!
assert !benv.eval('!windows && tcc')!
assert !benv.eval('windows && gcc')!
assert !benv.eval('gcc && !windows')!
}
fn test_eval_or() {
assert benv.eval('windows||tinyc')!
assert benv.eval('windows || macos || tinyc')!
assert benv.eval('windows || macos || tinyc')!
assert benv.eval('windows || macos || gcc || abc?')!
assert benv.eval('!windows||gcc')!
}
fn test_complex() {
assert benv.eval(' (windows || tinyc) && linux ')!
assert !benv.eval(' (windows || gcc) && linux ')!
assert benv.eval(' (windows || tinyc) && !macos ')!
assert !benv.eval(' (windows || tinyc) && macos ')!
}
fn test_precedence() {
assert benv.eval(' tinyc && !windows ')! == benv.eval(' tinyc && (!windows)')!
assert benv.eval(' tinyc && !windows ')! == benv.eval(' (!windows) && tinyc')!
assert benv.eval(' !windows && tinyc')! == benv.eval(' (!windows) && tinyc')!
assert benv.eval(' !windows || tinyc')! == benv.eval(' (!windows) || tinyc')!
assert benv.eval(' !linux && tinyc')! == benv.eval(' (!linux) && tinyc')!
assert benv.eval(' !linux || tinyc')! == benv.eval(' (!linux) || tinyc')!
assert benv.eval(' !windows && gcc ')! == benv.eval(' (!windows) && gcc ')!
assert benv.eval(' !windows || gcc ')! == benv.eval(' (!windows) || gcc ')!
assert benv.eval(' !linux && gcc ')! == benv.eval(' (!linux) && gcc ')!
assert benv.eval(' !linux || gcc ')! == benv.eval(' (!linux) || gcc ')!
}

View file

@ -0,0 +1,43 @@
module build_constraint
// evaluating the AST nodes, in the given environment
fn (b BExpr) eval(env &Environment) !bool {
return b.expr.eval(env)
}
fn (b BOr) eval(env &Environment) !bool {
for e in b.exprs {
if e.eval(env)! {
return true
}
}
return false
}
fn (b BAnd) eval(env &Environment) !bool {
for e in b.exprs {
if !e.eval(env)! {
return false
}
}
return true
}
fn (b BUnary) eval(env &Environment) !bool {
match b {
BNot, BExpr, BFact, BDefine { return b.eval(env)! }
}
return false
}
fn (b BNot) eval(env &Environment) !bool {
return !b.expr.eval(env)!
}
fn (b BFact) eval(env &Environment) !bool {
return env.is_fact(b)
}
fn (b BDefine) eval(env &Environment) !bool {
return env.is_define(b)
}

View file

@ -0,0 +1,102 @@
module build_constraint
// lexing:
enum BTokenKind {
tfact // linux, tinyc, prod etc
tdefine // abc, gcboehm
tor // ||
tand // &&
tnot // !
tparen_open
tparen_close
teof
}
struct Token {
kind BTokenKind
value string
}
fn unexpected(c u8) IError {
return error('unexpected character `${rune(c)}`')
}
fn new_token(kind BTokenKind, value string) Token {
return Token{
kind: kind
value: value
}
}
fn new_op(kind BTokenKind) Token {
return new_token(kind, '')
}
fn new_span(kind BTokenKind, mut span []u8) Token {
t := new_token(kind, span.bytestr())
span.clear()
return t
}
fn lex(s string) ![]Token {
mut res := []Token{}
mut span := []u8{cap: s.len}
mut op := []u8{}
for c in s {
match c {
` `, `\t`, `\n` {}
`(` {
if span.len > 0 {
res << new_span(.tfact, mut span)
}
res << new_op(.tparen_open)
}
`)` {
if span.len > 0 {
res << new_span(.tfact, mut span)
}
res << new_op(.tparen_close)
}
`&`, `|` {
if span.len > 0 {
res << new_span(.tfact, mut span)
}
op << c
if op == [c, c] {
op.clear()
if c == `&` {
res << new_op(.tand)
} else if c == `|` {
res << new_op(.tor)
} else {
return unexpected(c)
}
}
if op.len == 2 {
return unexpected(c)
}
}
`?` {
res << new_span(.tdefine, mut span)
}
`!` {
res << new_op(.tnot)
if span.len > 0 {
return unexpected(c)
}
}
else {
if u8(c).is_alnum() || c in [`_`, `-`] {
span << c
} else {
return unexpected(c)
}
}
}
}
if span.len > 0 {
res << new_span(.tfact, mut span)
}
res << new_op(.teof)
return res
}

View file

@ -0,0 +1,95 @@
module build_constraint
// parsing:
struct BParser {
tokens []Token
mut:
pos int
}
fn (mut p BParser) peek(n int) Token {
if p.pos + n >= p.tokens.len {
return Token{
kind: .teof
}
}
t := p.tokens[p.pos + n]
return t
}
fn (mut p BParser) next() {
p.pos++
}
fn (mut p BParser) parse() !BExpr {
return p.expr()
}
fn (mut p BParser) expr() !BExpr {
return BExpr{
expr: p.or_expr()!
}
}
fn (mut p BParser) or_expr() !BOr {
mut exprs := []BAnd{}
exprs << p.and_expr()!
for t := p.peek(0); t.kind == .tor; t = p.peek(0) {
p.next()
exprs << p.and_expr()!
}
return BOr{
exprs: exprs
}
}
fn (mut p BParser) and_expr() !BAnd {
mut exprs := []BUnary{}
exprs << p.unary_expr()!
for t := p.peek(0); t.kind == .tand; t = p.peek(0) {
p.next()
exprs << p.unary_expr()!
}
return BAnd{
exprs: exprs
}
}
fn (mut p BParser) unary_expr() !BUnary {
t := p.peek(0)
match t.kind {
.tfact {
p.next()
return BUnary(BFact(t.value))
}
.tdefine {
p.next()
return BUnary(BDefine(t.value))
}
.tnot {
p.next()
nt := p.peek(0)
if nt.kind in [.tfact, .tdefine] {
ident := p.unary_expr()!
return BNot{
expr: ident
}
}
expr := p.expr()!
return BNot{
expr: expr
}
}
.tparen_open {
p.next()
expr := p.expr()!
if p.peek(0).kind != .tparen_close {
return error('expected closing )')
}
p.next()
return BUnary(expr)
}
else {}
}
return error('unary failed, unexpected ${t}')
}

View file

@ -0,0 +1,44 @@
module build_constraint
// Environment represents the current build environment.
@[heap]
pub struct Environment {
pub mut:
facts map[string]bool
defines map[string]bool
}
// new_environment creates a new Environment.
// `facts` is a list of predefined platforms, compilers, build options etc, for example: ['linux', 'tinyc', 'prod', 'amd64']
// `defines` is a list of the user defines, for example: ['abc', 'gcboehm_opt', 'gg_record', 'show_fps']
pub fn new_environment(facts []string, defines []string) &Environment {
mut b := &Environment{}
for f in facts {
b.facts[f] = true
}
for d in defines {
b.defines[d] = true
}
return b
}
// eval evaluates the given build `constraint` against the current environment.
// The constraint can be for example something simple like just `linux`,
// but it can be also a more complex logic expression like: `(windows && tinyc) || prod`
pub fn (b &Environment) eval(constraint string) !bool {
mut parser := BParser{
tokens: lex(constraint)!
}
expr := parser.parse()!
return expr.eval(b)
}
// is_fact checks whether the given `fact` is present in the environment.
pub fn (b &Environment) is_fact(fact string) bool {
return fact in b.facts
}
// is_define checks whether the given `define` is present in the environment.
pub fn (b &Environment) is_define(define string) bool {
return define in b.defines
}