Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/testing/executables.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub fn prepare_executable(tool_name string) string {
$if debug {
eprintln('>> compiling with: `${compilation_cmd}`')
}
res := os.execute(compilation_cmd)
res := os.exec_opt(split_args(compilation_cmd)) or { panic('could not run ${compilation_cmd}') }
if res.exit_code != 0 {
eprintln('Tool ${tool_name} could not be compiled.')
eprintln('Compilation command:\n${compilation_cmd}')
Expand Down
67 changes: 67 additions & 0 deletions common/testing/split_args.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module testing

import os

// split_args breaks a command line into the argument array os.exec takes.
//
// It exists because os.execute is deprecated and `make test` builds with -W, where a
// deprecation is an error: on current V that stopped 31 of the 49 test files from
// compiling at all. Every command in this repository is written as a string by the
// tests, so the string has to be split somewhere, and this is that place.
//
// The rules are the ones those strings actually use, which is not all of a shell:
//
// tool -a "two words" 'and more' three kinds of quoting, all of them in use
// tool --delimiter="|" a quoted value with a character in it
// sh -c "exit 42" a quoted argument containing a space
// tr -d '\r' a backslash inside single quotes
// C:\Users\me\file.txt a backslash outside quotes, which is a path
//
// So: whitespace between words, single and double quotes, and nothing else. A
// backslash is an ordinary character everywhere, because on this platform it is
// almost always part of a path, and a shell's rule of eating it would take the
// volume off. An unterminated quote is not an error here: the rest of the line
// becomes one word, so a test fails on what the program said rather than on a parse
// in this file.
pub fn split_args(cmd string) []string {
mut args := []string{}
mut word := []rune{}
mut in_word := false
mut quote := u8(0)
for ch in cmd.runes() {
c := u8(ch)
if quote != 0 {
if c == quote {
quote = 0
} else {
word << ch
}
continue
}
if c == `'` || c == `"` {
quote = c
in_word = true
continue
}
if c == ` ` || c == `\t` || c == `\n` || c == `\r` {
if in_word {
args << word.string()
word = []rune{}
in_word = false
}
continue
}
word << ch
in_word = true
}
if in_word {
args << word.string()
}
return args
}

// run_args is split_args for the common case where the caller has the executable and
// the rest of the command line already separate, which is how the tests below read.
pub fn run_args(exe string, args string) os.Result {
return os.exec(split_args('${exe} ${args}'))
}
40 changes: 40 additions & 0 deletions common/testing/split_args_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module testing

// Every shape here is one a test in this repository actually writes, so the rules in
// split_args are pinned by something rather than by taste.
fn test_split_args_plain_words() {
assert split_args('tool -a -b file') == ['tool', '-a', '-b', 'file']
assert split_args('') == []string{}
assert split_args(' ') == []string{}
}

fn test_split_args_runs_of_whitespace() {
assert split_args('tool -a\t\t-b\nfile') == ['tool', '-a', '-b', 'file']
// comm_test.v writes this one
assert split_args('comm --output-delimiter="|" f1 f2') ==
['comm', '--output-delimiter=|', 'f1', 'f2']
}

fn test_split_args_quotes() {
// timeout_test.v writes this one
assert split_args('timeout --preserve-status 1 sh -c "exit 42"') ==
['timeout', '--preserve-status', '1', 'sh', '-c', 'exit 42']
// sum_test.v writes this one, where the backslash is a character and not an escape
assert split_args("sum tr -d '\\r' f") == ['sum', 'tr', '-d', '\\r', 'f']
// An empty quoted word is still a word
assert split_args('tool "" x') == ['tool', '', 'x']
}

fn test_split_args_keeps_backslashes_in_paths() {
// The reason this is not a shell: eating the backslash would take the volume off.
path := r'C:\Users\me\coreutils\test.txt'
assert split_args('cksum ${path}') == ['cksum', path]
assert split_args('cksum "${path}"') == ['cksum', path]
}

// An unterminated quote is not an error: whatever followed becomes one word, so the
// test that made the mistake fails on the program's message.
fn test_split_args_unterminated_quote() {
assert split_args('tool "two words') == ['tool', 'two words']
assert split_args("tool 'one") == ['tool', 'one']
}
14 changes: 7 additions & 7 deletions common/testing/testing.v
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ pub fn (p CommandPair) same_results(options string) bool {
// and the deputy commands with them, and ensure that they both fail
// with the same exit_code
pub fn (p CommandPair) expected_failure(options string) !os.Result {
ores := os.execute('${p.original} ${options}')
ores := run_args(p.original, options)
if ores.exit_code == 0 {
return DidNotFailError{
msg: '${p.original} ${options}'
code: 1
}
}
dres := os.execute('${p.deputy} ${options}')
dres := run_args(p.deputy, options)
if dres.exit_code == 0 {
return DidNotFailError{
msg: '${p.deputy} ${options}'
Expand All @@ -76,13 +76,13 @@ pub fn (p CommandPair) expected_failure(options string) !os.Result {
pub fn (p CommandPair) ensure_help_and_version_options_work() ! {
// For now, assume that the original has --version and --help
// and that they already work correctly.
if os.execute('${p.deputy} --help').exit_code != 0 {
if run_args(p.deputy, '--help').exit_code != 0 {
return DoesNotWorkError{
msg: '--help'
code: 1
}
}
if os.execute('${p.deputy} --version').exit_code != 0 {
if run_args(p.deputy, '--version').exit_code != 0 {
return DoesNotWorkError{
msg: '--version'
code: 2
Expand All @@ -96,7 +96,7 @@ pub fn (p CommandPair) ensure_help_and_version_options_work() ! {
// It also returns the actual result of the execution,
// so that you can inspect it further for more details.
pub fn command_fails(cmd string) !os.Result {
res := os.execute(cmd)
res := os.exec(split_args(cmd))
if res.exit_code == 0 {
return DidNotFailError{
msg: cmd
Expand All @@ -115,8 +115,8 @@ const use_multi_binary_to_test = os.getenv('USE_MULTI_BINARY_TO_TEST')
// and for their output.
// note: use `v -d trace_same_results ...` to enable trace output
pub fn same_results(cmd1 string, cmd2 string) bool {
cmd1_res := os.execute(cmd1)
cmd2_res := os.execute(cmd2)
cmd1_res := os.exec(split_args(cmd1))
cmd2_res := os.exec(split_args(cmd2))
mut noutput1 := normalise(cmd1_res.output)
mut noutput2 := normalise(cmd2_res.output)
$if trace_same_results ? {
Expand Down
12 changes: 6 additions & 6 deletions common/testing/testrig.v
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub:
}

pub fn (rig TestRig) call_for_test(args string) os.Result {
res := os.execute('${rig.executable_under_test} ${args}')
res := run_args(rig.executable_under_test, args)
assert res.exit_code == 0
return res
}
Expand Down Expand Up @@ -66,7 +66,7 @@ pub fn (rig TestRig) clean_up() {
}

pub fn (rig TestRig) assert_platform_util() {
platform_ver := os.execute('${rig.platform_util_call} --version')
platform_ver := run_args(rig.platform_util_call, '--version')
eprintln('Platform util version: [${platform_ver.output}]')
assert platform_ver.exit_code == 0

Expand All @@ -90,11 +90,11 @@ pub fn (rig TestRig) assert_platform_util() {
}

pub fn (rig TestRig) call_orig(args string) os.Result {
return os.execute('${rig.platform_util_call} ${args}')
return run_args(rig.platform_util_call, args)
}

pub fn (rig TestRig) call_new(args string) os.Result {
return os.execute('${rig.executable_under_test} ${args}')
return run_args(rig.executable_under_test, args)
}

// print_small_diff eprints only differing small results that differ
Expand Down Expand Up @@ -208,8 +208,8 @@ pub fn (rig TestRig) assert_help_and_version_options_work() {
// For now, assume that the original has --version and --help
// and that they already work correctly.

ver := os.execute('${rig.executable_under_test} --version')
ver := run_args(rig.executable_under_test, '--version')
assert ver.output.trim_space() == '${rig.util} (V coreutils) ${common.version}'
assert ver.exit_code == 0
assert os.execute('${rig.executable_under_test} --help').exit_code == 0
assert run_args(rig.executable_under_test, '--help').exit_code == 0
}
4 changes: 2 additions & 2 deletions src/base64/base64_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ fn test_help_and_version() {
}

fn test_abcd() {
res := os.execute('${rig.executable_under_test} abcd')
res := os.exec(testing.split_args('${rig.executable_under_test} abcd'))
assert res.exit_code == 1
assert res.output.trim_space() == 'base64: abcd: No such file or directory'
}

fn expected_result(input string, output string) {
c := '${executable_under_test} ${input}'
res := os.execute(c)
res := os.exec(testing.split_args(c))
eprintln('>>>> cmd: `${c}`')
if res.exit_code != 0 || res.output.split_into_lines() != output.split_into_lines() {
eprintln('>>>> res.exit_code: ${res.exit_code}')
Expand Down
8 changes: 4 additions & 4 deletions src/cksum/cksum_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,28 @@ fn test_stdin() {
if no_standard_input_here() {
return
}
res := os.execute('cat ${test1_txt_path} | ${executable_under_test}')
res := os.exec(testing.split_args('cat ${test1_txt_path} | ${executable_under_test}'))

assert res.exit_code == 0
assert res.output.trim_space() == '365965416 25'
}

fn test_file_not_exist() {
res := os.execute('${executable_under_test} abcd')
res := os.exec(testing.split_args('${executable_under_test} abcd'))

assert res.exit_code == 1
assert res.output.trim_space() == 'cksum: abcd: No such file or directory'
}

fn test_one_file() {
res := os.execute('${executable_under_test} ${test1_txt_path}')
res := os.exec(testing.split_args('${executable_under_test} ${test1_txt_path}'))

assert res.exit_code == 0
assert res.output == '365965416 25 ${test1_txt_path}${eol}'
}

fn test_several_files() {
res := os.execute('${executable_under_test} ${test1_txt_path} ${test2_txt_path}')
res := os.exec(testing.split_args('${executable_under_test} ${test1_txt_path} ${test2_txt_path}'))

assert res.exit_code == 0
assert res.output == '365965416 25 ${test1_txt_path}${eol}1338884673 131077 ${test2_txt_path}${eol}'
Expand Down
28 changes: 14 additions & 14 deletions src/comm/comm_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn test_comm_basic() {
os.write_lines(file2_path, ['banana', 'cherry', 'date', 'fig'])!

// Test basic functionality
res := os.execute('${executable_under_test} ${file1_path} ${file2_path}')
res := os.exec(testing.split_args('${executable_under_test} ${file1_path} ${file2_path}'))
assert res.exit_code == 0

// Normalize line endings for cross-platform compatibility
Expand All @@ -45,22 +45,22 @@ fn test_comm_suppress_columns() {
os.write_lines(file2_path, ['b', 'c', 'd'])!

// Test -1 flag
res1 := os.execute('${executable_under_test} -1 ${file1_path} ${file2_path}')
res1 := os.exec(testing.split_args('${executable_under_test} -1 ${file1_path} ${file2_path}'))
assert res1.exit_code == 0
assert res1.output.replace('\r\n', '\n') == '\tb\n\tc\nd\n'

// Test -2 flag
res2 := os.execute('${executable_under_test} -2 ${file1_path} ${file2_path}')
res2 := os.exec(testing.split_args('${executable_under_test} -2 ${file1_path} ${file2_path}'))
assert res2.exit_code == 0
assert res2.output.replace('\r\n', '\n') == 'a\n\tb\n\tc\n'

// Test -3 flag
res3 := os.execute('${executable_under_test} -3 ${file1_path} ${file2_path}')
res3 := os.exec(testing.split_args('${executable_under_test} -3 ${file1_path} ${file2_path}'))
assert res3.exit_code == 0
assert res3.output.replace('\r\n', '\n') == 'a\n\td\n'

// Test -12 (show only common)
res12 := os.execute('${executable_under_test} -12 ${file1_path} ${file2_path}')
res12 := os.exec(testing.split_args('${executable_under_test} -12 ${file1_path} ${file2_path}'))
assert res12.exit_code == 0
assert res12.output.replace('\r\n', '\n') == 'b\nc\n'

Expand All @@ -83,17 +83,17 @@ fn test_comm_empty_files() {
os.write_lines(nonempty_file, ['hello', 'world'])!

// Both files empty
res1 := os.execute('${executable_under_test} ${empty_file} ${empty_file}')
res1 := os.exec(testing.split_args('${executable_under_test} ${empty_file} ${empty_file}'))
assert res1.exit_code == 0
assert res1.output == ''

// First file empty
res2 := os.execute('${executable_under_test} ${empty_file} ${nonempty_file}')
res2 := os.exec(testing.split_args('${executable_under_test} ${empty_file} ${nonempty_file}'))
assert res2.exit_code == 0
assert res2.output.replace('\r\n', '\n') == '\thello\n\tworld\n'

// Second file empty
res3 := os.execute('${executable_under_test} ${nonempty_file} ${empty_file}')
res3 := os.exec(testing.split_args('${executable_under_test} ${nonempty_file} ${empty_file}'))
assert res3.exit_code == 0
assert res3.output.replace('\r\n', '\n') == 'hello\nworld\n'

Expand All @@ -120,7 +120,7 @@ fn test_comm_stdin() {
os.write_lines(stdin_file, ['banana', 'cherry'])!

// Test stdin as first file
res1 := os.execute('cat ${stdin_file} | ${executable_under_test} - ${file_path}')
res1 := os.exec(testing.split_args('cat ${stdin_file} | ${executable_under_test} - ${file_path}'))
assert res1.exit_code == 0
// stdin has: banana, cherry
// file_path has: apple, banana
Expand All @@ -129,7 +129,7 @@ fn test_comm_stdin() {
assert res1.output.replace('\r\n', '\n') == expected1

// Test stdin as second file
res2 := os.execute('cat ${stdin_file} | ${executable_under_test} ${file_path} -')
res2 := os.exec(testing.split_args('cat ${stdin_file} | ${executable_under_test} ${file_path} -'))
assert res2.exit_code == 0
expected2 := 'apple\n\t\tbanana\n\tcherry\n'
assert res2.output.replace('\r\n', '\n') == expected2
Expand All @@ -141,20 +141,20 @@ fn test_comm_stdin() {

fn test_comm_missing_file() {
// Missing file should produce error
res := os.execute('${executable_under_test} /nonexistent/file1 /nonexistent/file2')
res := os.exec(testing.split_args('${executable_under_test} /nonexistent/file1 /nonexistent/file2'))
assert res.exit_code == 1
// Error message varies by platform, but should contain the filename
assert res.output.contains('/nonexistent/file1') || res.output.contains('\\nonexistent\\file1')
}

fn test_comm_missing_operand() {
// No arguments should produce error
res1 := os.execute('${executable_under_test}')
res1 := os.exec(testing.split_args('${executable_under_test}'))
assert res1.exit_code == 1
assert res1.output.contains('missing operand')

// One argument should produce error
res2 := os.execute('${executable_under_test} /tmp/file1')
res2 := os.exec(testing.split_args('${executable_under_test} /tmp/file1'))
assert res2.exit_code == 1
assert res2.output.contains('missing operand')
}
Expand All @@ -167,7 +167,7 @@ fn test_comm_delimiter() {
os.write_lines(file2_path, ['b', 'c'])!

// Test custom delimiter
res := os.execute('${executable_under_test} --output-delimiter="|" ${file1_path} ${file2_path}')
res := os.exec(testing.split_args('${executable_under_test} --output-delimiter="|" ${file1_path} ${file2_path}'))
assert res.exit_code == 0
assert res.output.replace('\r\n', '\n') == 'a\n|b\n||c\n'

Expand Down
2 changes: 1 addition & 1 deletion src/dirname/dirname_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn test_help_and_version() {
}

fn expected_result(input string, output string) {
res := os.execute('${executable_under_test} ${input}')
res := os.exec(testing.split_args('${executable_under_test} ${input}'))
assert res.exit_code == 0
assert res.output.trim_space() == output
testing.same_results('${rig.util} ${input}', '${executable_under_test} ${input}')
Expand Down
Loading