Skip to content

Commit

Permalink
System Test Framework based on unittest
Browse files Browse the repository at this point in the history
Implement a new version of the system test framework.
It is based on Python's `unittest` framework.

Add requirements for the system test framework.
Add a `README.md` to explain these requirements and to give context.
  • Loading branch information
phiwuu committed Feb 26, 2025
1 parent 3379e6e commit 0852bd1
Show file tree
Hide file tree
Showing 65 changed files with 838 additions and 342 deletions.
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ jobs:
- name: Executing linter
run: |
make lint
lint-system-tests:
name: PyLint System Tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements_dev.txt
- name: Executing linter
run: |
make lint-system-tests
trlc:
name: TRLC
runs-on: ubuntu-24.04
Expand All @@ -37,7 +49,7 @@ jobs:
make trlc
selenium-tests:
name: Run Selenium Tests
needs: [lint, trlc]
needs: [lint, lint-system-tests, trlc]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
Expand Down
31 changes: 27 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ lint: style
python3 -m pylint --rcfile=pylint3.cfg \
--reports=no \
--ignore=assets.py \
lobster util tests-system/run_tool_tests.py
lobster util

lint-system-tests: style
python3 -m pylint --rcfile=tests-system/pylint3.cfg \
--reports=no \
tests-system/systemtestcase.py \
tests-system/asserter.py \
tests-system/lobster-json

trlc:
trlc lobster --error-on-warnings --verify

style:
@python3 -m pycodestyle lobster \
@python3 -m pycodestyle lobster tests-system \
--exclude=assets.py

packages:
Expand Down Expand Up @@ -66,6 +73,7 @@ integration-tests: packages

system-tests:
mkdir -p docs
python -m unittest discover -s tests-system -v -t .
make -B -C tests-system TOOL=lobster-report
make -B -C tests-system TOOL=lobster-json
make -B -C tests-system TOOL=lobster-trlc
Expand Down Expand Up @@ -118,6 +126,7 @@ docs:
rm -rf docs
mkdir -p docs
@-make tracing
@-make tracing-stf

tracing:
@mkdir -p docs
Expand Down Expand Up @@ -166,8 +175,22 @@ unit-tests.lobster-%:
lobster-python --activity --out unit-tests.lobster tests-unit/lobster-$(TOOL_NAME)

system-tests.lobster-%:
$(eval TOOL_PATH := $(subst -,/,$*))
python3 tests-system/lobster-trlc-system-test.py $(TOOL_PATH);
lobster-python --activity --out=system-tests.lobster tests-system/lobster-$*

# STF is short for System Test Framework
STF_TRLC_FILES := $(wildcard tests-system/*.trlc)
STF_PYTHON_FILES := $(filter-out tests-system/test_%.py tests-system/run_tool_tests.py, $(wildcard tests-system/*.py))

# This target is used to generate the LOBSTER report for the requirements of the system test framework itself.
tracing-stf: $(STF_TRLC_FILES)
lobster-trlc tests-system lobster/tools/requirements.rsl --config-file=lobster/tools/lobster-trlc-system.conf --out=stf_system_requirements.lobster
lobster-trlc tests-system lobster/tools/requirements.rsl --config-file=lobster/tools/lobster-trlc-software.conf --out=stf_software_requirements.lobster
lobster-python --out=stf_code.lobster --only-tagged-functions $(STF_PYTHON_FILES)
lobster-report --lobster-config=tests-system/stf-lobster.conf --out=stf_report.lobster
lobster-online-report stf_report.lobster
lobster-html-report stf_report.lobster --out=docs/tracing-stf.html
@echo "Deleting STF *.lobster files..."
rm -f stf_system_requirements.lobster stf_software_requirements.lobster stf_code.lobster stf_report.lobster

clean-coverage:
@rm -rf htmlcov
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ The individual PyPI packages that `bmw-lobster` depends on are:

* [Code Coverage Report](https://bmw-software-engineering.github.io/lobster/htmlcov/index.html)
* [Coding Guideline](CODING_GUIDELINE.md)
* [Requirements Guideline](lobster/tools/REQUIREMENTS.md)
* [System Test Framework](tests-system/README.md)

#### Requirements Coverage

Expand Down
1 change: 1 addition & 0 deletions lobster/tools/lobster-trlc-software.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
req.Software_Requirement {
description = description
tags = derived_from
}
16 changes: 12 additions & 4 deletions lobster/tools/requirements.rsl
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ enum Reason {
}

type System_Requirement {
description '''
description
'''
The content of the requirement.
A tool requirement describes the behavior of a lobster tool from the point of view of the user.
It does not describe implementation details.
''' String
'''
String
}

type System_Requirement_Aspect extends System_Requirement {
Expand All @@ -36,10 +38,16 @@ type System_Requirement_Aspect extends System_Requirement {
}

type Software_Requirement {
description '''
description
'''
The content of the requirement.
A software requirement describes the behavior of a class method or module function.
''' String
'''
String

derived_from
'''List of system requirements that resulted into this software requirement'''
optional System_Requirement[1..*]
}

type Definition {
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
trlc>=2.0.1
requests>=2.31.0
libcst>=1.1.0
PyYAML>=6.0.2
2 changes: 1 addition & 1 deletion tests-system/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
!**/expected-output/*
!lobster-html-report/**/input/*.lobster
!lobster-online-report/**/input/*.lobster
!lobster-online-report/**/input/*.lobster
83 changes: 82 additions & 1 deletion tests-system/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,84 @@
The script `run_tool_tests.py` executes system tests.
System tests run a tool and verify their output.
They are different from unit tests, which test single Python functions within a tool.

It runs on Windows and Linux.

# Features
This section describes the features of the LOBSTER system test framework.

It consists of these main classes: `Asserter`, `SystemTestCase` and `TestRunner`.

The `SystemTestCase` class inherits from `unittest` and takes care to provide
a temporary directory for the tool under test.

The `TestRunner`
- provides logic to prepare the temporary directory such that it
contains necessary input files,
- provides ways to set command line arguments,
- and can execute the tool under test.

The `Asserter` is a convenience class which implements assertions that are commonly
needed to verify the test result.

## Test Setup
During test setup, features of `TestRunner` are:
- enrich `*.lobster` files with current git hashes
(not yet implemented, needed to test `lobster-online-report`),
- copy input files to a temporary directory,
- and use the temporary directory as the current working directory.

Notes:
- For some tests it is mandatory to copy the input files into the current working
directory, for others it is not.
The current implementation of the system test framework *always* copies the input
files into the temporary directory.
This should be optimized in the future.
For example, a test might verify that the tool under test scans the current working
directory for input files.
In this case it is necessary to prepare the directory properly.
On the other hand, a test that verifies the output based on a certain input
can have that input located anywhere in the file system.
Here it is not necessary to copy the files.
- The tool `lobster-html-report` should be executed in a virtual environment
where one can control whether `graphviz` is available.
The tool behaves differently whether the package is or is not available.
Hence the system test framework should control the creation of the virtual
environment.
This is currently not supported.

## Test Execution
After test execution the `Asserter` class can compare actual values of the following data against their expectation values:
- `STDOUT`
- `STDERR`
- the tool exit code
- and all `*.lobster` and `*.html` files generated by the tool under test.

The class replaces the placeholder string `TEST_CASE_PATH` in `*.lobster` files with
actual paths of the current environment to ease the comparison of paths.
It also replaces all pairs of `\` with `/` for the same reason
(so `\\` becomes `/`).

The `TestRunner` measures the branch coverage.

## Teardown
During teardown the `SystemTestCase` class deletes every temporary folder.
Any files that the tool under test has created outside of this folder will not be
deleted and must be cleaned up separately.

# Traceability
The traces from system test cases to requirements can be collected by running `lobster-python`.
The target `system-tests.lobster-%` in the [Makefile](../Makefile) executes
`lobster-python` for a particular tool.
For example
```bash
> make system-tests.lobster-json
```
will create a file called `system-tests.lobster` which contains LOBSTER items that
- represent the system test cases of `lobster-json`,
- and contain information about the linked requirements.

# Deprecated Framework
The deprecated script `run_tool_tests.py` also executes system tests.
It can execute system tests for all lobster tools.

The one and only command line argument to the script must be the name of the tool under
Expand Down Expand Up @@ -43,3 +123,4 @@ folder.
The expected values will all be compared against their actual values, and test cases
only count as "passed" if the values match.

Each test case using this deprecated approach shall be migrated to the above approach using `unittest`.
File renamed without changes.
87 changes: 87 additions & 0 deletions tests-system/asserter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from subprocess import CompletedProcess
from unittest import TestCase
from .testrunner import TestRunner


# pylint: disable=invalid-name


# Setting this flag will tell unittest not to print tracebacks from this frame.
# This way our custom assertions will show the interesting line number from the caller
# frame, and not from this boring file.
__unittest = True


class Asserter:
def __init__(self, system_test_case: TestCase, completed_process: CompletedProcess,
test_runner: TestRunner):
self._test_case = system_test_case
self._completed_process = completed_process
self._test_runner = test_runner

def assertExitCode(self, expected: int, msg="Exit code differs"):
# lobster-trace: system_test.Compare_Exit_Code
self._test_case.assertEqual(
expected,
self._completed_process.returncode,
msg,
)

def assertStdOutText(self, expected: str, msg="STDOUT differs"):
# lobster-trace: system_test.Compare_Stdout
self._test_case.assertEqual(expected, self._completed_process.stdout, msg)

def assertStdErrText(self, expected: str, msg="STDERR differs"):
# lobster-trace: system_test.Compare_Stderr
self._test_case.assertEqual(expected, self._completed_process.stderr, msg)

def assertNoStdErrText(self, msg="STDERR contains output"):
self.assertStdErrText("", msg)

def assertOutputFiles(self):
"""For each expected file, checks if an actual file has been created with the
expected content
Before comparing the actual text with the expected text, we do the
following replacements:
a) Replace Windows-like slashes \\ with / in order to be able to
compare the actual output on all OS against the expected output on
Linux
b) Replace the fixed string TEST_CASE_PATH with the absolute path to
the current working directory. This is necessary for tools like
lobster-cpptest which write absolute paths into their *.lobster
output files.
"""
# lobster-trace: system_test.Compare_Output_Files
for expected_file_ref in self._test_runner.tool_output_files:
expected_location = self._test_runner.working_dir / expected_file_ref.name
try:
with open(
expected_file_ref,
"r",
encoding="UTF-8",
) as expected_file:
try:
with open(
expected_location,
"r",
encoding="UTF-8",
) as actual_file:
# lobster-trace: system_test.Slashes
modified_actual = actual_file.read().replace("\\\\", "/")

# lobster-trace: system_test.CWD_Placeholder
modified_expected = expected_file.read().replace(
"CURRENT_WORKING_DIRECTORY",
str(self._test_runner.working_dir),
)
self._test_case.assertEqual(
modified_actual,
modified_expected,
f"File differs from expectation {expected_file_ref}!",
)
except FileNotFoundError:
self._test_case.fail(f"File {expected_file_ref} was not "
f"generated by the tool under test!")
except FileNotFoundError as ex:
self._test_case.fail(f"Invalid test setup: {ex}")
File renamed without changes.
1 change: 1 addition & 0 deletions tests-system/lobster-json/data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!*.lobster
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"data": [
{
"tag": "json data.json:apple",
"tag": "json valid-json.txt:banana",
"location": {
"kind": "file",
"file": "data.json",
"file": "valid-json.txt",
"line": null,
"column": null
},
"name": "data.json:apple",
"name": "valid-json.txt:banana",
"messages": [],
"just_up": [],
"just_down": [],
Expand All @@ -18,14 +18,14 @@
"status": null
},
{
"tag": "json data.txt:banana",
"tag": "json valid-mini.json:apple",
"location": {
"kind": "file",
"file": "data.txt",
"file": "valid-mini.json",
"line": null,
"column": null
},
"name": "data.txt:banana",
"name": "valid-mini.json:apple",
"messages": [],
"just_up": [],
"just_down": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"data": [
{
"tag": "json data.txt:cherry",
"tag": "json valid-json.txt:banana",
"location": {
"kind": "file",
"file": "data.txt",
"file": "valid-json.txt",
"line": null,
"column": null
},
"name": "data.txt:cherry",
"name": "valid-json.txt:banana",
"messages": [],
"just_up": [],
"just_down": [],
Expand Down
Loading

0 comments on commit 0852bd1

Please sign in to comment.