Skip to content

Commit

Permalink
tools: support v doc -run-examples math, to ensure that all `// Exa…
Browse files Browse the repository at this point in the history
…mple: code` doc comments are working (#19852)
  • Loading branch information
spytheman committed Nov 13, 2023
1 parent 453f65a commit 999328a
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 191 deletions.
162 changes: 162 additions & 0 deletions cmd/tools/vdoc/main.v
@@ -0,0 +1,162 @@
module main

import os
import os.cmdline
import term
import v.doc
import v.vmod

const vexe = os.getenv_opt('VEXE') or { @VEXE }

const vroot = os.dir(vexe)

const allowed_formats = ['md', 'markdown', 'json', 'text', 'stdout', 'html', 'htm']

fn main() {
if os.args.len < 2 || '-h' in os.args || '-help' in os.args || '--help' in os.args
|| os.args[1..] == ['doc', 'help'] {
os.system('${os.quoted_path(vexe)} help doc')
exit(0)
}
args := os.args[2..].clone()
cfg := parse_arguments(args)
if cfg.input_path.len == 0 {
eprintln('vdoc: No input path found.')
exit(1)
}
// Config is immutable from this point on
mut vd := &VDoc{
cfg: cfg
manifest: vmod.Manifest{
repo_url: ''
}
}
vd.vprintln('Setting output type to "${cfg.output_type}"')
vd.generate_docs_from_file()
if cfg.run_examples {
println('')
if vd.example_oks == 0 && vd.example_failures == 0 {
println(term.colorize(term.bright_yellow, 'Found NO examples.'))
} else {
println(term.colorize(term.gray, 'Found ${vd.example_oks} ok examples.'))
}
if vd.example_failures > 0 {
println(term.colorize(term.red, 'Found ${vd.example_failures} failing examples.'))
exit(1)
}
}
}

fn parse_arguments(args []string) Config {
mut cfg := Config{}
cfg.is_color = term.can_show_color_on_stdout()
for i := 0; i < args.len; i++ {
arg := args[i]
current_args := args[i..]
match arg {
'-all' {
cfg.pub_only = false
}
'-f' {
format := cmdline.option(current_args, '-f', '')
if format !in allowed_formats {
allowed_str := allowed_formats.join(', ')
eprintln('vdoc: "${format}" is not a valid format. Only ${allowed_str} are allowed.')
exit(1)
}
cfg.output_type = set_output_type_from_str(format)
i++
}
'-color' {
cfg.is_color = true
}
'-no-color' {
cfg.is_color = false
}
'-inline-assets' {
cfg.inline_assets = true
}
'-theme-dir' {
cfg.theme_dir = cmdline.option(current_args, '-theme-dir', default_theme)
}
'-l' {
cfg.show_loc = true
}
'-comments' {
cfg.include_comments = true
}
'-m' {
cfg.is_multi = true
}
'-o' {
opath := cmdline.option(current_args, '-o', '')
cfg.output_path = if opath == 'stdout' { opath } else { os.real_path(opath) }
i++
}
'-os' {
platform_str := cmdline.option(current_args, '-os', '')
if platform_str == 'cross' {
eprintln('`v doc -os cross` is not supported yet.')
exit(1)
}
selected_platform := doc.platform_from_string(platform_str) or {
eprintln(err.msg())
exit(1)
}
cfg.platform = selected_platform
i++
}
'-run-examples' {
cfg.run_examples = true
}
'-no-timestamp' {
cfg.no_timestamp = true
}
'-no-examples' {
cfg.include_examples = false
}
'-readme' {
cfg.include_readme = true
}
'-v' {
cfg.is_verbose = true
}
else {
if cfg.input_path.len < 1 {
cfg.input_path = arg
} else if !cfg.is_multi {
// Symbol name filtering should not be enabled
// in multi-module documentation mode.
cfg.symbol_name = arg
}
if i == args.len - 1 {
break
}
}
}
}
// Correct from configuration from user input
if cfg.output_path == 'stdout' && cfg.output_type == .html {
cfg.inline_assets = true
}
$if windows {
cfg.input_path = cfg.input_path.replace('/', os.path_separator)
} $else {
cfg.input_path = cfg.input_path.replace('\\', os.path_separator)
}
is_path := cfg.input_path.ends_with('.v') || cfg.input_path.split(os.path_separator).len > 1
|| cfg.input_path == '.'
if cfg.input_path.trim_right('/') == 'vlib' {
cfg.is_vlib = true
cfg.is_multi = true
cfg.input_path = os.join_path(vroot, 'vlib')
} else if !is_path {
// TODO vd.vprintln('Input "$cfg.input_path" is not a valid path. Looking for modules named "$cfg.input_path"...')
mod_path := doc.lookup_module(cfg.input_path) or {
eprintln('vdoc: ${err}')
exit(1)
}
cfg.input_path = mod_path
}
return cfg
}
65 changes: 65 additions & 0 deletions cmd/tools/vdoc/run_examples.v
@@ -0,0 +1,65 @@
module main

import v.doc
import v.vmod
import strings
import os
import rand
import term

const normalised_default_vmodules_path = os.vmodules_dir().replace('\\', '/')

fn get_mod_name_by_file_path(file_path string) string {
mut mcache := vmod.get_cache()
dn_folder := os.dir(os.real_path(file_path)).replace('\\', '/').trim_string_right('/src')
vmodpath := mcache.get_by_folder(dn_folder)
normal_folder := dn_folder.replace('\\', '/')
vmod_folder := vmodpath.vmod_folder.replace('\\', '/')
mut relative_mod_path := normal_folder
relative_mod_path = relative_mod_path.trim_string_left(vmod_folder).trim_string_left('/')
relative_mod_path = relative_mod_path.trim_string_left(normalised_default_vmodules_path)
relative_mod_path = relative_mod_path.trim_string_left('vlib/')
mod_name := relative_mod_path.replace('/', '.').trim('.')
return mod_name
}

fn (mut vd VDoc) run_examples(dn doc.DocNode, mut pw strings.Builder) {
if dn.comments.len == 0 || !vd.cfg.run_examples {
return
}
examples := dn.examples()
if examples.len == 0 {
return
}
efolder := os.vtmp_dir()
mut example_program_source_files := []string{}
defer {
for sfile in example_program_source_files {
os.rm(sfile) or {}
}
}
mut failures := 0
mut oks := 0
for example in examples {
code := example.all_after('Example:').all_after('example:').trim_space()
mod_name := get_mod_name_by_file_path(dn.file_path)
vsource_path := os.join_path(efolder, 'example_${rand.ulid()}.v')
// eprintln('>>> example dn.file_path: ${dn.file_path} | mod_name: ${mod_name} | vsource_path: ${vsource_path} | code: `${code}`')
import_clause := if mod_name in ['builtin', ''] { '' } else { 'import ${mod_name}\n' }
source := '${import_clause}fn main() {\n\t${code}\n}\n'
os.write_file(vsource_path, source) or { continue }
cmd := '${os.quoted_path(vexe)} -g run ${os.quoted_path(vsource_path)}'
res := os.execute(cmd)
if res.exit_code != 0 {
eprintln('${dn_to_location(dn)}:${term.ecolorize(term.red, 'error in documentation example')}')
eprintln('cmd: ${cmd}')
eprintln('result: ${res.output}')
failures++
continue
}
example_program_source_files << vsource_path
oks++
}
vd.example_failures += failures
vd.example_oks += oks
}
Empty file.
@@ -0,0 +1,5 @@
module main

fn abc()
abc just prints 'xyz'. The important thing however is the next line, that does an assertion, that should FAIL to be executed with `v doc -run-examples good.v`:
Example: assert 5 * 5 == 77
3 changes: 3 additions & 0 deletions cmd/tools/vdoc/tests/testdata/run_examples_bad/main.out
@@ -0,0 +1,3 @@
module main

fn abc()
10 changes: 10 additions & 0 deletions cmd/tools/vdoc/tests/testdata/run_examples_bad/main.v
@@ -0,0 +1,10 @@
// abc just prints 'xyz'. The important thing however is the next line, that does an assertion,
// that should FAIL to be executed with `v doc -run-examples good.v`:
// Example: assert 5 * 5 == 77
pub fn abc() {
println('xyz')
}

fn main() {
abc()
}
Empty file.
@@ -0,0 +1,5 @@
module main

fn abc()
abc just prints 'xyz'. The important thing however is the next line, that does an assertion, that should be executed with `v doc -run-examples good.v`, and should succeed:
Example: assert 5 * 5 == 25
3 changes: 3 additions & 0 deletions cmd/tools/vdoc/tests/testdata/run_examples_good/main.out
@@ -0,0 +1,3 @@
module main

fn abc()
10 changes: 10 additions & 0 deletions cmd/tools/vdoc/tests/testdata/run_examples_good/main.v
@@ -0,0 +1,10 @@
// abc just prints 'xyz'. The important thing however is the next line, that does an assertion,
// that should be executed with `v doc -run-examples good.v`, and should succeed:
// Example: assert 5 * 5 == 25
pub fn abc() {
println('xyz')
}

fn main() {
abc()
}
25 changes: 25 additions & 0 deletions cmd/tools/vdoc/tests/vdoc_file_test.v
Expand Up @@ -23,6 +23,31 @@ fn test_vet() {
assert fails == 0
}

fn test_run_examples_good() {
os.setenv('VCOLORS', 'never', true)
os.chdir(vroot)!
res := os.execute('${os.quoted_path(vexe)} doc -comments -run-examples cmd/tools/vdoc/tests/testdata/run_examples_good/main.v')
assert res.exit_code == 0
assert res.output.contains('module main'), res.output
assert res.output.contains('fn abc()'), res.output
assert res.output.contains("abc just prints 'xyz'"), res.output
assert res.output.contains('and should succeed'), res.output
assert res.output.contains('Example: assert 5 * 5 == 25'), res.output
}

fn test_run_examples_bad() {
os.setenv('VCOLORS', 'never', true)
os.chdir(vroot)!
res := os.execute('${os.quoted_path(vexe)} doc -comments -run-examples cmd/tools/vdoc/tests/testdata/run_examples_bad/main.v')
assert res.exit_code != 0
assert res.output.contains('error in documentation example'), res.output
assert res.output.contains(' left value: 5 * 5 = 25'), res.output
assert res.output.contains('right value: 77'), res.output
assert res.output.contains('V panic: Assertion failed...'), res.output
assert res.output.contains('module main'), res.output
assert res.output.contains('Example: assert 5 * 5 == 77'), res.output
}

fn get_main_files_in_dir(dir string) []string {
mut mfiles := os.walk_ext(dir, '.v')
mfiles.sort()
Expand Down

0 comments on commit 999328a

Please sign in to comment.