diff --git a/vlib/v/preludes/test_runner_teamcity.v b/vlib/v/preludes/test_runner_teamcity.v index f801991740..5231d986ac 100644 --- a/vlib/v/preludes/test_runner_teamcity.v +++ b/vlib/v/preludes/test_runner_teamcity.v @@ -1,6 +1,7 @@ module main import time +import term // Provide a Teamcity implementation of the TestRunner interface. // Used in Teamcity and JetBrains IDEs for nice test reporting. @@ -20,6 +21,7 @@ mut: fn_passes u64 fn_fails u64 + assertion_info VAssertMetaInfo total_assert_passes u64 total_assert_fails u64 } @@ -58,8 +60,15 @@ fn (mut runner TeamcityTestRunner) fn_start() bool { runner.fn_assert_passes = 0 runner.start_time = time.now() runner.fname = normalise_fname(runner.fn_test_info.name) - println("Start '${runner.fname}'") - eprintln("##teamcity[testStarted name='${runner.fname}' locationHint='v_qn://${runner.file_test_info.file}:${runner.fname}']") + + msg := "Start '${runner.fname}' test" + println(term.gray(msg)) + + runner.print_service(" + |##teamcity[ + |testStarted name='${runner.fname}' + |locationHint='v_qn://${runner.file_test_info.file}:${runner.fname}' + |]".strip_margin()) return true } @@ -67,16 +76,78 @@ fn (mut runner TeamcityTestRunner) fn_pass() { runner.fn_passes++ duration := runner.test_duration() eprintln("##teamcity[testFinished name='${runner.fname}' duration='${duration}']") + end_msg := "Finish '${runner.fname}' test" + println(term.gray(end_msg)) + msg := "Test '${runner.fname}' passed" + println(term.green(msg)) println('\n') } fn (mut runner TeamcityTestRunner) fn_fail() { runner.fn_fails++ duration := runner.test_duration() - eprintln("##teamcity[testFailed name='${runner.fname}' duration='${duration}' message='assertion failed']") + assertion := runner.assertion_info + + mut actual := runner.prepare_value(assertion.lvalue) + mut expected := runner.prepare_value(assertion.rvalue) + + message := if assertion.has_msg { + 'Assertion "${assertion.message}" failed' + } else { + op := if assertion.op == '' { '' } else { assertion.op } + + if op == 'in' { + parts := assertion.src.split('in') + if parts.len == 2 { + left := parts[0].trim(' ') + right := parts[1].trim(' ') + 'Assertion that "${left}" in "${right}" failed' + } else { + 'Assertion "${assertion.src}" failed' + } + } else if op == 'is' { + 'Assertion that left and right type are equal failed' + } else if op == 'call' { + actual = 'false' + expected = 'true' + 'Assertion that function call "${assertion.src}" returns true failed' + } else { + lines := assertion.src.split_into_lines() + if lines.len == 1 { + 'Assertion "${lines.first()}" failed' + } else { + 'Assertion failed' + } + } + } + + details := 'See failed assertion: ${assertion.fpath}:${assertion.line_nr + 1}' + + runner.print_service(" + |##teamcity[ + |testFailed name='${runner.fname}' + |duration='${duration}' + |details='${details}' + |type='comparisonFailure' + |actual='${actual}' + |expected='${expected}' + |message='${message}' + |]".strip_margin()) println('\n') } +// prepare_value escapes the value for Teamcity output. +// For example, it replaces `\n` with `|n`, otherwise Teamcity +// will not correctly parse the output. +fn (mut _ TeamcityTestRunner) prepare_value(raw string) string { + return raw + .replace('\n', '|n') + .replace('\r', '|r') + .replace('[', '|[') + .replace(']', '|]') + .replace("'", "|'") +} + fn (mut runner TeamcityTestRunner) fn_error(line_nr int, file string, mod string, fn_name string, errmsg string) { eprintln('>>> TeamcityTestRunner fn_error ${runner.fname}, line_nr: ${line_nr}, file: ${file}, mod: ${mod}, fn_name: ${fn_name}, errmsg: ${errmsg}') } @@ -85,6 +156,15 @@ fn (mut runner TeamcityTestRunner) test_duration() i64 { return time.now().unix_time_milli() - runner.start_time.unix_time_milli() } +// print_service prepare and prints a Teamcity service message. +fn (mut runner TeamcityTestRunner) print_service(msg string) { + without_new_lines := msg + .trim('\n\r ') + .replace('\r', '') + .replace('\n', ' ') + eprintln(without_new_lines) +} + // fn (mut runner TeamcityTestRunner) assert_pass(i &VAssertMetaInfo) { @@ -92,45 +172,13 @@ fn (mut runner TeamcityTestRunner) assert_pass(i &VAssertMetaInfo) { runner.fn_assert_passes++ filepath := i.fpath.clone() - println('>>> assert_pass ${filepath}:${i.line_nr + 1}') + msg := '>>> assertion passed ${filepath}:${i.line_nr + 1}' + println(term.green(msg)) unsafe { i.free() } } fn (mut runner TeamcityTestRunner) assert_fail(i &VAssertMetaInfo) { runner.total_assert_fails++ - - filepath := i.fpath.clone() - mut final_filepath := filepath + ':${i.line_nr + 1}:' - mut final_funcname := 'fn ' + i.fn_name.replace('main.', '').replace('__', '.') - final_src := 'assert ' + i.src - eprintln('${final_filepath} ${final_funcname}') - if i.op.len > 0 && i.op != 'call' { - mut lvtitle := ' Left value:' - mut rvtitle := ' Right value:' - mut slvalue := '${i.lvalue}' - mut srvalue := '${i.rvalue}' - cutoff_limit := 30 - if slvalue.len > cutoff_limit || srvalue.len > cutoff_limit { - eprintln(' > ${final_src}') - eprintln(lvtitle) - eprintln(' ${slvalue}') - eprintln(rvtitle) - eprintln(' ${srvalue}') - } else { - eprintln(' > ${final_src}') - eprintln(' ${lvtitle} ${slvalue}') - eprintln('${rvtitle} ${srvalue}') - } - } else { - eprintln(' ${final_src}') - } - if i.has_msg { - mut mtitle := ' Message:' - mut mvalue := '${i.message}' - eprintln('${mtitle} ${mvalue}') - } - eprintln('') - - unsafe { i.free() } + runner.assertion_info = *i }