chore(linux): add TC test runner

This adds a modified version of JetBrains' TeamCity test runner that
makes use of the `parent` property in the service messages and thus
allows to properly indent and fold the block that contains the test
output.

Test-bot: skip
This commit is contained in:
Eberhard Beilharz 2025-05-28 18:00:53 +02:00 committed by Eberhard Beilharz
parent e0dc544bd4
commit 581e4e03ef
5 changed files with 103 additions and 11 deletions

View file

@ -1,5 +1,5 @@
#!/bin/bash
PYTHONPATH=.:${PYTHONPATH}
export PYTHONPATH=.:${PYTHONPATH}
XDG_CONFIG_HOME=$(mktemp --directory)
export XDG_CONFIG_HOME
@ -13,6 +13,8 @@ if [[ "$1" == "--coverage" ]]; then
fi
if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
PYTHONPATH=$(dirname "$0")/../tools:${PYTHONPATH}
if ! pip3 list --format=columns | grep -q teamcity-messages; then
if [[ -n "${TEAMCITY_PLATFORM}" ]] || [[ -n "${DOCKER_RUNNING}" ]]; then
# Ubuntu 24.04+ prevents mixing pip and system packages and wants us
@ -29,7 +31,9 @@ if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
# shellcheck disable=SC2086
pip3 install --user ${PIP_ARGS:-} teamcity-messages
fi
test_module=teamcity.unittestpy
test_module=teamcity_testrunner.unittestpy
echo "##teamcity[testStarted name='|[keyman-config|] Running unit tests']"
echo "##teamcity[flowStarted flowId='unit_tests']"
else
test_module=unittest
extra_opts=-v
@ -38,4 +42,8 @@ fi
# shellcheck disable=SC2086
python3 ${coverage:-} -m "${test_module:-}" discover ${extra_opts:-} -s tests/ -p "*_tests.py"
if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
echo "##teamcity[flowFinished flowId='unit_tests']"
echo "##teamcity[testFinished name='|[keyman-config|] Finished running unit tests']"
fi
rm -rf "${XDG_CONFIG_HOME}"

View file

@ -0,0 +1,12 @@
# Teamcity Test Runner
This implements an improved version of JetBrains' unittest runner for
Teamcity. It makes the output of the tests indented and collapsible under
the test block by specifying a parent flow.
Requires the `teamcity-messages` package.
## References
- <https://www.jetbrains.com/help/teamcity/service-messages.html#Message+FlowId>
- <https://github.com/JetBrains/teamcity-messages>

View file

@ -0,0 +1,61 @@
import sys
from teamcity.messages import TeamcityServiceMessages
from teamcity.unittestpy import TeamcityTestResult, TeamcityTestRunner
from unittest import main
class KeymanTeamcityServiceMessages(TeamcityServiceMessages):
def __init__(self, parent_flow='unit_tests'):
self.parent_flow = parent_flow
super().__init__()
def testStarted(self, testName, captureStandardOutput=None, flowId=None, metainfo=None):
self.message('testStarted', name=testName, metainfo=metainfo)
self.message('flowStarted', flowId=flowId, parent=self.parent_flow)
def testFinished(self, testName, testDuration=None, flowId=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
if testDuration is not None:
duration_ms = testDuration.days * 86400000 + \
testDuration.seconds * 1000 + \
int(testDuration.microseconds / 1000)
self.message('testFinished', name=testName, duration=str(duration_ms))
else:
self.message('testFinished', name=testName)
def testIgnored(self, testName, message='', flowId=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
self.message('testIgnored', name=testName, message=message)
def testFailed(self, testName, message='', details='', flowId=None, comparison_failure=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
if not comparison_failure:
self.message('testFailed', name=testName, message=message, details=details)
else:
diff_message = u"\n{0} != {1}\n".format(comparison_failure.actual, comparison_failure.expected)
self.message(
'testFailed',
name=testName,
message=str(message) + diff_message,
details=details,
type="comparisonFailure",
actual=comparison_failure.actual,
expected=comparison_failure.expected,
)
class KeymanTestResult(TeamcityTestResult):
def __init__(self, stream=sys.stdout, descriptions=None, verbosity=None):
super().__init__(stream, descriptions, verbosity)
self.messages = KeymanTeamcityServiceMessages()
class KeymanTestRunner(TeamcityTestRunner):
resultclass = KeymanTestResult
def run(self, test):
return super().run(test)
if __name__ == '__main__':
main(module=None, testRunner=KeymanTestRunner())

View file

@ -181,28 +181,30 @@ function builder_heading() {
builder_echo() {
local color=white message= mark= block= action= do_output=true
local color=white message= mark= block= action= do_output=true test=
local echo_target=echo
if [[ $# -gt 1 ]]; then
if [[ $1 =~ ^(white|grey|green|success|blue|heading|yellow|warning|red|error|purple|brightwhite|teal|debug|setmark)$ ]]; then
color="$1"
shift
elif [[ $1 == "start" ]]; then
elif [[ $1 == "start" ]] || [[ $1 == "startTest" ]]; then
# builder_echo start block message
action="$1"
test="$1"
block="$2"
shift 2
action="start"
color="heading"
if ! builder_is_running_on_teamcity && builder_is_child_build; then
do_output=${_builder_debug_internal:-false}
fi
elif [[ $1 == "end" ]]; then
elif [[ $1 == "end" ]] || [[ $1 == "endTest" ]]; then
# builder_echo end block status message
action="$1"
test="$1"
block="$2"
color="$3"
shift 3
action="end"
if [[ "${color}" != "error" ]] && ! builder_is_running_on_teamcity && builder_is_child_build; then
do_output=${_builder_debug_internal:-false}
fi
@ -211,7 +213,11 @@ builder_echo() {
message="$*"
if [[ "${action}" == "start" ]] && builder_is_running_on_teamcity; then
$echo_target -e "##teamcity[blockOpened name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
if [[ "${test}" == "startTest" ]]; then
$echo_target -e "##teamcity[testSuiteStarted name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
else
$echo_target -e "##teamcity[blockOpened name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
fi
fi
if ${do_output}; then
@ -241,7 +247,11 @@ builder_echo() {
fi
if [[ "${action}" == "end" ]] && builder_is_running_on_teamcity; then
$echo_target -e "##teamcity[blockClosed name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
if [[ "${test}" == "endTest" ]]; then
$echo_target -e "##teamcity[testSuiteFinished name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
else
$echo_target -e "##teamcity[blockClosed name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
fi
fi
}

View file

@ -47,7 +47,8 @@ linux_build_action() {
# Run unit tests for Keyman for Linux.
linux_unit_tests_action() {
builder_echo start unit_tests "Running unit tests"
builder_echo startTest unit_tests "Running unit tests"
rm -f /tmp/ibus-engine-keyman.log
rm -f /tmp/ibus-daemon.log
# symlink might point to wrong location, so delete it - will be re-created during tests
@ -56,6 +57,6 @@ linux_unit_tests_action() {
export NO_AT_BRIDGE=1
# shellcheck disable=SC2068
"${KEYMAN_ROOT}/linux/build.sh" test $@
builder_echo end unit_tests success "Finished running unit tests"
builder_echo endTest unit_tests success "Finished running unit tests"
}