Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/run results v6 #8

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@
Convert your dbt test results into jUnit XML format so that CI/CD platforms (such as Jenkins, CircleCI, etc.)
can better report on tests in their UI.

## About this fork

This is the fork repository based on https://github.com/chasleslr/dbt-junitxml/ version 0.1.5
On top of that here were added:
1. Support of DBT Core 1.3+ (originally it supported only up to 1.2). Versions 0.2.x Tested on DBT 1.5
2. In case of test failures Junit XML contains additional information regarding Stored Results and original test SQL. Details can be found below.
3. Test name in the resulted xml is more specific rather than in original version .
4. Supported integration with https://reportportal.io/

## Installation

Publishing as a regular pip module is considered

```shell
pip install dbt-junitxml
pip install "git+https://github.com/SOVALINUX/dbt-junitxml@0.2.1#egg=dbt-junitxml"
```

We recommend you to stick to some specific version, since newer versions might contain changes that may impact your operations (not being backward incompatible at all, but rather change some visualizations you might be used to).

## Usage

Expand All @@ -19,6 +31,54 @@ to parse your run results and output a jUnit XML formatted report named `report.
dbt-junitxml parse target/run_results.json report.xml
```

## Features description

### Rich XML output in case of test failure

In order to help you handle test failures right where you see it we're adding supporting information into Junit XML in case of test failure
It's even more than you see in the DBT CLI console output!
For example:

```
Got 19 results, configured to fail if != 0
2023-06-08 10:47:02
------------------------------------------------------------------------------------------------
select * from db_dbt_test__audit.not_null_table_reporter_employee_id
------------------------------------------------------------------------------------------------

select *
from (select * from "datacatalog"."db"."table" where NOT regexp_like(reporter_email_address, 'auto_.*[email protected]') AND reporter_email_address NOT IN ('[email protected]') AND reporter_email_address IS NOT NULL) dbt_subquery
where reporter_employee_id is null
```

### Saving test SQL files for further analysis

Sometimes it's handy to see the exact SQL that was executed and tested by DBT without repeating compilation steps.
To achieve it we suggest you to save compiled tests SQL during your test run.
Below you can find a reference script:
```shell
dbt test --store-failures
mkdir -p target/compiled_all_sql && find target/compiled/ -name *.sql -print0 | xargs -0 cp -t target/compiled_all_sql/
zip -r -q compiled_all_sql.zip target/compiled_all_sql
```

### Integration with Report Portal

https://reportportal.io/ helps you to manage your test launches. Here at EPAM we're using this tool to manage over 4,000 DBT tests

In order to upload your test run to reportportal you can use the following script:
```shell
dbt-junitxml parse target/run_results.json target/manifest.json dbt_test_report.xml
zip dbt_test_report.zip dbt_test_report.xml
REPORT_PORTAL_TOKEN=`Your token for Report Portal`
RESPONSE=`curl -X POST "https://reportportal.io/api/v1/epm-plxd/launch/import" -H "accept: */*" -H "Content-Type: multipart/form-data" -H "Authorization: bearer ${REPORT_PORTAL_TOKEN}" -F "file=@dbt_test_report.zip;type=application/x-zip-compressed"`
LAUNCH_ID=`echo "${RESPONSE}" | sed 's/.*Launch with id = \(.*\) is successfully imported.*/\1/'`
```

## Limitations

Currently, only v4 of the [Run Results](https://docs.getdbt.com/reference/artifacts/run-results-json) specifications is supported.

## Contribution

Development of this fork was partially sponsored by EPAM Systems Inc. https://www.epam.com/
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[tool.poetry]
name = "dbt-junitxml"
version = "0.0.0"
description = ""
authors = ["Charles Lariviere <[email protected]>"]
version = "0.2.1"
description = "Utility to convert DBT test results into Junit XML format"
authors = ["Charles Lariviere <[email protected]>", "Siarhei Nekhviadovich <[email protected]>", "Aliaksandra Sidarenka <[email protected]>"]
readme = "README.md"
license = "MIT"
repository = "https://github.com/chasleslr/dbt-junitxml"
Expand Down
Empty file added src/dbt_junitxml/__init__.py
Empty file.
229 changes: 229 additions & 0 deletions src/dbt_junitxml/dbt_junit_xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
from junit_xml import TestSuite, TestCase, decode
import xml.etree.ElementTree as ET


class DBTTestCase(TestCase):
"""A JUnit test case with a result and possibly some stdout or stderr"""

def __init__(
self,
name,
classname=None,
elapsed_sec=None,
stdout=None,
stderr=None,
assertions=None,
timestamp=None,
status=None,
category=None,
file=None,
line=None,
log=None,
url=None,
allow_multiple_subelements=False,
):
self.name = name
self.assertions = assertions
self.elapsed_sec = elapsed_sec
self.timestamp = timestamp
self.classname = classname
self.status = status
self.category = category
self.file = file
self.line = line
self.log = log
self.url = url
self.stdout = stdout
self.stderr = stderr

self.is_enabled = True
self.errors = []
self.failures = []
self.skipped = []
self.allow_multiple_subalements = allow_multiple_subelements


class DBTTestSuite(TestSuite):
def __init__(self,
name,
test_cases=None,
hostname=None,
id=None,
package=None,
timestamp=None,
properties=None,
file=None,
log=None,
url=None,
stdout=None,
stderr=None,
time=None):
super(DBTTestSuite, self).__init__(name,
test_cases=None,
hostname=None,
id=None,
package=None,
timestamp=None,
properties=None,
file=None,
log=None,
url=None,
stdout=None,
stderr=None)
self.name = name
if not test_cases:
test_cases = []
try:
iter(test_cases)
except TypeError:
raise TypeError("test_cases must be a list of test cases")
self.test_cases = test_cases
self.timestamp = timestamp
self.hostname = hostname
self.id = id
self.package = package
self.file = file
self.log = log
self.url = url
self.stdout = stdout
self.stderr = stderr
self.properties = properties
self.time = time

def build_xml_doc(self, encoding=None):
super(DBTTestSuite, self).build_xml_doc(encoding=None)
"""
Builds the XML document for the JUnit test suite.
Produces clean unicode strings and decodes non-unicode with the help of encoding.
@param encoding: Used to decode encoded strings.
@return: XML document with unicode string elements
"""

# build the test suite element
test_suite_attributes = dict()
if any(c.assertions for c in self.test_cases):
test_suite_attributes["assertions"] = str(
sum([int(c.assertions) for c in self.test_cases if c.assertions]))
test_suite_attributes["disabled"] = str(
len([c for c in self.test_cases if not c.is_enabled]))
test_suite_attributes["errors"] = str(len([c for c in self.test_cases if c.is_error()]))
test_suite_attributes["failures"] = str(len([c for c in self.test_cases if c.is_failure()]))
test_suite_attributes["name"] = decode(self.name, encoding)
test_suite_attributes["skipped"] = str(len([c for c in self.test_cases if c.is_skipped()]))
test_suite_attributes["tests"] = str(len(self.test_cases))
test_suite_attributes["time"] = str(
sum(c.elapsed_sec for c in self.test_cases if c.elapsed_sec))

if self.hostname:
test_suite_attributes["hostname"] = decode(self.hostname, encoding)
if self.id:
test_suite_attributes["id"] = decode(self.id, encoding)
if self.package:
test_suite_attributes["package"] = decode(self.package, encoding)
if self.timestamp:
test_suite_attributes["timestamp"] = decode(self.timestamp, encoding)
if self.file:
test_suite_attributes["file"] = decode(self.file, encoding)
if self.log:
test_suite_attributes["log"] = decode(self.log, encoding)
if self.url:
test_suite_attributes["url"] = decode(self.url, encoding)
if self.time:
test_suite_attributes["time"] = decode(self.time, encoding)

xml_element = ET.Element("testsuite", test_suite_attributes)

# add any properties
if self.properties:
props_element = ET.SubElement(xml_element, "properties")
for k, v in self.properties.items():
attrs = {"name": decode(k, encoding), "value": decode(v, encoding)}
ET.SubElement(props_element, "property", attrs)

# add test suite stdout
if self.stdout:
stdout_element = ET.SubElement(xml_element, "system-out")
stdout_element.text = decode(self.stdout, encoding)

# add test suite stderr
if self.stderr:
stderr_element = ET.SubElement(xml_element, "system-err")
stderr_element.text = decode(self.stderr, encoding)

# test cases
for case in self.test_cases:
test_case_attributes = dict()
test_case_attributes["name"] = decode(case.name, encoding)
if case.assertions:
# Number of assertions in the test case
test_case_attributes["assertions"] = "%d" % case.assertions
if case.elapsed_sec:
test_case_attributes["time"] = "%f" % case.elapsed_sec
if case.timestamp:
test_case_attributes["timestamp"] = decode(case.timestamp, encoding)
if case.classname:
test_case_attributes["classname"] = decode(case.classname, encoding)
if case.status:
test_case_attributes["status"] = decode(case.status, encoding)
if case.category:
test_case_attributes["class"] = decode(case.category, encoding)
if case.file:
test_case_attributes["file"] = decode(case.file, encoding)
if case.line:
test_case_attributes["line"] = decode(case.line, encoding)
if case.log:
test_case_attributes["log"] = decode(case.log, encoding)
if case.url:
test_case_attributes["url"] = decode(case.url, encoding)

test_case_element = ET.SubElement(xml_element, "testcase", test_case_attributes)

# failures
for failure in case.failures:
if failure["output"] or failure["message"]:
attrs = {"type": "failure"}
if failure["message"]:
attrs["message"] = decode(failure["message"], encoding)
if failure["type"]:
attrs["type"] = decode(failure["type"], encoding)
failure_element = ET.Element("failure", attrs)
if failure["output"]:
failure_element.text = decode(failure["output"], encoding)
test_case_element.append(failure_element)

# errors
for error in case.errors:
if error["message"] or error["output"]:
attrs = {"type": "error"}
if error["message"]:
attrs["message"] = decode(error["message"], encoding)
if error["type"]:
attrs["type"] = decode(error["type"], encoding)
error_element = ET.Element("error", attrs)
if error["output"]:
error_element.text = decode(error["output"], encoding)
test_case_element.append(error_element)

# skipped
for skipped in case.skipped:
attrs = {"type": "skipped"}
if skipped["message"]:
attrs["message"] = decode(skipped["message"], encoding)
skipped_element = ET.Element("skipped", attrs)
if skipped["output"]:
skipped_element.text = decode(skipped["output"], encoding)
test_case_element.append(skipped_element)

# test stdout
if case.stdout:
stdout_element = ET.Element("system-out")
stdout_element.text = decode(case.stdout, encoding)
test_case_element.append(stdout_element)

# test stderr
if case.stderr:
stderr_element = ET.Element("system-err")
stderr_element.text = decode(case.stderr, encoding)
test_case_element.append(stderr_element)

return xml_element
Loading