Skip to content

Commit

Permalink
Initial JSON Parser implementation (useblocks#65)
Browse files Browse the repository at this point in the history
* Initial JSON Parser implementation (80% done)

* Uses JSON parser if file has json extension
* test and docs

* doc fixes

* Linter problems fixed

* Updated font

* Added testcases for the :ref:`json_parser` and updated docs.

* Updated `parse_testsuite()` and `parse_testcase()` to read dict-keys from `tr_json_mapping` config value.

* Bugfix and complex json test

---------

Co-authored-by: Duodu Randy <[email protected]>
  • Loading branch information
danwos and iSOLveIT authored Jul 3, 2023
1 parent ebe4680 commit 7cab1e1
Show file tree
Hide file tree
Showing 29 changed files with 1,094 additions and 36 deletions.
2 changes: 1 addition & 1 deletion docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = sphinx-test-reports
SOURCEDIR = .
BUILDDIR = build
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
Expand Down
5 changes: 4 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ Changelog
-----
:Released: under development

* Added testcases for the :ref:`json_parser` and updated docs.
* Updated project documentation to use the Useblocks theme customization.
* Improvement: Template file encoding coud be configured. See :ref:`tr_import_encoding`.
* Improvement: Template file encoding could be configured. See :ref:`tr_import_encoding`.
`#60 <https://github.com/useblocks/sphinx-test-reports/issues/60>`_
* Improvement: Supporting JSON files containing test results: :ref:`json_parser`.
* Improvement: Implemented :ref:`tr_json_mapping` config option for JSON mapping.

1.0.2
-----
Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
other_options = {
"repo_url": "https://github.com/useblocks/sphinx-test-reports",
"repo_name": "sphinx-test-reports",
"repo_type": "github",
}
html_theme_options.update(other_options)
html_theme_options["features"].extend(["navigation.tabs", "navigation.tabs.sticky"])
Expand Down
80 changes: 79 additions & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,87 @@ Default: **5**
.. _tr_import_encoding:

tr_import_encoding
__________________
------------------
.. versionadded:: 1.0.3

Defines the encoding for imported files, e.g. in custom templates.

Default: **utf8**

.. _tr_json_mapping:

tr_json_mapping
---------------
.. versionadded:: 1.0.3

Takes a mapping configuration, which defines how to map the JSON structure to the internal structure used by
``Sphinx-Test-Reports``.

``tr_json_mapping`` is a dictionary, where the first key is a name for the configuration.
The name is currently just a placeholder and the first config is used for all JSON imports.

Two mappings must be configured as dictionary, one for ``testsuite`` and one for the nested ``testcase``.

The key of this dictionary elements is the **internal** name and fix.

The value is a tuple, containing a **selector list** and a **default value**, if the selector does not find any data.

The **selector** is a list, where each entry is representing one level of the data structure.
If the entry is a string, it is used as a key for a dict. If it is a integer number, it is taken as position
of a list.

**JSON example**

.. code-block:: python
{
"level_1": {
"level_2": [
{"value": "Hello!"}
{"value": "Bye Bye!"}
]
}
}
Given the above JSON example, the following "selector" will address the value ``Bye Bye!``::

["level_1", "level_2", 1, "value"]


**Example config**

This example contains **all** internal elements and a mapping as example.
For ``testsuite`` the value ``testcases`` defines the location of nested testcases.

An example of a JSON file, which supports the below configuration, can be seen in :ref:`json_example`.

.. code-block:: python
tr_json_mapping = {
"json_config_1": {
"testsuite": {
"name": (["name"], "unknown"),
"tests": (["tests"], "unknown"),
"errors": (["errors"], "unknown"),
"failures": (["failures"], "unknown"),
"skips": (["skips"], "unknown"),
"passed": (["passed"], "unknown"),
"time": (["time"], "unknown"),
"testcases": (["testcase"], "unknown"),
},
"testcase": {
"name": (["name"], "unknown"),
"classname": (["classname"], "unknown"),
"file": (["file"], "unknown"),
"line": (["line"], "unknown"),
"time": (["time"], "unknown"),
"result": (["result"], "unknown"),
"type": (["type"], "unknown"),
"text": (["text"], "unknown"),
"message": (["message"], "unknown"),
"system-out": (["system-out"], "unknown"),
}
}
}
33 changes: 29 additions & 4 deletions docs/examples/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
Examples
========
.. contents:: Contents
:local:

.. _json_example:

JSON data example
-----------------
Data:

.. literalinclude:: ../../tests/doc_test/utils/json_data.json

.. code-block:: rst
.. test-file:: My JSON Test file
:file: ../tests/doc_test/utils/json_data.json
:id: JSON_001
:auto_suites:
:auto_cases:
.. test-file:: My JSON Test file
:file: ../tests/doc_test/utils/json_data.json
:id: JSON_001
:auto_suites:
:auto_cases:





Importing
---------
Expand Down Expand Up @@ -132,5 +157,5 @@ Test framework related examples
.. toctree::
:maxdepth: 1

pytest.rst
casperjs.rst
pytest
casperjs
2 changes: 2 additions & 0 deletions docs/filter.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
:hide-navigation:

.. _filter:

Filtering Test Data
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Content
install
directives/index
configuration
parsers
filter
functions
examples/index
Expand Down
52 changes: 52 additions & 0 deletions docs/parsers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
:hide-navigation:

Parsers
=======

``Sphinx-Test-Reports`` provides different parsers for test result files.
One for **XML-files**, following the **JUnit** format, and one for
generic **JSON-files**, which are not following any standard.

The needed parser is automatically selected by the file extensions ``xml`` or ``json``.

Junit Parser
------------
This parser reads **xml** files and handles the content as **JUnit** data.
Other XML formats are not supported.

As **JUnit** format is not really standardized, different test framework produce slightly different JUnit xml files.
``Sphinx-Test-Reports`` supports the "dialects" of:

* pytest
* GoogleTest
* CasperJS

There is a high chance, that other tet frameworks are supported as well, as long as they provide a Junit file.

.. _json_parser:

JSON Parser
-----------

The JSON parser can read any JSON file. But as the data structure is not following any standard, the user need to
provide a mapping between the used JSON structure and the internal structure used by ``Sphinx-Test-Reports``.

Even if this means some more configuration effort, the benefit is a completely customizable data structure.

For an example please take a look into :ref:`json_example`.

For mapping configuration please see :ref:`tr_json_mapping`.


Technical details
-----------------
Each parser is realized by a class, which needs to provide specific functions.
Also the internal object representation is the same. So each parser must map the external format to the internal
representation.

Only the parser cares about the format. Other internal functions (like directives) just work with the common
data representation and rely on it.
So parsers are not allowed to rename or even extend this internal representation.
If this is needed, all available parsers need to be updated as well.


2 changes: 1 addition & 1 deletion docs/ub_theme/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"icon": {
"repo": "fontawesome/brands/github",
},
"font": {"code": "JetBrains Mono", "text": "Urbanist"},
"font": {"code": "JetBrains Mono", "text": "Recursive"},
"globaltoc_collapse": True,
"features": [
"navigation.top",
Expand Down
6 changes: 3 additions & 3 deletions sphinxcontrib/test_reports/directives/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ def run(self, nested=False, suite_count=-1, case_count=-1):
# access n-th nested suite here
self.results = self.results[0]["testsuites"][suite_count]

suite_name = self.options.get("suite", None)
suite_name = self.options.get("suite")

if suite_name is None:
raise TestReportInvalidOption("Suite not given!")

case_full_name = self.options.get("case", None)
class_name = self.options.get("classname", None)
case_full_name = self.options.get("case")
class_name = self.options.get("classname")
if case_full_name is None and class_name is None:
raise TestReportInvalidOption("Case or classname not given!")

Expand Down
16 changes: 11 additions & 5 deletions sphinxcontrib/test_reports/directives/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from sphinx.util import logging
from sphinx_needs.api import make_hashed_id

from sphinxcontrib.test_reports.exceptions import SphinxError, TestReportFileNotSetException
from sphinxcontrib.test_reports.exceptions import (
SphinxError, TestReportFileNotSetException)
from sphinxcontrib.test_reports.jsonparser import JsonParser
from sphinxcontrib.test_reports.junitparser import JUnitParser

# fmt: on
Expand Down Expand Up @@ -65,7 +67,11 @@ def load_test_file(self):
return None

if self.test_file not in self.app.testreport_data.keys():
parser = JUnitParser(self.test_file)
if os.path.splitext(self.test_file)[1] == ".json":
mapping = list(self.app.config.tr_json_mapping.values())[0]
parser = JsonParser(self.test_file, json_mapping=mapping)
else:
parser = JUnitParser(self.test_file)
self.app.testreport_data[self.test_file] = parser.parse()

self.results = self.app.testreport_data[self.test_file]
Expand All @@ -89,17 +95,17 @@ def prepare_basic_options(self):
),
)
else:
self.test_id = self.options.get("id", None)
self.test_id = self.options.get("id")

if self.test_id is None:
raise SphinxError("ID must be set for test-report.")

self.test_file = self.options.get("file", None)
self.test_file = self.options.get("file")
self.test_file_given = self.test_file[:]

self.test_links = self.options.get("links", "")
self.test_tags = self.options.get("tags", "")
self.test_status = self.options.get("status", None)
self.test_status = self.options.get("status")

self.collapse = str(self.options.get("collapse", ""))

Expand Down
4 changes: 2 additions & 2 deletions sphinxcontrib/test_reports/directives/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ class EnvReportDirective(Directive):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.data_option = self.options.get("data", None)
self.environments = self.options.get("env", None)
self.data_option = self.options.get("data")
self.environments = self.options.get("env")

if self.environments is not None:
self.req_env_list_cpy = self.environments.split(",")
Expand Down
3 changes: 2 additions & 1 deletion sphinxcontrib/test_reports/directives/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from docutils import nodes
from docutils.parsers.rst import directives

from sphinxcontrib.test_reports.directives.test_common import TestCommonDirective
from sphinxcontrib.test_reports.directives.test_common import \
TestCommonDirective
from sphinxcontrib.test_reports.exceptions import InvalidConfigurationError

# fmt: on
Expand Down
2 changes: 1 addition & 1 deletion sphinxcontrib/test_reports/directives/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def run(self, nested=False, count=-1):
# access n-th nested suite here
self.results = self.results[0]["testsuite_nested"]

suite_name = self.options.get("suite", None)
suite_name = self.options.get("suite")

if suite_name is None:
raise TestReportInvalidOption("Suite not given!")
Expand Down
Loading

0 comments on commit 7cab1e1

Please sign in to comment.