Skip to content

Commit

Permalink
Add reporting of unit tests results (#5426)
Browse files Browse the repository at this point in the history
This change adds reporting of unit tests to our PRs using an external
github action. The action will create a test run summary as well as
annoations (errors and warnings) to failing unit tests.

See an example of this in action here:

  #5422

There are three parts to this PR:

- Producing the jUnit XML file format (gotestsum)
- Working around bug in file/line number reporting
- Processing and uploading results

The main that needs commentary is the integration with gotestsum. This
is a tool I've been using for a while now because it provides a better
local experience for understanding test failures. This PR integrates it
as an OPTIONAL dependency. Our various `make test` commands know how to
use gotestsum if its present and fall back to `go test` otherwise.
gotestsum is compatible (mostly) with `go test` so the only change in
behavior is the reporting.

However, there's a bug where the test report is missing file/line
number. The GH action I'm using doesn't understand how to parse the
output of `go test` and retrieve this info, but the file format we're
using (jUnit XML) supports it. I wrote a basic script to address this.
Potentially this could be fixed inside gotestsum.

I looked at a few different github actions and this one seemed like it
has the best features and is actively maintained. In general they all
work with the jUnit format so we're not getting locked in.
  • Loading branch information
rynowak authored Apr 28, 2023
1 parent ff2ceae commit 42a281c
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 14 deletions.
68 changes: 68 additions & 0 deletions .github/actions/process-test-results/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: "Process Test Results"
description: |
Processes a batch of test results including creating a github artifact and annotating the Pull Request (if applicable).
This requires using 'gotestum' to run tests, which our makefile can do automatically. Install 'gotestsum' using:
go install gotest.tools/[email protected]
You will also need to set the path for the results file using an environment variable. This would output the jUnit
test results format which is what we require.
GOTESTSUM_OPTS: '--junitfile ./dist/unit_test_results_raw.xml'
Then running 'make <test target>' will do the right thing :)
inputs:
test_group_name:
description: 'Name to use for reporting (eg: Unit Tests)'
required: true
artifact_name:
description: 'Name to use for uploading artifacts (eg: unit_test_results)'
required: true
result_directory:
description: 'Directory containing result XML files. These should be in jUnit format. See the description of the action.'
required: true
runs:
using: "composite"
steps:
# The test results file output by gotestsum is missing file and line number on the XML elements
# which is needed for the annotations to work. This script adds the missing information.
- name: 'Transform ${{ inputs.test_group_name }} Results'
# Always is REQUIRED here. Otherwise, the action will be skipped when the unit tests fail, which
# defeats the purpose. YES it is counterintuitive. This applies to all of the actions in this file.
if: always()
id: 'process_files'
shell: 'bash'
working-directory: ${{ github.workspace }}
env:
INPUT_DIRECTORY: ${{ inputs.result_directory }}
run: |
echo "repository root is $GITHUB_WORKSPACE"
INPUT_FILES="$INPUT_DIRECTORY*.xml"
mkdir -p "$INPUT_DIRECTORY/processed"
for INPUT_FILE in $INPUT_FILES
do
DIRECTORY=$(dirname -- "$INPUT_FILE")
FILENAME=$(basename -- "$INPUT_FILE")
FILENAME="${FILENAME%.*}"
OUTPUT_FILE="${DIRECTORY}/processed/${FILENAME}.xml"
echo "processing test results in $INPUT_FILE to add line and file info..."
python3 ./.github/scripts/transform_test_results.py $GITHUB_WORKSPACE "$INPUT_FILE" "$OUTPUT_FILE"
echo "wrote ${OUTPUT_FILE}"
done
- name: 'Create ${{ inputs.test_group_name }} Result Report'
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: |
${{ inputs.result_directory }}/processed/*.xml
- name: 'Upload ${{ inputs.test_group_name }} Results'
uses: actions/upload-artifact@v3
if: always()
with:
name: ${{ inputs.artifact_name }}
path: |
${{ inputs.result_directory }}/*.xml
${{ inputs.result_directory }}processed/*.xml
54 changes: 54 additions & 0 deletions .github/scripts/transform_test_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# parse an xml file and transform it into a junit xml file
# that can be used by the github actions junit reporter
# Path: .github/scripts/transform-test-results.py

import re
import sys
import xml.etree.ElementTree

def main():
if len(sys.argv) != 4:
print("Usage: transform-test-results.py <repository root> <input file> <output file>")
sys.exit(1)

repository_root = sys.argv[1]
input_file = sys.argv[2]
output_file = sys.argv[3]

print(f"Processing {input_file}")
pattern = re.compile(r"\tError Trace:\t(.*):(\d+)")
et = xml.etree.ElementTree.parse(input_file)
for testcase in et.findall('./testsuite/testcase'):
failure = testcase.find('./failure')
if failure is None:
continue

# Extract file name by matching regex pattern in the text
# it will look like \tError Trace:\tfilename:line
match = pattern.search(failure.text)
if match is None:
continue

file = match.group(1)
line = match.group(2)

# The filename will contain the fully-qualified path, and we need to turn that into
# a relative path from the repository root
if not file.startswith(repository_root):
print(f"Could not find repository name in file path: {file}")
continue

file = file[len(repository_root) + 1:]

testcase.attrib["file"] = file
testcase.attrib["line"] = line
failure.attrib["file"] = file
failure.attrib["line"] = line


# Write back to file
print(f"Writing {output_file}")
et.write(output_file)

if __name__ == "__main__":
main()
25 changes: 18 additions & 7 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ concurrency:

env:
GOVER: '^1.20'
GOTESTSUMVERSION: 1.10.0
# Use radiusdev.azurecr.io for PR build. Otherwise, use radius.azurecr.io.
DOCKER_REGISTRY: ${{ github.event.pull_request.number && 'radiusdev.azurecr.io' || 'radius.azurecr.io' }}

Expand Down Expand Up @@ -83,9 +84,26 @@ jobs:
- name: Run make test (unit tests)
if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux'
env:
GOTESTSUM_OPTS: '--junitfile ./dist/unit_test/results.xml'
GOTEST_OPTS: '-race -coverprofile ./dist/ut_coverage_orig.out'
run: |
go install gotest.tools/gotestsum@v${{ env.GOTESTSUMVERSION }}
make test
- name: Process Unit Test Results
uses: ./.github/actions/process-test-results
# Always is required here to make sure this target runs even when tests fail.
if: always() && matrix.target_arch == 'amd64' && matrix.target_os == 'linux'
with:
test_group_name: 'Unit Tests'
artifact_name: 'unit_test_results'
result_directory: 'dist/unit_test/'
- name: Upload CLI binary
uses: actions/upload-artifact@v3
with:
name: rad_cli_${{ matrix.target_os}}_${{ matrix.target_arch}}
path: |
./dist/${{ matrix.target_os}}_${{ matrix.target_arch}}/release/rad
./dist/${{ matrix.target_os}}_${{ matrix.target_arch}}/release/rad.exe
- name: Generate unit-test coverage files
if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux'
run: |
Expand Down Expand Up @@ -122,13 +140,6 @@ jobs:
env:
COVERAGE_FILE: ./dist/ut_coverage.out
GO_TOOL_COVER: go tool cover
- name: Upload CLI binary
uses: actions/upload-artifact@v3
with:
name: rad_cli_${{ matrix.target_os}}_${{ matrix.target_arch}}
path: |
./dist/${{ matrix.target_os}}_${{ matrix.target_arch}}/release/rad
./dist/${{ matrix.target_os}}_${{ matrix.target_arch}}/release/rad.exe
- name: Upload unit-test coverage artifact
if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux'
uses: actions/upload-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
ARROW := \033[34;1m=>\033[0m

# order matters for these
include build/help.mk build/version.mk build/build.mk build/util.mk build/generate.mk build/test.mk build/controller.mk build/docker.mk build/install.mk
include build/help.mk build/version.mk build/build.mk build/util.mk build/generate.mk build/test.mk build/controller.mk build/docker.mk build/install.mk build/debug.mk
13 changes: 13 additions & 0 deletions build/debug.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ------------------------------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------------------------------

##@ Debugging

.PHONY: dump
dump: ## Outputs the values of all variables in the makefile.
$(foreach v, \
$(shell echo "$(filter-out .VARIABLES,$(.VARIABLES))" | tr ' ' '\n' | sort), \
$(info $(shell printf "%-20s" "$(v)")= $(value $(v))) \
)
29 changes: 23 additions & 6 deletions build/test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,46 @@ ENVTEST_ASSETS_DIR=$(shell pwd)/bin
K8S_VERSION=1.23.*
ENV_SETUP=$(GOBIN)/setup-envtest$(BINARY_EXT)

# Use gotestsum if available, otherwise use go test. We want to enable testing with just 'make test'
# without external dependencies, but want to use gotestsum in our CI pipelines for the improved
# reporting.
#
# See: https://github.com/gotestyourself/gotestsum
#
# Gotestsum is a drop-in replacement for go test, but it provides a much nicer formatted output
# and it can also generate JUnit XML reports.
ifeq (, $(shell which gotestsum))
GOTEST_TOOL ?= go test
else
# Use these options by default but allow an override via env-var
GOTEST_OPTS ?=
# We need the double dash here to separate the 'gotestsum' options from the 'go test' options
GOTEST_TOOL ?= gotestsum $(GOTESTSUM_OPTS) --
endif

.PHONY: test
test: test-get-envtools ## Runs unit tests, excluding kubernetes controller tests
KUBEBUILDER_ASSETS="$(shell $(ENV_SETUP) use -p path ${K8S_VERSION} --arch amd64)" CGO_ENABLED=1 go test -v ./pkg/... $(GOTEST_OPTS)
KUBEBUILDER_ASSETS="$(shell $(ENV_SETUP) use -p path ${K8S_VERSION} --arch amd64)" CGO_ENABLED=1 $(GOTEST_TOOL) -v ./pkg/... $(GOTEST_OPTS)

.PHONY: test-get-envtools
test-get-envtools:
$(call go-install-tool,$(ENV_SETUP),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)

.PHONY: test-validate-cli
test-validate-cli: ## Run cli integration tests
CGO_ENABLED=1 go test -coverpkg= ./pkg/cli/cmd/... ./cmd/rad/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)
CGO_ENABLED=1 $(GOTEST_TOOL) -coverpkg= ./pkg/cli/cmd/... ./cmd/rad/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)

test-functional-kubernetes: ## Runs Kubernetes functional tests
CGO_ENABLED=1 go test ./test/functional/kubernetes/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)
CGO_ENABLED=1 $(GOTEST_TOOL) ./test/functional/kubernetes/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)

test-functional-corerp: ## Runs Applications.Core functional tests
CGO_ENABLED=1 go test ./test/functional/corerp/... -timeout ${TEST_TIMEOUT} -v -parallel 10 $(GOTEST_OPTS)
CGO_ENABLED=1 $(GOTEST_TOOL) ./test/functional/corerp/... -timeout ${TEST_TIMEOUT} -v -parallel 10 $(GOTEST_OPTS)

test-functional-samples: ## Runs Samples functional tests
CGO_ENABLED=1 go test ./test/functional/samples/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)
CGO_ENABLED=1 $(GOTEST_TOOL) ./test/functional/samples/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)

test-functional-ucp: ## Runs UCP functional tests
CGO_ENABLED=1 go test ./test/functional/ucp/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)
CGO_ENABLED=1 $(GOTEST_TOOL) ./test/functional/ucp/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS)

test-validate-bicep: ## Validates that all .bicep files compile cleanly
BICEP_PATH="${HOME}/.rad/bin" ./build/validate-bicep.sh
Expand Down

0 comments on commit 42a281c

Please sign in to comment.