From a4c81bf281ebac0c3477d79f8cdd22edd3835efc Mon Sep 17 00:00:00 2001 From: Marvin Klerx Date: Thu, 15 Jan 2026 01:14:37 +0100 Subject: [PATCH 1/5] python typing with new base model structure + reading user keywords in suite --- atest/second_test_cli.robot | 14 +++ atest/test_cli.robot | 16 ++- pyproject.toml | 3 +- src/testdoc/html/rendering/render.py | 4 +- src/testdoc/parser/models.py | 15 +++ .../parser/modifier/suitefilemodifier.py | 6 +- src/testdoc/parser/testcaseparser.py | 7 +- src/testdoc/parser/testsuiteparser.py | 97 ++++++++++++++----- src/testdoc/testdoc.py | 3 +- 9 files changed, 133 insertions(+), 32 deletions(-) create mode 100644 atest/second_test_cli.robot create mode 100644 src/testdoc/parser/models.py diff --git a/atest/second_test_cli.robot b/atest/second_test_cli.robot new file mode 100644 index 0000000..ef5bb1f --- /dev/null +++ b/atest/second_test_cli.robot @@ -0,0 +1,14 @@ +*** Settings *** +Name Suite 2 +Documentation Basic Console Logging - Suite Doc +Metadata Author=Marvin Klerx +Metadata Creation=January 2026 +Test Tags Global-Tag + + +*** Test Cases *** +Suite 2 - TC-001 + [Documentation] Basic Console Logging + [Tags] RobotTestDoc + + Log Log message in suite 2 - TC-001 diff --git a/atest/test_cli.robot b/atest/test_cli.robot index 4cb86c5..d669cda 100644 --- a/atest/test_cli.robot +++ b/atest/test_cli.robot @@ -9,4 +9,18 @@ Test Tags Global-Tag Log Message [Documentation] Basic Console Logging [Tags] RobotTestDoc - Log RobotFramework Test Documentation Generator! \ No newline at end of file + + # this is my commentary + Log RobotFramework Test Documentation Generator! + + +*** Keywords *** +MyUserKeword + [Documentation] User Keyword + + Log This is a user keyword + +My Second User Keyword + [Documentation] User Keyword + + Log This is a user keyword diff --git a/pyproject.toml b/pyproject.toml index 34a1ef6..2543c80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,8 @@ dependencies = [ "click", "robotframework", "jinja2", - "tomli" + "tomli", + "pydantic" ] [project.optional-dependencies] diff --git a/src/testdoc/html/rendering/render.py b/src/testdoc/html/rendering/render.py index bc1995d..fcceb9e 100644 --- a/src/testdoc/html/rendering/render.py +++ b/src/testdoc/html/rendering/render.py @@ -1,6 +1,8 @@ from jinja2 import Environment, FileSystemLoader import os +from ...parser.models import SuiteInfoModel + from ...html.themes.theme_config import ThemeConfig from ...helper.cliargs import CommandLineArguments from ...helper.datetimeconverter import DateTimeConverter @@ -26,7 +28,7 @@ def _html_templ_selection(self): raise ValueError(f"CLI Argument 'html_template' got value '{self.args.html_template}' - value not known!") def render_testdoc(self, - suites, + suites: list[SuiteInfoModel], output_file ): env = Environment(loader=FileSystemLoader(self.TEMPLATE_DIR)) diff --git a/src/testdoc/parser/models.py b/src/testdoc/parser/models.py new file mode 100644 index 0000000..79225d5 --- /dev/null +++ b/src/testdoc/parser/models.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + +class SuiteInfoModel(BaseModel): + id: str + filename: str + name: str + doc: str | None + is_folder: bool + num_tests: int + source: str + total_tests: int = 0 + tests: list = [] + user_keywords: list = [] + sub_suites: list = [] + metadata: str | None diff --git a/src/testdoc/parser/modifier/suitefilemodifier.py b/src/testdoc/parser/modifier/suitefilemodifier.py index 5213b2f..2d94187 100644 --- a/src/testdoc/parser/modifier/suitefilemodifier.py +++ b/src/testdoc/parser/modifier/suitefilemodifier.py @@ -3,6 +3,7 @@ from ...helper.cliargs import CommandLineArguments from ...helper.logger import Logger from .sourceprefixmodifier import SourcePrefixModifier +from ..models import SuiteInfoModel class SuiteFileModifier(): @@ -12,7 +13,10 @@ def __init__(self): ############################################################################################################################# - def run(self, suite_object: TestSuite = None): + def run( + self, + suite_object: list[SuiteInfoModel] | None = None + ) -> list[SuiteInfoModel]: if not suite_object: raise KeyError(f"[{self.__class__}] - Error - Suite Object must not be None!") self.suite = suite_object diff --git a/src/testdoc/parser/testcaseparser.py b/src/testdoc/parser/testcaseparser.py index 150c0f9..7d50b2f 100644 --- a/src/testdoc/parser/testcaseparser.py +++ b/src/testdoc/parser/testcaseparser.py @@ -2,6 +2,7 @@ from robot.running.model import Keyword, Body from robot.errors import DataError from ..helper.cliargs import CommandLineArguments +from .models import SuiteInfoModel import textwrap class TestCaseParser(): @@ -11,8 +12,8 @@ def __init__(self): def parse_test(self, suite: TestSuite, - suite_info: dict - ) -> dict: + suite_info: SuiteInfoModel + ) -> SuiteInfoModel: for test in suite.tests: test_info = { @@ -23,7 +24,7 @@ def parse_test(self, "source": str(test.source), "keywords": self._keyword_parser(test.body) } - suite_info["tests"].append(test_info) + suite_info.tests.append(test_info) return suite_info # Consider tags via officially provided robot api diff --git a/src/testdoc/parser/testsuiteparser.py b/src/testdoc/parser/testsuiteparser.py index bc3afb0..090900b 100644 --- a/src/testdoc/parser/testsuiteparser.py +++ b/src/testdoc/parser/testsuiteparser.py @@ -2,12 +2,17 @@ # Derived code: see class `RobotSuiteFiltering`. import os from pathlib import Path +from typing import Tuple, cast from robot.api import SuiteVisitor, TestSuite +from robot.api.parsing import get_model +from robot.parsing.model.blocks import File, KeywordSection, Keyword +from robot import running from .testcaseparser import TestCaseParser from .modifier.suitefilemodifier import SuiteFileModifier from ..helper.cliargs import CommandLineArguments -from..helper.pathconverter import PathConverter +from ..helper.pathconverter import PathConverter +from .models import SuiteInfoModel from robot.conf import RobotSettings from robot.running import TestSuiteBuilder @@ -19,7 +24,7 @@ class RobotSuiteParser(SuiteVisitor): def __init__(self): self.suite_counter = 0 - self.suites = [] + self.suites: list[SuiteInfoModel] = [] self.tests = [] self.args = CommandLineArguments() @@ -28,32 +33,50 @@ def visit_suite(self, suite): # Skip suite if its already parsed into list self._already_parsed(suite) + suite_info: SuiteInfoModel = SuiteInfoModel( + id=str(suite.longname).lower().replace(".", "_").replace(" ", "_"), + filename=str(Path(suite.source).name) if suite.source else suite.name, + name=suite.name, + doc="
".join(line.replace("\\n","") for line in suite.doc.splitlines() if line.strip()) if suite.doc else None, + is_folder=self._is_directory(suite), + num_tests=len(suite.tests), + source=str(suite.source), + metadata="
".join([f"{k}: {v}" for k, v in suite.metadata.items()]) if suite.metadata else None, + ) + # Test Suite Parser - suite_info = { - "id": str(suite.longname).lower().replace(".", "_").replace(" ", "_"), - "filename": str(Path(suite.source).name) if suite.source else suite.name, - "name": suite.name, - "doc": "
".join(line.replace("\\n","") for line in suite.doc.splitlines() if line.strip()) if suite.doc else None, - "is_folder": self._is_directory(suite), - "num_tests": len(suite.tests), - "source": str(suite.source), - "total_tests": 0, - "tests": [], - "sub_suites": [], - "metadata": "
".join([f"{k}: {v}" for k, v in suite.metadata.items()]) if suite.metadata else None - } + # suite_info = { + # "id": str(suite.longname).lower().replace(".", "_").replace(" ", "_"), + # "filename": str(Path(suite.source).name) if suite.source else suite.name, + # "name": suite.name, + # "doc": "
".join(line.replace("\\n","") for line in suite.doc.splitlines() if line.strip()) if suite.doc else None, + # "is_folder": self._is_directory(suite), + # "num_tests": len(suite.tests), + # "source": str(suite.source), + # "total_tests": 0, + # "tests": [], + # "user_keywords": [], + # "sub_suites": [], + # "metadata": "
".join([f"{k}: {v}" for k, v in suite.metadata.items()]) if suite.metadata else None + # } # Parse Test Cases suite_info = TestCaseParser().parse_test(suite, suite_info) + if not suite_info.is_folder: + # visit suite model to check if user keywords got created + suite_info = self.get_suite_user_keywords(str(suite.source) ,suite_info) + # Collect sub-suites recursive suite_info, total_tests = self._recursive_sub_suite(suite, suite_info) + # add count of total tests + suite_info.total_tests = total_tests + # Append to suites object - suite_info["total_tests"] = total_tests self.suites.append(suite_info) - def parse_suite(self): + def parse_suite(self) -> list[SuiteInfoModel]: # Use official Robot Framework Application Package to parse cli arguments and modify suite object. robot_options = self._convert_args() _rfs = RobotSuiteFiltering() @@ -63,30 +86,56 @@ def parse_suite(self): # Custom suite object modification with new test doc library suite = SuiteFileModifier()._modify_root_suite_details(suite) suite.visit(self) + return self.suites ############################################################################################## # Helper: ############################################################################################## + def get_suite_user_keywords( + self, + suite_path: str, + suite_info: SuiteInfoModel + ) -> SuiteInfoModel: + """ + function checks if user keywords are defined within the currently visiting suite object + """ + + suite_model: File = get_model(suite_path) + for section in suite_model.sections: + if not isinstance(section, KeywordSection): + continue + + if len(section.body) == 0: + return + + section = cast(KeywordSection, section) + suite_keywords: list = [] + for kw in section.body: + kw = cast(Keyword, kw) + suite_keywords.append(kw.name) + suite_info.user_keywords = suite_keywords + return suite_info + def _recursive_sub_suite(self, suite: TestSuite, - suite_info: dict - ): - total_tests = suite_info["num_tests"] + suite_info: SuiteInfoModel + ) -> Tuple[SuiteInfoModel, int]: + total_tests = suite_info.num_tests for sub_suite in suite.suites: sub_parser = RobotSuiteParser() sub_parser.visit_suite(sub_suite) - suite_info["sub_suites"].extend(sub_parser.suites) - total_tests += sum(s["total_tests"] for s in sub_parser.suites) + suite_info.sub_suites.extend(sub_parser.suites) + total_tests += sum(s.total_tests for s in sub_parser.suites) return suite_info, total_tests def _is_directory(self, suite) -> bool: suite_path = suite.source if suite.source else "" return(os.path.isdir(suite_path) if suite_path else False) - def _already_parsed(self, suite): - existing_suite = next((s for s in self.suites if s["name"] == suite.name), None) + def _already_parsed(self, suite: running.TestSuite): + existing_suite = next((s for s in self.suites if s.name == suite.name), None) if existing_suite: return diff --git a/src/testdoc/testdoc.py b/src/testdoc/testdoc.py index 1d5662c..b7fc085 100644 --- a/src/testdoc/testdoc.py +++ b/src/testdoc/testdoc.py @@ -2,12 +2,13 @@ from .parser.testsuiteparser import RobotSuiteParser from .html.rendering.render import TestDocHtmlRendering from .parser.modifier.suitefilemodifier import SuiteFileModifier +from .parser.models import SuiteInfoModel class TestDoc(): def main(self): # Parse suite object & return complete suite object with all information - suite_object = RobotSuiteParser().parse_suite() + suite_object: list[SuiteInfoModel] = RobotSuiteParser().parse_suite() # Run SuiteFileModifier to modify the test suite object suite_object = SuiteFileModifier().run(suite_object) From d789da6acbaf292ad9017718c5e6e293ce7a7dd8 Mon Sep 17 00:00:00 2001 From: Marvin Klerx Date: Thu, 15 Jan 2026 08:56:46 +0100 Subject: [PATCH 2/5] showing suite user keywords --- .../html/templates/v2/jinja_template_03.html | 10 ++++++++++ src/testdoc/parser/models.py | 9 ++++++++- src/testdoc/parser/testcaseparser.py | 16 ++++++++-------- src/testdoc/parser/testsuiteparser.py | 1 + src/testdoc/testdoc.py | 2 +- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/testdoc/html/templates/v2/jinja_template_03.html b/src/testdoc/html/templates/v2/jinja_template_03.html index 544edbc..e304682 100644 --- a/src/testdoc/html/templates/v2/jinja_template_03.html +++ b/src/testdoc/html/templates/v2/jinja_template_03.html @@ -44,6 +44,16 @@ {{ suite.metadata }} {% endif %} + {% if suite.user_keywords is not none %} + + 🔑 User Keywords: + + {% if suite.user_keywords %} +
- {{ suite.user_keywords | join('\n- ') }}
+ {% endif %} + + + {% endif %} {% for test in suite.tests %} diff --git a/src/testdoc/parser/models.py b/src/testdoc/parser/models.py index 79225d5..0d6f501 100644 --- a/src/testdoc/parser/models.py +++ b/src/testdoc/parser/models.py @@ -10,6 +10,13 @@ class SuiteInfoModel(BaseModel): source: str total_tests: int = 0 tests: list = [] - user_keywords: list = [] + user_keywords: list | None = None sub_suites: list = [] metadata: str | None + +class TestInfoModel(BaseModel): + name: str + doc: str + tags: list + source: str + keywords: list[str] | list \ No newline at end of file diff --git a/src/testdoc/parser/testcaseparser.py b/src/testdoc/parser/testcaseparser.py index 7d50b2f..fc6a682 100644 --- a/src/testdoc/parser/testcaseparser.py +++ b/src/testdoc/parser/testcaseparser.py @@ -2,7 +2,7 @@ from robot.running.model import Keyword, Body from robot.errors import DataError from ..helper.cliargs import CommandLineArguments -from .models import SuiteInfoModel +from .models import SuiteInfoModel, TestInfoModel import textwrap class TestCaseParser(): @@ -16,14 +16,14 @@ def parse_test(self, ) -> SuiteInfoModel: for test in suite.tests: - test_info = { - "name": test.name, - "doc": "
".join(line.replace("\\n","") for line in test.doc.splitlines() + test_info: TestInfoModel = TestInfoModel( + name=test.name, + doc="
".join(line.replace("\\n","") for line in test.doc.splitlines() if line.strip()) if test.doc else "No Test Case Documentation Available", - "tags": test.tags if test.tags else ["No Tags Configured"], - "source": str(test.source), - "keywords": self._keyword_parser(test.body) - } + tags=test.tags if test.tags else ["No Tags Configured"], + source=str(test.source), + keywords=self._keyword_parser(test.body) + ) suite_info.tests.append(test_info) return suite_info diff --git a/src/testdoc/parser/testsuiteparser.py b/src/testdoc/parser/testsuiteparser.py index 090900b..21172c3 100644 --- a/src/testdoc/parser/testsuiteparser.py +++ b/src/testdoc/parser/testsuiteparser.py @@ -42,6 +42,7 @@ def visit_suite(self, suite): num_tests=len(suite.tests), source=str(suite.source), metadata="
".join([f"{k}: {v}" for k, v in suite.metadata.items()]) if suite.metadata else None, + user_keywords=None ) # Test Suite Parser diff --git a/src/testdoc/testdoc.py b/src/testdoc/testdoc.py index b7fc085..7acd53e 100644 --- a/src/testdoc/testdoc.py +++ b/src/testdoc/testdoc.py @@ -6,7 +6,7 @@ class TestDoc(): - def main(self): + def main(self): # Parse suite object & return complete suite object with all information suite_object: list[SuiteInfoModel] = RobotSuiteParser().parse_suite() From 1974adc56b21efe9a144fb7f07b80186c4907d88 Mon Sep 17 00:00:00 2001 From: Marvin Klerx Date: Thu, 15 Jan 2026 12:38:55 +0100 Subject: [PATCH 3/5] increase python requirement to min. 3.10 due to typing operands --- .github/workflows/build.yml | 2 +- pyproject.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b88fefe..759485f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 2543c80..1313357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "robotframework-testdoc" dynamic = ["version"] description = "A CLI Tool to generate a Test Documentation for your RobotFramework Test Scripts." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [ { name = "Marvin Klerx", email = "marvinklerx20@gmail.com" } ] @@ -16,7 +16,6 @@ license = { text = "Apache-2.0" } classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12" From 0fcb6b84b8b6e4cbc6a09482ecb8b248fa8e1a84 Mon Sep 17 00:00:00 2001 From: Marvin Klerx Date: Thu, 15 Jan 2026 12:40:51 +0100 Subject: [PATCH 4/5] fix python version in ci job --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 759485f..3fb3e19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: '3.12' - name: Install Hatch run: pip install hatch From 21ac78b4d4383f960036673b51f79189571aa986 Mon Sep 17 00:00:00 2001 From: Marvin Klerx Date: Thu, 15 Jan 2026 14:57:01 +0100 Subject: [PATCH 5/5] showing keywords and tests in RF structured object, docs extended, test resources created --- .gitignore | 6 +- DEVELOPMENT.md | 101 +++++++++++++++++ README.md | 26 ++++- atest/test_cli.py | 15 ++- atest/testcases/component_a/suite_a.robot | 14 +++ .../subcomponent_b/subsuite_b.robot | 14 +++ atest/testcases/component_b/suite_b.robot | 20 ++++ docs/suite_keywords_visualization.png | Bin 0 -> 5615 bytes docs/test_keywords_visualization.png | Bin 0 -> 6170 bytes .../html/templates/v2/jinja_template_03.html | 103 ++++++++++++------ src/testdoc/html/themes/themes.py | 12 ++ .../parser/modifier/sourceprefixmodifier.py | 20 ++-- src/testdoc/parser/testcaseparser.py | 20 +++- src/testdoc/parser/testsuiteparser.py | 2 +- 14 files changed, 300 insertions(+), 53 deletions(-) create mode 100644 DEVELOPMENT.md create mode 100644 atest/testcases/component_a/suite_a.robot create mode 100644 atest/testcases/component_b/subcomponent_b/subsuite_b.robot create mode 100644 atest/testcases/component_b/suite_b.robot create mode 100644 docs/suite_keywords_visualization.png create mode 100644 docs/test_keywords_visualization.png diff --git a/.gitignore b/.gitignore index bb6b757..f798f27 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,8 @@ image.png output_doc_robot.html devel.html build/** -uv.lock \ No newline at end of file +uv.lock +atest/output_classic.html +atest/output_big.html +results/ +oc_output.html diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..c18f1ff --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,101 @@ +# Development + +## Release Creation + +### Precondition + +- You are required to have an account for **[PyPi](https://pypi.org/)**. +- Additionally, check that you have required permissions for the projects at ``PyPi & GitHub``! +- Check that all pipeline-checks did pass, your code changes are ready and everything is already merged to the main branch! +- Your local git configuration must work - check your authentication! + +### *Preferred* - Using Shell Script for Release Creation + +In the ``root directory`` of this repository you will find a script called ``create_release.sh``. +When executing this script, it expects exactly one argument: the new name of the release / tag. +Here you need to provide always the syntax ``X.X.X``, e.g. ``0.1.9``, ``0.5.0`` or ``1.0.0``. + +> [!TIP] +> Please use following versioning to create ``alpha``, ``beta`` or ``releasecandidate`` release at ``PyPi``! +> Example: +> ```bash +> ./create_release.sh 1.0.0a1 # alpha +> ./create_release.sh 1.0.0b2 # beta +> ./create_release.sh 1.0.0rc1 # release candidate +> ``` + +The script will do two things automatically for you: +1. Updating the version string in the **[\_\_about\_\_.py](src/testdoc/__about__.py)** +2. Creating & Pushing a new tag to GitHub. + +Execute the following command to create a new release: +```bash +cd +./create_release.sh 1.0.0 +``` + +After executing the script, three pipeline jobs are getting triggered automatically: +1. First job creates a new ``Release`` in github with the name of the created ``Tag``. +2. Second job uploads the new wheel package to ``PyPi`` with the ``__version__`` from the ``__about__.py`` file. +3. Third job generates a new keyword documentation via ``libdoc`` on the main branch, but pushes it to the ``gh_pages`` where it is available as public ``GitHub Page``. + +### *Backup* - Manual Version Bump & Tag Creation + +If the preferred way using the ``create_release.sh`` script does not work, you can also manually create a new release. +Therefore, execute the following steps. + +#### 1. Increase Version + +Open the file **[\_\_about\_\_.py](src/testdoc/__about__.py)** and increase the version (``0.0.5 = major.minor.path``) before building & uploading the new python wheel package! + +The new version **must be unique** & **not already existing** in ``PyPi`` & ``GitHub Releases``!!! + +Commit & push the changed version into the ``main`` branch of the repository. + +#### 2. Create new Tag + +Now, create a new tag from the main branch with the syntax ``vX.X.X`` -> example: ``v1.0.5``. + +Use the following commands to create & push the tag: +``` +git tag v0.0.5 +git push origin v0.0.5 +``` + +#### 2.1. Creating GitHub Release & Deploy Wheel Package to PyPi + +After pushing the new tag, three pipeline jobs are getting triggered automatically: +1. First job creates a new ``Release`` in github with the name of the created ``Tag``. +2. Second job uploads the new wheel package to ``PyPi`` with the ``__version__`` from the ``__about__.py`` file. +3. Third job generates a new keyword documentation via ``libdoc`` on the main branch, but pushes it to the ``gh_pages`` where it is available as public ``GitHub Page``. + +## Installing Dev Dependencies + +Contributing to this project & working with it locally, requires you to install some ``dev dependencies`` - use the following command in the project root directory: +``` +pip install -e .[dev] +``` + +Afterwards, the library gets installed in ``editable mode`` & you will have a ``dev dependencies`` installed. + +## Hatch + +You will need the python package tool ``hatch`` for several operations in this repository. +Hatch can be used to execute the linter, the tests or to build a wheel package. + +Use the following command: +```shell +pip install hatch +``` + +### Ececute Linting Checks via ruff + +```shell +hatch run dev:lint +``` + +### Execute Acceptance Tests via PyTest + +```shell +hatch run dev:atest +``` \ No newline at end of file diff --git a/README.md b/README.md index fa3d1db..7ff9496 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,20 @@ testdoc --hide-source tests/ TestDocumentation.html testdoc --hide-keywords tests/ TestDocumentation.html ``` +## Visualization of Keywords + +Keywords are visualized in a specific design and to replicate the robot framework test case structure (model) as best as possible. + +### User / Library Keywords called in a Test Case Body + +This view does actually replicate the robot framework suite file with the ``*** Test Cases ***`` section, the ``Test Case Name`` and the ``Test Case Body``. + +![alt text](docs/test_keywords_visualization.png) + +### User Keyword defined in a Test Suite + +![alt text](docs/suite_keywords_visualization.png) + ## Robot Framework Tags The commandline arguments ``include`` & ``exclude`` have more or less the same functionality like in the known ``robot ...`` command. You can decide to weither include and / or exclude specific test cases into the test documentation. @@ -138,6 +152,8 @@ border_color = "#CCCCCC" text_color = "#CCCCCC" title_color = "#00ffb9" robot_icon = "#00ffb9" +code_area_background = "#303030" +code_area_foreground = "#f1f1f1" ``` ## HTML Template Selection @@ -149,7 +165,7 @@ These template can be configured via ``cli arguments`` or within a ``.toml confi - v2 -### Available HTML Templates +### Available HTML Templates - NOT RECOMMENDED You can choose one of the following designs: - v1 @@ -230,6 +246,8 @@ border_color = "#CCCCCC" text_color = "#CCCCCC" title_color = "#00ffb9" robot_icon = "#00ffb9" +code_area_background = "#303030" +code_area_foreground = "#f1f1f1" ``` > [!TIP] @@ -247,4 +265,8 @@ robot_icon = "#00ffb9" #### Robot / Default -![alt text](docs/style_robot.png) \ No newline at end of file +![alt text](docs/style_robot.png) + +## Contribution & Development + +See [Development.md](./DEVELOPMENT.md) for more information about contributing & developing this library. \ No newline at end of file diff --git a/atest/test_cli.py b/atest/test_cli.py index 5c2d3b0..ac46858 100644 --- a/atest/test_cli.py +++ b/atest/test_cli.py @@ -12,12 +12,23 @@ def test_cli_help(): def test_cli_cmd(): current_dir = os.path.dirname(os.path.abspath(__file__)) robot = os.path.join(current_dir, "test_cli.robot") - output = os.path.join(current_dir, "output.html") + output = os.path.join(current_dir, "output_classic.html") runner = CliRunner() result = runner.invoke(main, [robot, output]) assert result.exit_code == 0 assert "Generated" in result.output - assert "output.html" in result.output + assert "output_classic.html" in result.output + assert os.path.exists(output) + +def test_cli_cmd_big_suite(): + current_dir = os.path.dirname(os.path.abspath(__file__)) + robot = os.path.join(current_dir) + output = os.path.join(current_dir, "output_big.html") + runner = CliRunner() + result = runner.invoke(main, [robot, output]) + assert result.exit_code == 0 + assert "Generated" in result.output + assert "output_big.html" in result.output assert os.path.exists(output) diff --git a/atest/testcases/component_a/suite_a.robot b/atest/testcases/component_a/suite_a.robot new file mode 100644 index 0000000..f909c85 --- /dev/null +++ b/atest/testcases/component_a/suite_a.robot @@ -0,0 +1,14 @@ +*** Test Cases *** +Suite A - TC-001 + [Tags] CompA Regression + Log A-TC001 + +Suite A - TC002 + [Tags] CompA Smoke + Log A-TC002 + + +*** Keywords *** +Suite A - User Keyword A + [Documentation] docs + Log A-KW001 diff --git a/atest/testcases/component_b/subcomponent_b/subsuite_b.robot b/atest/testcases/component_b/subcomponent_b/subsuite_b.robot new file mode 100644 index 0000000..7d92fe0 --- /dev/null +++ b/atest/testcases/component_b/subcomponent_b/subsuite_b.robot @@ -0,0 +1,14 @@ +*** Test Cases *** +Subsuite B - TC-001 + [Tags] CompB Regression + Log SB-TC001 + +Subsuite B - TC002 + [Tags] CompB Smoke Feature-XYZ + Log SB-TC002 + + +*** Keywords *** +Subsuite B - User Keyword B + [Documentation] docs + Log SB-KW001 \ No newline at end of file diff --git a/atest/testcases/component_b/suite_b.robot b/atest/testcases/component_b/suite_b.robot new file mode 100644 index 0000000..8ea4256 --- /dev/null +++ b/atest/testcases/component_b/suite_b.robot @@ -0,0 +1,20 @@ +*** Settings *** +Name Custom Name Suite B +Metadata author=Marvin Klerx +Metadata creation_date=January 2026 +Metadata company=imbus AG + + +*** Test Cases *** +Suite C - TC001 + [Tags] CompC robot:exclude + Log C-TC001 + +Suite C - TC002 + [Tags] CompC test:retry(1) + Log C-TC002 + + +*** Keywords *** +Suite C - User Keyword 1 + Log Suite C - User Keyword 1 diff --git a/docs/suite_keywords_visualization.png b/docs/suite_keywords_visualization.png new file mode 100644 index 0000000000000000000000000000000000000000..e46efc07e27b672a5126a3819c4722fe338b9936 GIT binary patch literal 5615 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 JR$O;}{2xJo(Cq*K literal 0 HcmV?d00001 diff --git a/docs/test_keywords_visualization.png b/docs/test_keywords_visualization.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce2cb72d73bddac517b524573afb51e33b0338b GIT binary patch literal 6170 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 Ocw@ucH!6Q~e*7QuqJg~t literal 0 HcmV?d00001 diff --git a/src/testdoc/html/templates/v2/jinja_template_03.html b/src/testdoc/html/templates/v2/jinja_template_03.html index e304682..f8114b7 100644 --- a/src/testdoc/html/templates/v2/jinja_template_03.html +++ b/src/testdoc/html/templates/v2/jinja_template_03.html @@ -16,45 +16,53 @@ {% macro render_test_cases(suite) %} {% if suite.tests %}
-
Generic Suite Details:
+
Test Suite Details:
+
- + {% if not suite.is_folder %} - + {% endif %} - + {% if suite.doc is not none %} - + {% endif %} - {% if suite.metadata is not none %} - - - - - {% endif %} - {% if suite.user_keywords is not none %} + {% if suite.source is not none %} - + {% endif %} + {% if suite.metadata is not none %} + + + + + {% endif %}
📁 Suite Name:📁 Suite Name: {{ suite.name }}
📄 File Name:📄 File Name: {{ suite.filename }}
📊 Number of Tests:📊 Number of Tests: {{ suite.num_tests }}
📝 Docs:📝 Docs: {{ suite.doc }}
⚙️ Metadata:{{ suite.metadata }}
🔑 User Keywords:🔗 Source: - {% if suite.user_keywords %} -
- {{ suite.user_keywords | join('\n- ') }}
- {% endif %} + {{ suite.source }}
⚙️ Metadata:{{ suite.metadata }}
+ {% if suite.user_keywords is not none %} +
+
🔑 Suite User Keywords:
+ {% if suite.user_keywords %} +
+*** Keywords ***
+    {{ suite.user_keywords | join('\n    ') }}
+ {% endif %} + {% endif %}
{% for test in suite.tests %}
@@ -67,6 +75,8 @@
+
Test Case Details:
+
{% set has_info = test.doc is not none or test.source is not none or test.tags is not none or test.keywords is not none %} {% if test.doc is not none %} @@ -77,7 +87,7 @@ {% endif %} {% if test.source is not none %} - + @@ -85,30 +95,31 @@ {% endif %} {% if test.tags is not none %} - + {% endif %} - {% if test.keywords is not none %} - - - - - {% endif %} {% if not has_info %} - {% endif %}
🔗 Source:🔗 Source: {{ test.source }}
🏷 Tags:🏷 Tags: {{ test.tags | join(', ') }}
🔑 Keywords: - {% if test.keywords %} -
- {{ test.keywords | join('\n- ') }}
- {% endif %} -
+ No Details Available / Enabled !
+ {% if test.keywords is not none %} +
+
🔑 Keywords / Test Case Body:
+ {% if test.keywords %} +
+*** Test Cases ***
+{{ test.name }}
+    {{ test.keywords | join('\n    ') }}
+                                
+ {% endif %} + {% endif %}
@@ -116,25 +127,34 @@ {% endif %} {% if suite.sub_suites %}
-
Generic Parent Suite Details:
+
Parent Test Suite Details:
+
- + - + {% if suite.doc is not none %} - + {% endif %} + {% if suite.source is not none %} + + + + + {% endif %} {% if suite.metadata is not none %} - + {% endif %} @@ -220,6 +240,21 @@ .sidebar { min-width: 120px; max-width: 180px; } .main-content { padding: 10px; } } + .keywords-block, + .keywords-block pre { + color: {{ colors.text_color }}; + } + + .keywords-block-div { + margin-bottom: 8px; + } + + .keywords-block-pre { + background-color: {{ colors.code_area_background }}; + color: {{ colors.code_area_foreground }}; + border-radius: 8px; + padding: 12px 14px; + } diff --git a/src/testdoc/html/themes/themes.py b/src/testdoc/html/themes/themes.py index 92b24d8..bbb7ebe 100644 --- a/src/testdoc/html/themes/themes.py +++ b/src/testdoc/html/themes/themes.py @@ -8,6 +8,8 @@ "text_color": "#353535", "title_color": "#343a40", "robot_icon": "#00c0b5", + "code_area_background": "#f8f9fa", + "code_area_foreground": "#353535" } ROBOT_THEME = { @@ -19,6 +21,8 @@ "text_color": "black", "title_color": "black", "robot_icon": "#00c0b5", + "code_area_background": "#dcdcdc", + "code_area_foreground": "#black" } ROBOT_THEME_DARK = { @@ -30,6 +34,8 @@ "text_color": "#e3e3e3", "title_color": "#e3e3e3", "robot_icon": "#00c0b5", + "code_area_background": "#272729", + "code_area_foreground": "#e3e3e3" } GREEN_THEME = { @@ -41,6 +47,8 @@ "text_color": "#009770", "title_color": "#009770", "robot_icon": "#009770", + "code_area_background": "#272729", + "code_area_foreground": "#009770" } DARK_THEME = { @@ -52,6 +60,8 @@ "text_color": "white", "title_color": "#00c0b5", "robot_icon": "#00c0b5", + "code_area_background": "#5F5F5F", + "code_area_foreground": "#f1f1f1" } BLUE_THEME = { @@ -63,4 +73,6 @@ "text_color": "#CCCCCC", "title_color": "#00ffb9", "robot_icon": "#00ffb9", + "code_area_background": "#303030", + "code_area_foreground": "#f1f1f1" } diff --git a/src/testdoc/parser/modifier/sourceprefixmodifier.py b/src/testdoc/parser/modifier/sourceprefixmodifier.py index 8993c4f..235ab63 100644 --- a/src/testdoc/parser/modifier/sourceprefixmodifier.py +++ b/src/testdoc/parser/modifier/sourceprefixmodifier.py @@ -1,8 +1,11 @@ import os from abc import ABC, abstractmethod +from typing import cast from robot.api import TestSuite +from ...parser.models import SuiteInfoModel, TestInfoModel + from ...helper.cliargs import CommandLineArguments from ...helper.logger import Logger @@ -78,7 +81,7 @@ def _get_git_branch(self, git_root): with open(head_file, "r") as f: content = f.read().strip() if content.startswith("ref:"): - return content.split("/")[-1] + return content.replace("ref: refs/heads/", "") return "main" def _convert_to_gitlab_url(self, file_path, prefix): @@ -89,19 +92,20 @@ def _convert_to_gitlab_url(self, file_path, prefix): rel_path = os.path.relpath(file_path, git_root).replace(os.sep, "/") return prefix.rstrip("/") + "/-/blob/" + git_branch + "/" + rel_path - def apply(self, suite_dict, prefix): + def apply(self, suite_dict: SuiteInfoModel, prefix): try: - suite_dict["source"] = self._convert_to_gitlab_url(suite_dict["source"], prefix) + suite_dict.source = self._convert_to_gitlab_url(suite_dict.source, prefix) except: - suite_dict["source"] = "GitLink error" + suite_dict.source = "GitLink error" - for test in suite_dict.get("tests", []): + for test in suite_dict.tests: + test = cast(TestInfoModel, test) try: - test["source"] = self._convert_to_gitlab_url(test["source"], prefix) + test.source = self._convert_to_gitlab_url(test.source, prefix) except: - test["source"] = "GitLink error" + test.source = "GitLink error" - for sub_suite in suite_dict.get("sub_suites", []): + for sub_suite in suite_dict.sub_suites: self.apply(sub_suite, prefix) ######################################## diff --git a/src/testdoc/parser/testcaseparser.py b/src/testdoc/parser/testcaseparser.py index fc6a682..fabca84 100644 --- a/src/testdoc/parser/testcaseparser.py +++ b/src/testdoc/parser/testcaseparser.py @@ -89,7 +89,9 @@ def _handle_keyword_types(self, kw: Keyword, indent: int = 0): result.append(header) for subkw in getattr(branch, 'body', []): result.extend(self._handle_keyword_types(subkw, indent=indent+1)) - result.append(f"{_indent}END") + result.append( + f"{_indent}END\n" if indent == 0 else f"{_indent}END" + ) # FOR loop elif kw_type == "FOR": @@ -104,7 +106,9 @@ def _handle_keyword_types(self, kw: Keyword, indent: int = 0): if hasattr(kw, 'body'): for subkw in kw.body: result.extend(self._handle_keyword_types(subkw, indent=indent+1)) - result.append(f"{_indent}END") + result.append( + f"{_indent}END\n" if indent == 0 else f"{_indent}END" + ) # GROUP loop elif kw_type == "GROUP": @@ -117,7 +121,9 @@ def _handle_keyword_types(self, kw: Keyword, indent: int = 0): if hasattr(kw, 'body'): for subkw in kw.body: result.extend(self._handle_keyword_types(subkw, indent=indent+1)) - result.append(f"{_indent}END") + result.append( + f"{_indent}END\n" if indent == 0 else f"{_indent}END" + ) # WHILE loop elif kw_type == "WHILE": @@ -128,7 +134,9 @@ def _handle_keyword_types(self, kw: Keyword, indent: int = 0): if hasattr(kw, 'body'): for subkw in kw.body: result.extend(self._handle_keyword_types(subkw, indent=indent+1)) - result.append(f"{_indent}END") + result.append( + f"{_indent}END\n" if indent == 0 else f"{_indent}END" + ) # TRY/EXCEPT/FINALLY elif kw_type in ("TRY", "EXCEPT", "FINALLY"): @@ -142,7 +150,9 @@ def _handle_keyword_types(self, kw: Keyword, indent: int = 0): for subkw in kw.body: result.extend(self._handle_keyword_types(subkw, indent=indent+1)) if kw_type in ("EXCEPT", "FINALLY"): - result.append(f"{_indent}END") + result.append( + f"{_indent}END\n" if indent == 0 else f"{_indent}END" + ) # BREAK, CONTINUE, RETURN, ERROR elif kw_type in ("BREAK", "CONTINUE", "RETURN", "ERROR"): diff --git a/src/testdoc/parser/testsuiteparser.py b/src/testdoc/parser/testsuiteparser.py index 21172c3..0658a00 100644 --- a/src/testdoc/parser/testsuiteparser.py +++ b/src/testdoc/parser/testsuiteparser.py @@ -41,7 +41,7 @@ def visit_suite(self, suite): is_folder=self._is_directory(suite), num_tests=len(suite.tests), source=str(suite.source), - metadata="
".join([f"{k}: {v}" for k, v in suite.metadata.items()]) if suite.metadata else None, + metadata="
".join([f"{k} {v}" for k, v in suite.metadata.items()]) if suite.metadata else None, user_keywords=None )
📁 Suite Name:📁 Suite Name: {{ suite.name }}
📊 Number of Tests:📊 Number of Tests: {{ suite.total_tests }}
📝 Docs:📝 Docs: {{ suite.doc }}
🔗 Source: + {{ suite.source }} +
⚙️ Metadata:⚙️ Metadata: {{ suite.metadata }}