├── .gitignore
├── ARC42_DOC.md
├── LICENSE
├── README.md
├── README_german.md
├── example
├── 0_live_demo
│ ├── LIVE-DEMO.md
│ ├── normal-run
│ │ ├── log.html
│ │ ├── output.xml
│ │ ├── perfbot-graphics
│ │ │ ├── boxplot-01-24-2025-20-54-22-909875.png
│ │ │ └── boxplot-01-24-2025-20-54-23-352049.png
│ │ └── report.html
│ └── slow_run
│ │ ├── log.html
│ │ ├── output.xml
│ │ ├── perfbot-graphics
│ │ ├── boxplot-01-24-2025-20-57-06-861135.png
│ │ └── boxplot-01-24-2025-20-57-07-250893.png
│ │ └── report.html
├── log.html
├── output.xml
├── perfbot-graphics
│ ├── boxplot-01-04-2025-13-35-36-026603.png
│ ├── boxplot-01-04-2025-13-35-36-462290.png
│ ├── boxplot-05-19-2023-18-01-09-186797.png
│ └── boxplot-05-19-2023-18-01-09-531645.png
├── report.html
├── robot-exec-times.db
├── sut
│ ├── html
│ │ ├── demo.css
│ │ ├── error.html
│ │ ├── index.html
│ │ └── welcome.html
│ └── server.py
└── tests
│ ├── invalid_login.robot
│ ├── resource.robot
│ └── valid_login.robot
├── perfbot
├── PerfEvalResultModifier.py
├── PerfEvalVisualizer.py
├── PersistenceService.py
├── Sqlite3PersistenceService.py
├── __init__.py
├── model.py
├── perfbot.py
├── requirements.txt
├── schema.sql
└── tests
│ ├── __init__.py
│ ├── __main__.py
│ ├── golden.xml
│ ├── goldenTwice.xml
│ ├── run.py
│ ├── test_PerfEvalVisualizer.py
│ └── test_PersistenceService.py
├── res
├── architektur_high_level.png
├── example-test-suite-summary.png
├── example-testbreaker.png
├── legende.png
├── logo.png
├── perfbot_hochkant.png
├── perfbot_laufzeitsicht_details.svg
└── perfbot_laufzeitsicht_ueberblick.svg
├── setup.py
└── tests
├── Testplan.md
└── itests
└── Integrationstest.robot
/.gitignore:
--------------------------------------------------------------------------------
1 | ## see: https://github.com/github/gitignore/blob/main/Python.gitignore
2 | # Byte-compiled / optimized / DLL files
3 | __pycache__/
4 | *.py[cod]
5 | *$py.class
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | .pybuilder/
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | # For a library or package, you might want to ignore these files since the code is
88 | # intended to run in multiple environments; otherwise, check them in:
89 | # .python-version
90 |
91 | # pipenv
92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
95 | # install all needed dependencies.
96 | #Pipfile.lock
97 |
98 | # poetry
99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100 | # This is especially recommended for binary packages to ensure reproducibility, and is more
101 | # commonly ignored for libraries.
102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103 | #poetry.lock
104 |
105 | # pdm
106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107 | #pdm.lock
108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109 | # in version control.
110 | # https://pdm.fming.dev/#use-with-ide
111 | .pdm.toml
112 |
113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114 | __pypackages__/
115 |
116 | # Celery stuff
117 | celerybeat-schedule
118 | celerybeat.pid
119 |
120 | # SageMath parsed files
121 | *.sage.py
122 |
123 | # Environments
124 | .env
125 | .venv
126 | env/
127 | venv/
128 | ENV/
129 | env.bak/
130 | venv.bak/
131 |
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 |
136 | # Rope project settings
137 | .ropeproject
138 |
139 | # mkdocs documentation
140 | /site
141 |
142 | # mypy
143 | .mypy_cache/
144 | .dmypy.json
145 | dmypy.json
146 |
147 | # Pyre type checker
148 | .pyre/
149 |
150 | # pytype static type analyzer
151 | .pytype/
152 |
153 | # Cython debug symbols
154 | cython_debug/
155 |
156 | # PyCharm
157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159 | # and can be added to the global gitignore or merged into this file. For a more nuclear
160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161 | #.idea/
162 |
163 | .vscode/*
164 | .vscode/
165 | !.vscode/settings.json
166 | !.vscode/tasks.json
167 | !.vscode/launch.json
168 | !.vscode/extensions.json
169 | !.vscode/*.code-snippets
170 |
171 | # Local History for Visual Studio Code
172 | .history/
173 |
174 | # Built Visual Studio Code Extensions
175 | *.vsix
176 |
177 | # General
178 | .DS_Store
179 | .AppleDouble
180 | .LSOverride
181 |
182 | # Icon must end with two \
183 | Icon
184 |
185 |
186 | # Thumbnails
187 | ._*
188 |
189 | # Files that might appear in the root of a volume
190 | .DocumentRevisions-V100
191 | .fseventsd
192 | .Spotlight-V100
193 | .TemporaryItems
194 | .Trashes
195 | .VolumeIcon.icns
196 | .com.apple.timemachine.donotpresent
197 |
198 | # Directories potentially created on remote AFP share
199 | .AppleDB
200 | .AppleDesktop
201 | Network Trash Folder
202 | Temporary Items
203 | .apdisk
204 |
205 | # Own
206 | jupyter-workspace/
--------------------------------------------------------------------------------
/ARC42_DOC.md:
--------------------------------------------------------------------------------
1 | # Perfbot - Architekturdokumentation nach arc42
2 |
3 | # Einführung und Ziele
4 |
5 | ## Aufgabenstellung
6 |
7 | **Perfbot** ermittelt Performance-Veränderungen anhand von bestehenden automatisierten UI-Tests. Er erweitert dabei das [Robot Framework](http://www.robotframework.org) um die Möglichkeit, Test-Laufzeiten in einer Datenbank zu speichern und mit den archivierten Laufzeiten der Vergangenheit zu vergleichen. Das Ergebnisse der Performance-Analyse wird in die Robot-Testresults (`log.html` / `report.html`) integriert.
8 |
9 | ## Qualitätsziele
10 |
11 | Hier sind die wichtigsten Qualitätsziele beschrieben. Eine detaillierte Betrachtung der Qualität erfolgt im Kapitel Qualitätsanforderungen (s. u.).
12 |
13 | ID | Qualitätsziel | Motivation und Erläuterung
14 | ---|---------------------------------------------------------------------------------- | ----------------------------------------------
15 | Q1 | Vergleich der Laufzeiten von Robot-Testfällen ermöglichen (Funktionalität) | Die Kernaufgabe des Tool ist es, eine Performance-Analyse zu bieten.
16 | Q2 | Integration ins Robot Framework (Kompatibilität) | Das Tool soll ohne Veränderung der bestehenden Robot-Tests genutzt werden können. Die Ergebnisse sollen in die Standard-Ergebnisdokumente integriert werden.
17 | Q3 | Performantes Tool ohne Verlangsamung der eigentlichen Testausführung (Performance) | Die Tool soll die Ausführung der Tests nicht verlangsamen, auch um keine Seiteneffekte auf die Messung zu haben. Auch das Tool selbst soll performant sein z. B. nicht zu viele Datenbankzugriffe tätigen.
18 |
19 |
20 | ## Stakeholder
21 |
22 | Rolle | Erwartungshaltung
23 | ---------------------------|-----------------------
24 | Testautomatisierer | möchte Hinweise zur Performance der Testfälle bzw. über Performance-Veränderungen des Testobjektes; erwartet eine einfache Integration in sein bisheriges Robot-Setup
25 | Entwicklungsteam | möchte Performance-Probleme frühzeit entdecken; erhofft sich detaillierte Infos an welcher Komponente die Performance schlechter ist
26 | Anwendungsverantwortlicher (Auftraggeber) | möchte frühzeit über Performance-Probleme in Kenntnis gesetzt werden; möchte Nachweis über Performance des Anwendung
27 | Testmanagement / TA-Team | möchte das Qualitätsziel "Performance" im Unternehmen stärker in den Fokus setzen; möchte mit reduziertem Aufwand die funktionalen Tests um nicht-funktionale Kennzahlen erweitern
28 | Forscher (Masterarbeit) | möchte beurteilen, ob Kennzahlen zu bestehender UI-Tests brauchbar sind, um Aussagen über die Performance eines Testobjekts zu treffen
29 |
30 | # Randbedingungen
31 |
32 | # Kontextabgrenzung
33 |
34 | Hinweis: Da die Funktionalität dieses Werkzeugs selbst für den Softwaretest genutzt werden soll, ist eine Abgrenzung zwischen fachlichen und technischen Kontext an dieser Stelle schwierig.
35 |
36 | ## Fachlicher Kontext
37 |
38 | **Was bietet das Tool?**
39 |
40 | - Performance-Veränderungen zu bestehen Robot-Testfällen basierend auf archivierten Testläufen ermitteln
41 | - Daten des aktuellen Testlaufs archivieren
42 | - Performance-Analyse in die Robot-Result-Dateien aufbereiten
43 | - durch tabellarische Darstellung von Vergleichskennzahlen
44 | - durch grafische Darstellung der Testlaufzeiten im Box-Plot
45 | - Testfälle bei prozentualer Abweichung von den archivierten Testläufen auf FAIL setzten (Testbreaker)
46 |
47 | **Wohin bestehen Schnittstellen?**
48 |
49 | - zum Robot Framework und der dazugehörige Testausführungs- `robot` bzw. Testberichtgenierungs-Werkzeug `rebot`
50 | - zur Datenbank, die sich um die Persisitierung kümmert
51 |
52 | **Was bietet das Tool nicht?**
53 |
54 | - es führt selbst keine automatisierten Testfälle aus
55 | - es führt keine Last- und Performancetests im engeren Sinne durch (kann aber genutzt werden, um zu entscheiden, wo Last- und Performancetests mit dafür geeigneten Werkzeugen erfolgen sollen)
56 |
57 | ## Technischer Kontext
58 |
59 | Das Tool `Perfbot` (orange) integriert sich auf der Testausführungsschicht (schwarz) in das Robot Framework und nutzt dabei die Schnittstelle (API) des Frameworks.
60 |
61 | 
62 |
63 | # Lösungsstrategie
64 |
65 | Die grundlegende Lösungsidee zur Performance-Analyse unter Nutzung bestehender UI-Tests ist folgende: Eine generische Erweiterung des im Unternehmen verbreiteten Testautomatisierungswerkzeugs Robot Frameworks zu schaffen. Dadurch sollen Synergieeffekte durch Nutzung bestehender Testfälle und dem bestehenden Knowhow der Testautomatisierer gehoben werden.
66 | Konkret liegt beim Performance-Vergleich folgende Umsetzungsidee vor:
67 | - speichern der Test-Laufzeiten in einer Datenbank
68 | - vergleichen der aktuellen Laufzeit mit den archivierten Laufzeiten der Vergangenheit
69 | - integrieren der Ergebnisse der Performance-Analyse in die Robot-Testresults (`log.html` / `report.html`)
70 |
71 | Die Performance-Analyse soll durch die drei folgenden wichtigsten Funktionen erfolgen:
72 | - Vergleichskennzahlen: Tabellarische Darstellung verschiedene Kennzahlen (Minimum, Maximum, Durchschnitt, Abweichung vom Durchschnitt) zur früheren Laufzeiten
73 | - Box-Plot: grafische Aufbereitung der Laufzeiten der Vergangenheit zu jedem Testfall
74 | - Testbreaker: Testfälle werden als fehlerhaft markiert, wenn ein Schwellwert zu Abweichung von vergangen Test-Laufzeiten überschritten wird.
75 |
76 | Die Integration in das Robot Framework ist die wesentliche Technologieentscheidung, dadurch kann die bestehende API des Robot Frameworks genutzt werden. Gleichzeitig schafft dies auch eine klare Abhängigkeit zum Framework und gewisse Technologievorgaben z. B. die Nutzung von Python als Programmiersprache sind damit vorbestimmt.
77 |
78 |
79 | # Bausteinsicht
80 |
81 | ## Komponentendiagramm (Whitebox Gesamtsystem)
82 |
83 | ```plantuml
84 |
85 | node "Robot Framework" {
86 | () API
87 | }
88 |
89 | node Perfbot {
90 |
91 | }
92 |
93 | database Sqlite3 {
94 |
95 | }
96 |
97 | [Perfbot] ..> () API : uses
98 |
99 | [Perfbot] ..> () Sqlite3 : uses
100 |
101 | ```
102 |
103 | Siehe auch High-Level-Architektur unter "Technischer Kontext"
104 |
105 | **Begründung für diese Darstellungsweise**
106 |
107 | Das Komponentendiagramm fasst die wesentlichen Komponenten zusammen.
108 |
109 | **Enthaltene Bausteine**
110 |
111 |
112 | Bastein | Erläuterung
113 | ---------------------------|-----------------------
114 | perfbot | Perfbot Python-Modul als Blackbox
115 |
116 | **Wichtige Schnittstellen**
117 |
118 | Schnittstelle | Erläuterung
119 | ---------------------------|-----------------------
120 | Datenbank (hier Sqlite3) | Für die Speicherung der Testläufe wird eine Datenbank genutzt. Hier wird beispielsweise eine Sqlite3-DB unterstützt.
121 | Robot Framework API | Die Ausführung des Perfbots wird durch die API des Robot Frameworks getriggert.
122 |
123 | ## Klassendiagramm
124 |
125 | ```plantuml
126 |
127 |
128 | class robot.api.ResultVisitor
129 | class perfbot.PerfEvalResultModifier
130 | class perfbot.perfbot #DDDDDD
131 | abstract perfbot.PersistenceService
132 | class perfbot.PerfEvalVisualizer
133 | class perfbot.Sqlite3PersistenceService
134 |
135 | perfbot.PerfEvalResultModifier <|-- perfbot.perfbot
136 | robot.api.ResultVisitor <|-- perfbot.PerfEvalResultModifier
137 |
138 | perfbot.PerfEvalResultModifier o-- perfbot.PersistenceService
139 | perfbot.PerfEvalResultModifier o-- perfbot.PerfEvalVisualizer
140 | perfbot.PersistenceService <|-- perfbot.Sqlite3PersistenceService
141 |
142 | note left of perfbot.perfbot: Starter
143 |
144 | ```
145 |
146 | **Begründung für diese Darstellungsweise**
147 |
148 | Das Klassendiagramm gibt einen detaillierten Überblick über die Klassen und damit über die danach gegliederten Quellcode-Dateien.
149 |
150 | **Enthaltene Bausteine**
151 |
152 | Bastein | Erläuterung
153 | ---------------------------|-----------------------
154 | ResultVisitor | Teil der Robot-API; ermöglicht das Iterieren über die Testergebnisse vor der Report-Generierung
155 | perfbot | Wrapper, damit der Aufruf mit dem Parameter --prerebotmodifier perfbot.perfbot aufgerufen werden kann. Eigentliche Logik siehe PerfEvalResultModifier.
156 | PerfEvalResultModifier | übernimmt die eigentliche Verarbeitungslogik des Perfbots nach dem Aufruf durch rebot.
157 | PerfEvalVisualizer | übernimmt die visuelle Aufbereitung z. B. in Box-Plots von Performancedaten der Testfälle.
158 | PersistenceService | Abstrakte Klasse, um die eigentliche Implementierung, wie die Testlaufergebnisse gespeichert bzw. abgerufen werden zu verschleiern.
159 | Sqlite3PersistenceService | Konkrete Persistierung der Testergebnisse in einer lokalen Sqlite3-Datei.
160 |
161 |
162 | # Laufzeitsicht
163 |
164 | ## Laufzeitsenario Überblick: Testausführung und Berichtgenierung im Überblick
165 |
166 | 
167 |
168 | **Begründung für diese Darstellungsweise**
169 |
170 | Gibt einen Überblick, wie Perfbot sich in die Kernfunktionen, die das Robot Framework bereitstellt, integriert.
171 |
172 | ## Laufzeitsicht Details: Testausführung und Berichtgenerierung im Detail
173 |
174 | 
175 |
176 | **Begründung für diese Darstellungsweise**
177 |
178 | Zeigt den zeitlichen Ablauf und die Triggerpunkte, wo die verschiedenen Perfbot-Funktionen aufgerufen werden.
179 |
180 |
181 | # Verteilungssicht
182 |
183 | - siehe Komponentendiagramm oben
184 | - Offenes TODO: Perfbot als PyPI Paket verfügbar zu machen
185 |
186 | # Architekturentscheidungen
187 |
188 | An dieser Stelle sind die wichtigsten Architekturentscheidungen aufgelistet:
189 |
190 | ID | Zusammenfassung | Erläuterung/Begründung
191 | -----|-------------------------------------- | ----------------------------------------------
192 | ADR1 | Integration in rebot-Schritt | Durch die Anbindung in den Rebot-Schritt erfolgt die Ausführung von Perfbot nachgelagert zur Testausführung. Dadurch werden Seiteneffekte (Verlangsamung oder Fehler) auf die eigentliche TA vermieden. Ein Nachteil ist jedoch, dass damit Erkenntnisse des Testbreakers nicht auf der CLI oder in der output.xml berücksichtigt werden. Alternativ kann jedoch mit `rebot`auch eine neue aktualisierte `output.xml`erzeugt werden.
193 | ADR2 | Integration in die Robot-Reports | Durch die Integration in die `report.html`und `log.html` werden dem Testautomatisierer die Performance-Analyse in die bekannten Ergebnisdateien angezeigt. Er muss keine weiteren Dateien betrachten. Der Gestaltungsfreiraum innerhalb dieser Dokumente ist jedoch dabei etwas beschränkt z. B. können Metadaten-Informationen nur an die Testsuite und nicht an Testfälle gehangen werden. Zudem muss auf den vorhandenen Teststatus-Vorrat (PASS, FAIL, SKIP) für den Testbreaker zurückgegriffen werden.
194 | ADR3 | Eigene DB statt TestArchiver | Der TestArchiver bietet ein umfassendes Schema für die Speicherung von Testergebnissen inkl. der automatischen Persisitierung durch einen eigenen Listener. Beim Test dieser Tools musste jedoch festgestellt werden, dass die entscheidende Tabelle mit den Ergebnissen der Testfälle nicht gefüllt werden. Deshalb wurde für den MVP eine eigenen DB aufgesetzt. Abhängig von den Erweiterungsoptionen sollte jedoch die Nutzung des TestArchivers (z. B. durch einen Fork) geprüft werden.
195 | ADR4 | Sqlite3 als erste Persistierung | Für den MVP-Ansatz wurde das Datei-Datenbanksystem Sqlite3 ausgewählt, da es schnell einzurichten ist und das notwendige Python-Modul bereits im Python-Standard-Paket inkludiert ist. Die Erweiterung auf ein "echtes" DBMS wird angestrebt und sollte durch die erweitere Schnittstellendesign problemlos möglich sein.
196 |
197 | # Qualitätsanforderungen
198 |
199 | ## Qualitätsbaum
200 |
201 | Im der folgenden Grafik - dem sogenannten Qualitätsbaum (englisch: Utitlty Tree) - werden den Qualitätsmerkmalen den die Qualitätsziele (Qx) aus dem ersten Kapitel und die unten beschriebenen Qualitätsszenarien zugeordnet (Mehrfachnennung möglich).
202 |
203 | ```plantuml
204 | @startmindmap
205 | * Qualität
206 | ** Funktionalität
207 | ***_ Q1
208 | ***_ F1
209 | ***_ F2
210 | ***_ F3
211 | ***_ F4
212 | ** Performance
213 | ***_ Q3
214 | ** Kompatibilität
215 | ***_ Q2
216 | ***_ K1
217 | ** Benutzbarkeit
218 | ***_ B1
219 | ***_ B2
220 | ** Zuverlässigkeit
221 | ***_ Z1
222 | ** Sicherheit
223 | ***_ Z2
224 | ** Wartbarkeit
225 | ***_ W1
226 | ***_ W2
227 | ** Übertragbarkeit
228 | ***_ Ü1
229 | ***_ Ü2
230 | ***_ Ü3
231 | ***_ Ü4
232 | @endmindmap
233 | ```
234 |
235 |
236 | ## Qualitätsszenarien
237 |
238 | Konkrete Szenarien werden entweder als Nutzungs-/Anwendungsfall oder als Änderungsszenario, was passiert mit der Qualität bei Weiterentwicklung, angegeben. Die Anfangsbuchstabe der ID soll die Zuordnung zum (am besten passenden) Qualitätsmerkmale verdeutlichen.
239 |
240 | ID | Szenario
241 | ----|-----------------------
242 | F1 | Jeder Testausführung wird für die spätere Performance-Analyse archiviert.
243 | F2 | Die Funktion des Testbreaker lässt sich zeigen, wenn beispielweise ein Sleep in einen Testfall eingebaut wird.
244 | F3 | Die verschiedenen Testläufe eines Testfalls werden im Box-Plot dargestellt.
245 | F4 | Die prozentuale Abweichung vom Durchschnitt zu den vergangen Testläufen wird angezeigt.
246 | B1 | Der Testautomatisierer möchte das Tool ohne umfangreiche Kenntnisse in seinen Testausführungs-CLI-Befehl integrieren.
247 | B2 | Der Testautomatisierer erwartet die Performance-Analyse in den gewohnten Ergebnisdateien.
248 | K1 | Das Tool stellt keine Anforderungen oder Änderungen an die beschriebene Testspezifikation (`*.robot`).
249 | Z1 | Fehler im Perfbot gefährden nicht die eigentliche Testdurchführung z. B. sollen keine Ergebnisse einer langlaufenden Testsuite aufgrund eines Fehlers im Tool verloren gehen.
250 | W1 | Ein Entwickler erwartete eine gute Dokumentation und Struktur des Quellcodes bzw. Repos.
251 | W2 | Der Entwickler erwartet vorhandene Regressionstests und die Nutzung von statischer Codeanalyse.
252 | Ü1 | Das Tool soll auf einem bestehenden System mit Python/PiP installierbar sein.
253 | Ü2 | Der Tool soll nicht auf die Testfälle eines Unternehmens beschränkt sein.
254 | Ü3 | Das Tool ist um andere Persistierungsmöglichkeiten z. B. das DBMS MongoDB erweiterbar.
255 | Ü4 | Die Performance-Analyse kann auch auf Schlüsselwörter (oder andere Objekte) erweitert werden. (bereits geschehen)
256 |
257 | ### Tests
258 |
259 | Die Qualität wird mittels automatisierten Regressionstests laufend betrachtet. Einen Überblick über die statische Codeanalyse, Unit- und Integrationstests gibt der [Testplan](tests/Testplan.md).
260 |
261 |
262 | # Risiken und technische Schulden
263 |
264 | Kernrisiko ist, dass die Lösungsidee bzw. die Prämisse nicht trägt. D. h. dass sich bestehende UI-Tests nicht eignen, um Aussagen über die Performance des Testobjektes zu treffen. Dem Gegenüber steht jedoch die Chance, dass die Lösungsidee trägt. Zudem ist die Überprüfung der Prämisse durch die Entwicklung des Perfbots Teil der Forschungsfrage der Masterthesis.
265 |
266 | Die technischen Schulden der jeweiligen Entscheidungen sind bei den Architekturentscheidungen als Nachteile formuliert (s. o.).
267 |
268 | # Glossar
269 |
270 | Begriff | Definition
271 | ------------|-----------------------
272 | -/- | -/-
273 |
274 | # Quellen
275 |
276 | - diese Markdown-Dokument basiert auf folgender Vorlage:
277 | - Template Version 8.2 DE. (basiert auf AsciiDoc Version), Januar 2023, Created, maintained and © by Dr. Peter Hruschka, Dr. Gernot Starke and contributors. Siehe https://arc42.org.
278 | - Deutsches Beispiel für die Arc42-Dokumentation von Stefan Zörner: https://www.dokchess.de
279 | - Merkmale der Produktqualität nach ISO 25010 vgl. Seidl et al., Basiswissen Testautomatisierung, , S. 30
280 |
281 |
282 |
283 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Lennart Potthoff
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Perfbot - Robot Framework Performance Analyser
3 |
4 |
5 |
6 |
7 | **Perfbot** determines performance regression based on existing automated UI tests. The tool extends the [Robot Framework](http://www.robotframework.org) by the possibility to store test runtimes in a database and compare them with archived runtimes of the past. The results of the performance analysis are integrated into the Robot test results (`log.html` / `report.html`).
8 |
9 | ---
10 |
11 | For the german version of this quickstart see: [README_german.md](README_german.md)
12 |
13 | ---
14 |
15 | ## Installation
16 | Requires `python` und `pip` in Version 3.10. or higher
17 |
18 | Clone Repository and run this command:
19 | ```bash
20 | python setup.py install
21 | # or
22 | pip install [path to perfbot-folder]
23 | ```
24 |
25 | ## Quickstart
26 |
27 | Start the robot test cases including perfbot:
28 | ```bash
29 | robot --prerebotmodifier perfbot.perfbot [path to tests]
30 | ```
31 | ## Functionality
32 |
33 | **Perfbot** uses the `ResultVisitor` of the Robot API to iterate over the tests or their results and store them in a database. Based on the past test runs from the database, the current runtime of the tests are analyzed suite-wise and the result is written as metadata to the report or log file.
34 | The following additional functions are available:
35 | - **Box Plot** (enabled by default): A [box plot](https://de.wikipedia.org/wiki/Box-Plot) is generated for each test case, which graphically displays the statistical distribution of runtimes in quartiles. The current execution time of the test is marked with the dot. The box plot generation can be disabled due to its partly long-running generation (see configuration).
36 | - **Testbreaker** (deactivated by default, activation see configuration): The testbreaker compares the test duration of each test case with a maximum value of the percentage deviations from the average of the last runs. If this indicates a performance problem, the test case is set to FAIL.
37 | - **Keyword archiving** (enabled by default, under development): In addition to the runtimes of the test cases, the runtimes of the underlying keywords are also interesting. For this purpose, these runtimes are also stored in the database. To view the keyword runtimes, another tool called [Perfmetrics](https://git.fh-muenster.de/robotframework-performance/perfmetrics) (prototype, not published yet) has been added. This allows a detailed view of the performance of test and keywords.
38 |
39 |
40 | ## Configuration
41 |
42 | Starting the robot test cases incl. perfbot (by default with box plot mode and without testbreaker mode):
43 | ```bash
44 | robot --prerebotmodifier perfbot.perfbot [path to tests]
45 | ```
46 |
47 | Starting Perfbot with all possible parameters
48 | ```bash
49 | robot --prerebotmodifier perfbot.perfbot:stat_func='avg':devn=0.1:db_path="example/robot-exec-times.db":boxplot=True:boxplot_folder="perfbot-graphics/":testbreaker=True:keywordstats="True":readonly="False" [path to tests]
50 | ```
51 |
52 | Running Perfbot with rebot for generating a new output.xml
53 | ```bash
54 | rebot --prerebotmodifier perfbot.perfbot:devn=0.1:db_path="example/robot-exec-times.db":testbreaker=True --output example/newoutput.xml example/output.xml
55 | ```
56 |
57 | ## Example Login-Page
58 | The `./example` folder contains sample test cases from the Selenium library repo (taken from https://github.com/robotframework/SeleniumLibrary). The tests were executed several times with Perfbot. Also there are the corresponding database `robot-exec-times.db` and the robot test results (`log.html` / `report.html`) where the performance analysis was reported. The following command can be used to start the example (assuming the SeleniumLibrary is installed):
59 | ```bash
60 | # 1. Starting the system-under-test (Login-Page)
61 | python example/sut/server.py
62 | # 2. Running rests and perfbot
63 | robot --prerebotmodifier perfbot.perfbot:devn=0.1:db_path="example/robot-exec-times.db":testbreaker=True example/tests
64 | ```
65 | Screenshot 1: Included performance analysis in log.html. The boxplot demontrates the spread of archived test execution times. The orange dot symbolies the duration of the current test run.
66 | 
67 |
68 | Screenshot 2: Testbreaker
69 | 
70 |
71 |
72 | ## Technical documentation, testplan and references
73 |
74 | Currently only available in german: see [README_german.md](README_german.md), [ARC42_DOC.md](ARC42_DOC.md), [Testplan](tests/Testplan.md).
75 |
76 |
77 | ## License
78 | © Lennart Potthoff / MIT-Lizenz
79 |
80 | Translated with www.DeepL.com/Translator (free version)
81 |
--------------------------------------------------------------------------------
/README_german.md:
--------------------------------------------------------------------------------
1 |
2 | # Perfbot - Robot Framework Performance Analyser
3 |
4 |
5 |
6 | ---
7 |
8 | English version of this README: [README.md](README.md)
9 |
10 | ---
11 |
12 | **Perfbot** ermittelt Performance-Veränderungen anhand von bestehenden automatisierten UI-Tests. Das Werkzeug erweitert dabei das [Robot Framework](http://www.robotframework.org) um die Möglichkeit, Test-Laufzeiten in einer Datenbank zu speichern und mit den archivierten Laufzeiten der Vergangenheit zu vergleichen. Das Ergebnisse der Performance-Analyse wird in die Robot-Testresults (`log.html` / `report.html`) integriert.
13 |
14 |
15 | ## Installation
16 |
17 | Voraussetzung: `python` und `pip` ist installiert (Mindestversion 3.10 (getestet auf 3.11.2 und 3.10.11))
18 |
19 | Repository klonen und folgenden Befehl ausführen:
20 | ```bash
21 | python setup.py install
22 | # or
23 | pip install [path to perfbot-folder]
24 | ```
25 |
26 | ## Quickstart
27 |
28 | Starten der Robot-Testfälle inkl. Perfbot:
29 | ```bash
30 | robot --prerebotmodifier perfbot.perfbot [path to tests]
31 | ```
32 | ## Funktionsweise
33 |
34 | **Perfbot** nutzt den `ResultVisitor` der Robot-API, um über die Tests bzw. deren Ergebnisse zu iterieren und diese in einer Datenbank abzuspeichern. Basierend auf den vergangen Testläufen aus der Datenbank werden die aktuellen Laufzeit der Tests Suite-weise analysiert und das Ergebnis als Metadaten in die Report- bzw. Log-Datei geschrieben.
35 | Folgende weitere Funktionen stehen zur Verfügung:
36 | - **Box-Plot** (standardmäßig aktiviert): Zu jedem Testfall wird ein [Box-Plot](https://de.wikipedia.org/wiki/Box-Plot) generiert, der die statistische Verteilung der Laufzeiten in Quartile grafisch aufbereitet. Die aktuelle Ausführungszeit des Tests wird mit dem Punkt markiert. Die Box-Plot-Erstellung kann aufgrund ihrer teils langlaufenden Erstellung deaktiviert werden (siehe Konfiguration).
37 | - **Testbreaker** (standardmäßig deaktiviert, Aktivierung siehe Konfiguration): Der Testbreaker vergleicht die Testdauer jedes Testfalls mit einem Maximalwert der prozentualen Abweichungen vom Durchschnitt der letzten Läufe. Lässt sich daraus ein Performanceproblem erkennen, so wir der Testfall auf FAIL gesetzt.
38 | - **Keyword-Archivierung** (standardmäßig aktiviert, in Entwicklung): Neben den Laufzeiten der Testfälle, sind auch die Laufzeiten der darunter liegenden Keywords interessant. Dafür werden auch diese Laufzeiten in der Datenbank gespeichert. Zur Betrachtung der Keyword-Laufzeiten wurde eine weiteres Tool namens [Perfmetrics](https://git.fh-muenster.de/robotframework-performance/perfmetrics) (Veröffentlichung ausstehend). Diese ermöglicht eine detailerte Betrachtung der Performance von Test- und Schlüsselwörtern.
39 |
40 | ## Konfiguration
41 |
42 | Starten der Robot-Testfälle inkl. perfbot (standardmäßig mit Box-Plot-Modus und ohne Testbreaker-Modus):
43 | ```bash
44 | robot --prerebotmodifier perfbot.perfbot [path to tests]
45 | ```
46 |
47 | Angabe, welche [Sqlite3-Datenbank](https://docs.python.org/3/library/sqlite3.html) mit archivierten Testlaufzeiten genutzt werden soll (standardmäßig wird eine Datenbank mit dem Namen `robot-exec-times.db` erzeugt bzw. verwendet):
48 | ```bash
49 | robot --prerebotmodifier perfbot.perfbot:db_path=[path to sqlite3 file] [path to tests]
50 | ```
51 |
52 | Aktivierung des Testbreaker-Modus:
53 | Beispiel: Bei einer Abweichung (`devn`) der Testlaufzeit von 10% vom Durchschnitt der vergangen Testläufe soll der Testfall auf FAIL gesetzt werden. (Hinweis: Perfbot läuft im `rebot`-Schritt, die `output.xml` und CLI-Ausgaben werden durch den Testbreaker deshalb nicht verändert)
54 | ```bash
55 | robot --prerebotmodifier perfbot.perfbot:devn=0.1:testbreaker=True [path to tests]
56 | ```
57 |
58 | Deaktivierung und Konfiguration des Box-Plot-Modus:
59 | Die Generierung des Box-Plots benötigt weitere Datenbank-Zugriffe und zudem die Funktionen der Python-Module `pandas` und `matplotlib`. Zur Beschleunigung der Erstellung der Log- und Report-Dateien und zur Dependency-Reduzierung lässt sich der Box-Plot-Modus deaktivieren. Bei aktivierten Box-Plot-Modus kann der Ablageort mit dem Parameter `boxplot_folder` angegeben werden. Sofern der robot-Parameter `--outputdir` verwendet wird, muss der Ablageort als absoluter Pfad eingetragen werden.
60 | ```bash
61 | robot --prerebotmodifier perfbot.perfbot:boxplot="False":boxplot_folder="perfbot-graphics/" [path to tests]
62 | ```
63 |
64 | Lesender Zugriff auf Datenbank:
65 | Um lediglich die Performance zu analyisieren, jedoch nicht den aktuellen Testlauf in die Datenbank zu schreiben, so kann folgende Konfiguration genutzt werden:
66 | ```bash
67 | robot --prerebotmodifier perfbot.perfbot:readonly=True [path to tests]
68 | ```
69 |
70 | Deaktivierung der Keyword-Speicherung:
71 | Die Keyword-Analyse erfolgt nachgelagert mittels [Perfmetrics](https://git.fh-muenster.de/robotframework-performance/perfmetrics) (siehe oben). Sofern die Betrachtung der Keywords nicht relevant ist, kann zugunsten eines schnelleren Perfbots die Speicherung der Keyword-Laufzeiten deaktiviert werden:
72 | ```bash
73 | robot --prerebotmodifier perfbot.perfbot:keywordstats=False [path to tests]
74 | ```
75 |
76 | Ausführen von Perfbot mittels `rebot`:
77 | Die `log.html` und `report.html` von Robot-Testfällen können auch ohne Testausführung basierend auf der `output.xml` generiert werden.
78 | D. h. Perfbot kann nachträglich ohne Ausführung der Tests gestartet werden.
79 | Dazu ist eine bestehende `output.xml` nötig. Bei der Ausführung von Perfbot mittes `rebot` kann neben den HTML-Dokumenten auch eine neue `output.xml` erzeugt werden, die dann auch den fehlgeschlagene Tests des Testbreaker enthält.
80 | Hinweis: Standardmäßig führt jede Ausführung von Perfbot zu neuen Datensätzen, doppelte Ausführungen zu gleichen Testdurchläufen sollte deshalb vermieden werden bzw. dann der readonly-Modus genutzt werden.
81 | ```bash
82 | # Vgl. untenstehendes Beispiel
83 | rebot --prerebotmodifier perfbot.perfbot:devn=0.1:db_path="example/robot-exec-times.db":testbreaker=True --output example/newoutput.xml example/output.xml
84 | ```
85 |
86 | Ausführung von Perfbot mit allen möglichen Parameter:
87 | Hinweis zum Entwicklungsstand: Nicht zu alle Parameter sind andere Werte als die Defaults auswählbar.
88 | ```bash
89 | robot --prerebotmodifier perfbot.perfbot:stat_func='avg':devn=0.1:db_path="example/robot-exec-times.db":boxplot=True:boxplot_folder="perfbot-graphics/":testbreaker=True:keywordstats="True":readonly="False" [path to tests]
90 | ```
91 |
92 | ## Beispiel Login-Page
93 | Im Ordner `./example` sind Beispiel-Testfälle aus der Repo der Selenium-Library (entnommen aus https://github.com/robotframework/SeleniumLibrary) abgelegt. Die Tests wurden mehrmals mit Perfbot ausgeführt. Ebenfalls dort sind die dazugehörige Datenbank `robot-exec-times.db` und die Robot-Testresults (`log.html` / `report.html`), in denen die Performance-Analyse berichtet wurde, zu finden. Mit folgenden Befehl lassen sich das Beispiel starten (Installation der SeleniumLibrary vorausgesetzt):
94 | ```bash
95 | # 1. Starten des System-under-Test (Login-Page)
96 | python example/sut/server.py
97 | # 2. Ausführung der Tests inkl. Perfbot
98 | robot --prerebotmodifier perfbot.perfbot:devn=0.1:db_path="example/robot-exec-times.db":testbreaker=True example/tests
99 | ```
100 | Screenshot 1: Einbindung der Performance-Analyse in die Log-Datei:
101 | 
102 |
103 | Screenshot 2: Testbreaker in Aktion am Beispiel des Beispiels Login-Page
104 | 
105 |
106 | ## Box-Plot-Legende
107 |
108 |
109 |
110 | | Kennwert | Beschreibung | Lage im Box-Plot
111 | |--|--|--
112 | | Unteres Quartil | Die kleinsten 25 % der Datenwerte sind kleiner als dieser oder gleich diesem Kennwert | Beginn der Box
113 | | Median | Die kleinsten 50 % der Datenwerte sind kleiner als dieser oder gleich diesem Kennwert | Strich innerhalb der Box
114 | | Oberes Quartil | Die kleinsten 75 % der Datenwerte sind kleiner als dieser oder gleich diesem Kennwert | Ende der Box
115 | | Antenne (Whisker) | Bis 1,5-facher Interquartilabstand (Länge der Box) werden auf beiden Seiten die Antennen dargestellt. | Antennen
116 | | Ausreißer | Alle Punkte außerhalb der Antennen | Einzelne Punkte (Raute)
117 | | Einzelwerte | Alle Einzelwerte werden als Punktwolke dargestellt. | Punktwolke (rund/grau)
118 | | Aktuelle Laufzeit | Dauer des Testfalls/Keywords im der gerade betrachteten Testlauf. | oranger Punkt
119 |
120 | Quelle der Tabelle: [Wikipedia](https://de.wikipedia.org/wiki/Box-Plot) [Seaborn-Docs](https://seaborn.pydata.org/generated/seaborn.boxplot.html)
121 |
122 | ## Technische Dokumentation
123 |
124 | Für weitere Details u. a. den architektonischen Aufbau siehe [ARC42_DOC.md](ARC42_DOC.md).
125 | Einen Überblick über die statische Codeanalyse, Unit- und Integrationstests gibt der [Testplan](tests/Testplan.md).
126 |
127 | ## Quellen
128 | Für den Aufbau dieses Repositories wurde auf die Docs der entsprechenden Technologien zurückgegriffen und ggf. Codeschnipsel aus Implementierungsbeispielen übernommen. Im Folgenden eine Auflistung der entsprechenden Docs, Tutorials und Implementierungsbeispielen:
129 |
130 | - [Robot Framework User Guide](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html) Version 6.0.2, insbesondere:
131 | - Kapitel *4.3 Listener interface* und zugehöriger Unterpunkt *Modifying execution and results*
132 | - Kapitel *3.6.9 Programmatic modification of results* inkl. Listing *ExecutionTimeChecker*
133 | - [Robot Framework API documentation](https://robot-framework.readthedocs.io/en/latest/index.html#robot-framework-api-documentation)
134 | - [SeleniumLibrary](https://github.com/robotframework/SeleniumLibrary) bzw. deren Beispielprojekt zum Ausprobieren dieser Implementieren (siehe `example`)
135 | - Erweiterung von [robotmetrics](https://github.com/adiralashiva8/robotframework-metrics)
136 | - verwendete Python-Module inkl. Links zu den Docs:
137 | - [Robot Framework](http://www.robotframework.org)
138 | - [Pandas](https://pandas.pydata.org/docs/)
139 | - [Matplotlib](https://matplotlib.org/stable/index.html)
140 | - [Sqlite3](https://docs.python.org/3/library/sqlite3.html)
141 | - [Seaborn](https://seaborn.pydata.org)
142 | - Python3 (allgemein): [PythonDocs](https://docs.python.org/3/),[Python3 - Ein umfassende Handbuch](https://openbook.rheinwerk-verlag.de/python/),[W3Schools](https://www.w3schools.com/python/default.asp), [Pythonbuch](https://pythonbuch.com)
143 | - das Logo von Perfbot basiert auf dem [Robot Framework logo](https://github.com/robotframework/visual-identity) und ist damit unter [Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/) lizensiert
144 |
145 | ## Lizenz
146 | © Lennart Potthoff / MIT-Lizenz
147 |
148 | ## Schlussbemerkung
149 | Perfbot wurde im Rahmen einer Masterthesis erstellt:
150 | Titel der Masterthesis: Automatisierte Performance-Analyse von IT-Anwendungen mit dem Testautomatisierungswerkzeug Robot Framework und Evaluation bei einem Versicherungsunternehmen
151 | Student: Lennart Potthoff
152 | Studiengang: M. Sc. Wirtschaftsinformatik
153 | Semester: Sommersemester 2023
154 | Hochschule: FH Münster
155 |
--------------------------------------------------------------------------------
/example/0_live_demo/LIVE-DEMO.md:
--------------------------------------------------------------------------------
1 | # Guide zur Live-Demo auf der Robocon 2025
2 |
3 | ## Vorbereitung
4 |
5 | - Alle Sonstigen Anwendungen (außer PowerPoint und VSCode) sind geschlossen
6 | - VSCode ist geöffnet mit Ordner "Example" im perfbot-Repo
7 | - Python-Server als System-under-Test ist im seperaten Terminal gestartet: `python3 sut/server.py`
8 | - Der Testfall bzw. die Testsuite `valid_login.robot` ist geöffnet
9 | - Ein Sleep-Step ist auskommentiert
10 | - Das Terminal ist geöffnet
11 | - Der Zoom ist in ausreichender Größe eingestellt
12 | - Der Browser mit dem System-under-Test ist geöffnet: [Login-Page](http://localhost:7272)
13 |
14 | ## Schritt 1: System-under-Test und Testfälle zeigen
15 | - Test-Login-Seite zeigen
16 | - Beispiel-Testfall zeigen
17 |
18 | ## Schritt 2: Robot inkl. Perfbot starten per CLI
19 | - Testfälle starten
20 | ```bash
21 | robot --prerebotmodifier perfbot.perfbot tests/
22 | ```
23 | - log.html in VSCode per Rechtsklick "Open in Default-Browser" öffnen
24 | - Tabelle zeigen
25 | - Boxplot erläutern
26 |
27 | ## Schritt 3: Fehler injektieren und Perfbot erneut starten
28 | - Sleep in `valid_login.robot` schreiben bzw. einkommentieren
29 | - Erneut starten mit Optionen:
30 | - Datenbank angeben
31 | - Abweichung angeben
32 | - Testbreaker aktivieren
33 | - nur lesen drauf zu greifen
34 | ```bash
35 | robot --prerebotmodifier perfbot.perfbot:devn=0.5:db_path="robot-exec-times.db":testbreaker=True:readonly=True tests/
36 | ```
37 |
--------------------------------------------------------------------------------
/example/0_live_demo/normal-run/output.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | browser=${BROWSER}
8 | headless=${HEADLESS}
9 | BrowserControl
10 | Setter
11 | Create a new playwright Browser with specified options.
12 | {"browser": "chromium", "headless": false, "chromiumSandbox": false, "devtools": false, "handleSIGHUP": true, "handleSIGINT": true, "handleSIGTERM": true, "slowMo": 0.0, "timeout": 30000.0, "tracesDir": "/Users/lennart/Documents/Development/Studium/2022_Master-Thesis/robotframework-perfbot/example/browser/traces/temp/5741eade-f9bc-4f14-a7ac-739bf1dbe94e"}
13 | Starting Browser process /usr/local/lib/python3.11/site-packages/Browser/wrapper/index.js using port 55448
14 | Node startup parameters: ['node', '/usr/local/lib/python3.11/site-packages/Browser/wrapper/index.js', '55448']
15 | Successfully created browser with options: {"browser":"chromium","headless":false,"chromiumSandbox":false,"devtools":false,"handleSIGHUP":true,"handleSIGINT":true,"handleSIGTERM":true,"slowMo":0,"timeout":30000,"tracesDir":"/Users/lennart/Documents/Development/Studium/2022_Master-Thesis/robotframework-perfbot/example/browser/traces/temp/5741eade-f9bc-4f14-a7ac-739bf1dbe94e"}
16 |
17 |
18 |
19 | ${LOGIN URL}
20 | BrowserControl
21 | Setter
22 | Open a new Page.
23 | Successfully initialized new page object and opened url: http://localhost:7272/
24 | No context was open. New context was automatically opened when this page is created.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ${LOGIN URL}
33 | BrowserControl
34 | Setter
35 | Open a new Page.
36 | Successfully initialized new page object and opened url: http://localhost:7272/
37 |
38 |
39 |
40 |
41 |
42 | invalid
43 | ${VALID PASSWORD}
44 |
45 | ${username}
46 |
47 | id=username_field
48 | ${username}
49 | PageContent
50 | Setter
51 | Clears and fills the given ``txt`` into the text field found by ``selector``.
52 | Fills the text 'invalid' in the given field.
53 |
54 |
55 |
56 |
57 |
58 | ${password}
59 |
60 | id=password_field
61 | ${password}
62 | PageContent
63 | Setter
64 | Clears and fills the given ``txt`` into the text field found by ``selector``.
65 | Fills the text 'mode' in the given field.
66 |
67 |
68 |
69 |
70 |
71 |
72 | id=login_button
73 | PageContent
74 | Setter
75 | Simulates mouse click on the element found by ``selector``.
76 | Clicks the element 'id=login_button'.
77 |
78 |
79 |
80 |
81 |
82 |
83 | *=
84 | ${ERROR URL}
85 | Assertion
86 | Getter
87 | PageContent
88 | Returns the current URL.
89 |
90 |
91 |
92 | equals
93 | Error Page
94 | Assertion
95 | Getter
96 | PageContent
97 | Returns the title of the current page.
98 | Title: 'Error Page'
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | ${LOGIN URL}
111 | BrowserControl
112 | Setter
113 | Open a new Page.
114 | Successfully initialized new page object and opened url: http://localhost:7272/
115 |
116 |
117 |
118 |
119 |
120 | ${VALID USER}
121 | invalid
122 |
123 | ${username}
124 |
125 | id=username_field
126 | ${username}
127 | PageContent
128 | Setter
129 | Clears and fills the given ``txt`` into the text field found by ``selector``.
130 | Fills the text 'demo' in the given field.
131 |
132 |
133 |
134 |
135 |
136 | ${password}
137 |
138 | id=password_field
139 | ${password}
140 | PageContent
141 | Setter
142 | Clears and fills the given ``txt`` into the text field found by ``selector``.
143 | Fills the text 'invalid' in the given field.
144 |
145 |
146 |
147 |
148 |
149 |
150 | id=login_button
151 | PageContent
152 | Setter
153 | Simulates mouse click on the element found by ``selector``.
154 | Clicks the element 'id=login_button'.
155 |
156 |
157 |
158 |
159 |
160 |
161 | *=
162 | ${ERROR URL}
163 | Assertion
164 | Getter
165 | PageContent
166 | Returns the current URL.
167 |
168 |
169 |
170 | equals
171 | Error Page
172 | Assertion
173 | Getter
174 | PageContent
175 | Returns the title of the current page.
176 | Title: 'Error Page'
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | ${LOGIN URL}
189 | BrowserControl
190 | Setter
191 | Open a new Page.
192 | Successfully initialized new page object and opened url: http://localhost:7272/
193 |
194 |
195 |
196 |
197 |
198 | invalid
199 | whatever
200 |
201 | ${username}
202 |
203 | id=username_field
204 | ${username}
205 | PageContent
206 | Setter
207 | Clears and fills the given ``txt`` into the text field found by ``selector``.
208 | Fills the text 'invalid' in the given field.
209 |
210 |
211 |
212 |
213 |
214 | ${password}
215 |
216 | id=password_field
217 | ${password}
218 | PageContent
219 | Setter
220 | Clears and fills the given ``txt`` into the text field found by ``selector``.
221 | Fills the text 'whatever' in the given field.
222 |
223 |
224 |
225 |
226 |
227 |
228 | id=login_button
229 | PageContent
230 | Setter
231 | Simulates mouse click on the element found by ``selector``.
232 | Clicks the element 'id=login_button'.
233 |
234 |
235 |
236 |
237 |
238 |
239 | *=
240 | ${ERROR URL}
241 | Assertion
242 | Getter
243 | PageContent
244 | Returns the current URL.
245 |
246 |
247 |
248 | equals
249 | Error Page
250 | Assertion
251 | Getter
252 | PageContent
253 | Returns the title of the current page.
254 | Title: 'Error Page'
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | ${LOGIN URL}
267 | BrowserControl
268 | Setter
269 | Open a new Page.
270 | Successfully initialized new page object and opened url: http://localhost:7272/
271 |
272 |
273 |
274 |
275 |
276 | ${EMPTY}
277 | ${VALID PASSWORD}
278 |
279 | ${username}
280 |
281 | id=username_field
282 | ${username}
283 | PageContent
284 | Setter
285 | Clears and fills the given ``txt`` into the text field found by ``selector``.
286 | Fills the text '' in the given field.
287 |
288 |
289 |
290 |
291 |
292 | ${password}
293 |
294 | id=password_field
295 | ${password}
296 | PageContent
297 | Setter
298 | Clears and fills the given ``txt`` into the text field found by ``selector``.
299 | Fills the text 'mode' in the given field.
300 |
301 |
302 |
303 |
304 |
305 |
306 | id=login_button
307 | PageContent
308 | Setter
309 | Simulates mouse click on the element found by ``selector``.
310 | Clicks the element 'id=login_button'.
311 |
312 |
313 |
314 |
315 |
316 |
317 | *=
318 | ${ERROR URL}
319 | Assertion
320 | Getter
321 | PageContent
322 | Returns the current URL.
323 |
324 |
325 |
326 | equals
327 | Error Page
328 | Assertion
329 | Getter
330 | PageContent
331 | Returns the title of the current page.
332 | Title: 'Error Page'
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 | ${LOGIN URL}
345 | BrowserControl
346 | Setter
347 | Open a new Page.
348 | Successfully initialized new page object and opened url: http://localhost:7272/
349 |
350 |
351 |
352 |
353 |
354 | ${VALID USER}
355 | ${EMPTY}
356 |
357 | ${username}
358 |
359 | id=username_field
360 | ${username}
361 | PageContent
362 | Setter
363 | Clears and fills the given ``txt`` into the text field found by ``selector``.
364 | Fills the text 'demo' in the given field.
365 |
366 |
367 |
368 |
369 |
370 | ${password}
371 |
372 | id=password_field
373 | ${password}
374 | PageContent
375 | Setter
376 | Clears and fills the given ``txt`` into the text field found by ``selector``.
377 | Fills the text '' in the given field.
378 |
379 |
380 |
381 |
382 |
383 |
384 | id=login_button
385 | PageContent
386 | Setter
387 | Simulates mouse click on the element found by ``selector``.
388 | Clicks the element 'id=login_button'.
389 |
390 |
391 |
392 |
393 |
394 |
395 | *=
396 | ${ERROR URL}
397 | Assertion
398 | Getter
399 | PageContent
400 | Returns the current URL.
401 |
402 |
403 |
404 | equals
405 | Error Page
406 | Assertion
407 | Getter
408 | PageContent
409 | Returns the title of the current page.
410 | Title: 'Error Page'
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 | ${LOGIN URL}
423 | BrowserControl
424 | Setter
425 | Open a new Page.
426 | Successfully initialized new page object and opened url: http://localhost:7272/
427 |
428 |
429 |
430 |
431 |
432 | ${EMPTY}
433 | ${EMPTY}
434 |
435 | ${username}
436 |
437 | id=username_field
438 | ${username}
439 | PageContent
440 | Setter
441 | Clears and fills the given ``txt`` into the text field found by ``selector``.
442 | Fills the text '' in the given field.
443 |
444 |
445 |
446 |
447 |
448 | ${password}
449 |
450 | id=password_field
451 | ${password}
452 | PageContent
453 | Setter
454 | Clears and fills the given ``txt`` into the text field found by ``selector``.
455 | Fills the text '' in the given field.
456 |
457 |
458 |
459 |
460 |
461 |
462 | id=login_button
463 | PageContent
464 | Setter
465 | Simulates mouse click on the element found by ``selector``.
466 | Clicks the element 'id=login_button'.
467 |
468 |
469 |
470 |
471 |
472 |
473 | *=
474 | ${ERROR URL}
475 | Assertion
476 | Getter
477 | PageContent
478 | Returns the current URL.
479 |
480 |
481 |
482 | equals
483 | Error Page
484 | Assertion
485 | Getter
486 | PageContent
487 | Returns the title of the current page.
488 | Title: 'Error Page'
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 | BrowserControl
499 | Setter
500 | Closes the current browser.
501 | Closed browser
502 |
503 |
504 | A test suite containing tests related to invalid login.
505 |
506 | These tests are data-driven by their nature. They use a single
507 | keyword, specified with Test Template setting, that is called
508 | with different arguments to cover different scenarios.
509 |
510 | This suite also demonstrates using setups and teardowns in
511 | different levels.
512 |
513 |
514 |
515 |
516 |
517 |
518 | browser=${BROWSER}
519 | headless=${HEADLESS}
520 | BrowserControl
521 | Setter
522 | Create a new playwright Browser with specified options.
523 | {"browser": "chromium", "headless": false, "chromiumSandbox": false, "devtools": false, "handleSIGHUP": true, "handleSIGINT": true, "handleSIGTERM": true, "slowMo": 0.0, "timeout": 30000.0, "tracesDir": "/Users/lennart/Documents/Development/Studium/2022_Master-Thesis/robotframework-perfbot/example/browser/traces/temp/44fc1cf5-cb13-4d2a-9216-ee3908dfe3f0"}
524 | Successfully created browser with options: {"browser":"chromium","headless":false,"chromiumSandbox":false,"devtools":false,"handleSIGHUP":true,"handleSIGINT":true,"handleSIGTERM":true,"slowMo":0,"timeout":30000,"tracesDir":"/Users/lennart/Documents/Development/Studium/2022_Master-Thesis/robotframework-perfbot/example/browser/traces/temp/44fc1cf5-cb13-4d2a-9216-ee3908dfe3f0"}
525 |
526 |
527 |
528 | ${LOGIN URL}
529 | BrowserControl
530 | Setter
531 | Open a new Page.
532 | Successfully initialized new page object and opened url: http://localhost:7272/
533 | No context was open. New context was automatically opened when this page is created.
534 |
535 |
536 |
537 |
538 |
539 |
540 | equals
541 | Login Page
542 | Assertion
543 | Getter
544 | PageContent
545 | Returns the title of the current page.
546 | Title: 'Login Page'
547 |
548 |
549 |
550 |
551 |
552 | demo
553 |
554 | id=username_field
555 | ${username}
556 | PageContent
557 | Setter
558 | Clears and fills the given ``txt`` into the text field found by ``selector``.
559 | Fills the text 'demo' in the given field.
560 |
561 |
562 |
563 |
564 |
565 | mode
566 |
567 | id=password_field
568 | ${password}
569 | PageContent
570 | Setter
571 | Clears and fills the given ``txt`` into the text field found by ``selector``.
572 | Fills the text 'mode' in the given field.
573 |
574 |
575 |
576 |
577 |
578 |
579 | id=login_button
580 | PageContent
581 | Setter
582 | Simulates mouse click on the element found by ``selector``.
583 | Clicks the element 'id=login_button'.
584 |
585 |
586 |
587 |
588 |
589 |
590 | *=
591 | ${WELCOME URL}
592 | Assertion
593 | Getter
594 | PageContent
595 | Returns the current URL.
596 |
597 |
598 |
599 | equals
600 | Welcome Page
601 | Assertion
602 | Getter
603 | PageContent
604 | Returns the title of the current page.
605 | Title: 'Welcome Page'
606 |
607 |
608 |
609 |
610 |
611 | BrowserControl
612 | Setter
613 | Closes the current browser.
614 | Closed browser
615 |
616 |
617 |
618 |
619 | A test suite with a single test for valid login.
620 |
621 | This test has a workflow that is created using keywords in
622 | the imported resource file.
623 |
624 |
625 |
626 |
627 |
628 |
629 | All Tests
630 |
631 |
632 |
633 |
634 | Tests
635 | Tests.Invalid Login
636 | Tests.Valid Login
637 |
638 |
639 |
640 |
641 |
642 |
--------------------------------------------------------------------------------
/example/0_live_demo/normal-run/perfbot-graphics/boxplot-01-24-2025-20-54-22-909875.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/0_live_demo/normal-run/perfbot-graphics/boxplot-01-24-2025-20-54-22-909875.png
--------------------------------------------------------------------------------
/example/0_live_demo/normal-run/perfbot-graphics/boxplot-01-24-2025-20-54-23-352049.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/0_live_demo/normal-run/perfbot-graphics/boxplot-01-24-2025-20-54-23-352049.png
--------------------------------------------------------------------------------
/example/0_live_demo/slow_run/perfbot-graphics/boxplot-01-24-2025-20-57-06-861135.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/0_live_demo/slow_run/perfbot-graphics/boxplot-01-24-2025-20-57-06-861135.png
--------------------------------------------------------------------------------
/example/0_live_demo/slow_run/perfbot-graphics/boxplot-01-24-2025-20-57-07-250893.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/0_live_demo/slow_run/perfbot-graphics/boxplot-01-24-2025-20-57-07-250893.png
--------------------------------------------------------------------------------
/example/output.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | browser=${BROWSER}
8 | headless=${HEADLESS}
9 | BrowserControl
10 | Setter
11 | Create a new playwright Browser with specified options.
12 | {"browser": "chromium", "headless": false, "chromiumSandbox": false, "devtools": false, "handleSIGHUP": true, "handleSIGINT": true, "handleSIGTERM": true, "slowMo": 0.0, "timeout": 30000.0, "tracesDir": "/Users/lennart/Documents/Development/Studium/Robocon/robotframework-perfbot/example/browser/traces/temp/ea027267-4566-45f3-9492-860126acd828"}
13 | Starting Browser process /usr/local/lib/python3.11/site-packages/Browser/wrapper/index.js using port 50005
14 | Node startup parameters: ['node', '/usr/local/lib/python3.11/site-packages/Browser/wrapper/index.js', '50005']
15 | Successfully created browser with options: {"browser":"chromium","headless":false,"chromiumSandbox":false,"devtools":false,"handleSIGHUP":true,"handleSIGINT":true,"handleSIGTERM":true,"slowMo":0,"timeout":30000,"tracesDir":"/Users/lennart/Documents/Development/Studium/Robocon/robotframework-perfbot/example/browser/traces/temp/ea027267-4566-45f3-9492-860126acd828"}
16 |
17 |
18 |
19 | ${LOGIN URL}
20 | BrowserControl
21 | Setter
22 | Open a new Page.
23 | Successfully initialized new page object and opened url: http://localhost:7272/
24 | No context was open. New context was automatically opened when this page is created.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ${LOGIN URL}
33 | BrowserControl
34 | Setter
35 | Open a new Page.
36 | Successfully initialized new page object and opened url: http://localhost:7272/
37 |
38 |
39 |
40 |
41 |
42 | invalid
43 | ${VALID PASSWORD}
44 |
45 | ${username}
46 |
47 | id=username_field
48 | ${username}
49 | PageContent
50 | Setter
51 | Clears and fills the given ``txt`` into the text field found by ``selector``.
52 | Fills the text 'invalid' in the given field.
53 |
54 |
55 |
56 |
57 |
58 | ${password}
59 |
60 | id=password_field
61 | ${password}
62 | PageContent
63 | Setter
64 | Clears and fills the given ``txt`` into the text field found by ``selector``.
65 | Fills the text 'mode' in the given field.
66 |
67 |
68 |
69 |
70 |
71 |
72 | id=login_button
73 | PageContent
74 | Setter
75 | Simulates mouse click on the element found by ``selector``.
76 | Clicks the element 'id=login_button'.
77 |
78 |
79 |
80 |
81 |
82 |
83 | *=
84 | ${ERROR URL}
85 | Assertion
86 | Getter
87 | PageContent
88 | Returns the current URL.
89 |
90 |
91 |
92 | equals
93 | Error Page
94 | Assertion
95 | Getter
96 | PageContent
97 | Returns the title of the current page.
98 | Title: 'Error Page'
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | ${LOGIN URL}
111 | BrowserControl
112 | Setter
113 | Open a new Page.
114 | Successfully initialized new page object and opened url: http://localhost:7272/
115 |
116 |
117 |
118 |
119 |
120 | ${VALID USER}
121 | invalid
122 |
123 | ${username}
124 |
125 | id=username_field
126 | ${username}
127 | PageContent
128 | Setter
129 | Clears and fills the given ``txt`` into the text field found by ``selector``.
130 | Fills the text 'demo' in the given field.
131 |
132 |
133 |
134 |
135 |
136 | ${password}
137 |
138 | id=password_field
139 | ${password}
140 | PageContent
141 | Setter
142 | Clears and fills the given ``txt`` into the text field found by ``selector``.
143 | Fills the text 'invalid' in the given field.
144 |
145 |
146 |
147 |
148 |
149 |
150 | id=login_button
151 | PageContent
152 | Setter
153 | Simulates mouse click on the element found by ``selector``.
154 | Clicks the element 'id=login_button'.
155 |
156 |
157 |
158 |
159 |
160 |
161 | *=
162 | ${ERROR URL}
163 | Assertion
164 | Getter
165 | PageContent
166 | Returns the current URL.
167 |
168 |
169 |
170 | equals
171 | Error Page
172 | Assertion
173 | Getter
174 | PageContent
175 | Returns the title of the current page.
176 | Title: 'Error Page'
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | ${LOGIN URL}
189 | BrowserControl
190 | Setter
191 | Open a new Page.
192 | Successfully initialized new page object and opened url: http://localhost:7272/
193 |
194 |
195 |
196 |
197 |
198 | invalid
199 | whatever
200 |
201 | ${username}
202 |
203 | id=username_field
204 | ${username}
205 | PageContent
206 | Setter
207 | Clears and fills the given ``txt`` into the text field found by ``selector``.
208 | Fills the text 'invalid' in the given field.
209 |
210 |
211 |
212 |
213 |
214 | ${password}
215 |
216 | id=password_field
217 | ${password}
218 | PageContent
219 | Setter
220 | Clears and fills the given ``txt`` into the text field found by ``selector``.
221 | Fills the text 'whatever' in the given field.
222 |
223 |
224 |
225 |
226 |
227 |
228 | id=login_button
229 | PageContent
230 | Setter
231 | Simulates mouse click on the element found by ``selector``.
232 | Clicks the element 'id=login_button'.
233 |
234 |
235 |
236 |
237 |
238 |
239 | *=
240 | ${ERROR URL}
241 | Assertion
242 | Getter
243 | PageContent
244 | Returns the current URL.
245 |
246 |
247 |
248 | equals
249 | Error Page
250 | Assertion
251 | Getter
252 | PageContent
253 | Returns the title of the current page.
254 | Title: 'Error Page'
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | ${LOGIN URL}
267 | BrowserControl
268 | Setter
269 | Open a new Page.
270 | Successfully initialized new page object and opened url: http://localhost:7272/
271 |
272 |
273 |
274 |
275 |
276 | ${EMPTY}
277 | ${VALID PASSWORD}
278 |
279 | ${username}
280 |
281 | id=username_field
282 | ${username}
283 | PageContent
284 | Setter
285 | Clears and fills the given ``txt`` into the text field found by ``selector``.
286 | Fills the text '' in the given field.
287 |
288 |
289 |
290 |
291 |
292 | ${password}
293 |
294 | id=password_field
295 | ${password}
296 | PageContent
297 | Setter
298 | Clears and fills the given ``txt`` into the text field found by ``selector``.
299 | Fills the text 'mode' in the given field.
300 |
301 |
302 |
303 |
304 |
305 |
306 | id=login_button
307 | PageContent
308 | Setter
309 | Simulates mouse click on the element found by ``selector``.
310 | Clicks the element 'id=login_button'.
311 |
312 |
313 |
314 |
315 |
316 |
317 | *=
318 | ${ERROR URL}
319 | Assertion
320 | Getter
321 | PageContent
322 | Returns the current URL.
323 |
324 |
325 |
326 | equals
327 | Error Page
328 | Assertion
329 | Getter
330 | PageContent
331 | Returns the title of the current page.
332 | Title: 'Error Page'
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 | ${LOGIN URL}
345 | BrowserControl
346 | Setter
347 | Open a new Page.
348 | Successfully initialized new page object and opened url: http://localhost:7272/
349 |
350 |
351 |
352 |
353 |
354 | ${VALID USER}
355 | ${EMPTY}
356 |
357 | ${username}
358 |
359 | id=username_field
360 | ${username}
361 | PageContent
362 | Setter
363 | Clears and fills the given ``txt`` into the text field found by ``selector``.
364 | Fills the text 'demo' in the given field.
365 |
366 |
367 |
368 |
369 |
370 | ${password}
371 |
372 | id=password_field
373 | ${password}
374 | PageContent
375 | Setter
376 | Clears and fills the given ``txt`` into the text field found by ``selector``.
377 | Fills the text '' in the given field.
378 |
379 |
380 |
381 |
382 |
383 |
384 | id=login_button
385 | PageContent
386 | Setter
387 | Simulates mouse click on the element found by ``selector``.
388 | Clicks the element 'id=login_button'.
389 |
390 |
391 |
392 |
393 |
394 |
395 | *=
396 | ${ERROR URL}
397 | Assertion
398 | Getter
399 | PageContent
400 | Returns the current URL.
401 |
402 |
403 |
404 | equals
405 | Error Page
406 | Assertion
407 | Getter
408 | PageContent
409 | Returns the title of the current page.
410 | Title: 'Error Page'
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 | ${LOGIN URL}
423 | BrowserControl
424 | Setter
425 | Open a new Page.
426 | Successfully initialized new page object and opened url: http://localhost:7272/
427 |
428 |
429 |
430 |
431 |
432 | ${EMPTY}
433 | ${EMPTY}
434 |
435 | ${username}
436 |
437 | id=username_field
438 | ${username}
439 | PageContent
440 | Setter
441 | Clears and fills the given ``txt`` into the text field found by ``selector``.
442 | Fills the text '' in the given field.
443 |
444 |
445 |
446 |
447 |
448 | ${password}
449 |
450 | id=password_field
451 | ${password}
452 | PageContent
453 | Setter
454 | Clears and fills the given ``txt`` into the text field found by ``selector``.
455 | Fills the text '' in the given field.
456 |
457 |
458 |
459 |
460 |
461 |
462 | id=login_button
463 | PageContent
464 | Setter
465 | Simulates mouse click on the element found by ``selector``.
466 | Clicks the element 'id=login_button'.
467 |
468 |
469 |
470 |
471 |
472 |
473 | *=
474 | ${ERROR URL}
475 | Assertion
476 | Getter
477 | PageContent
478 | Returns the current URL.
479 |
480 |
481 |
482 | equals
483 | Error Page
484 | Assertion
485 | Getter
486 | PageContent
487 | Returns the title of the current page.
488 | Title: 'Error Page'
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 | BrowserControl
499 | Setter
500 | Closes the current browser.
501 | Closed browser
502 |
503 |
504 | A test suite containing tests related to invalid login.
505 |
506 | These tests are data-driven by their nature. They use a single
507 | keyword, specified with Test Template setting, that is called
508 | with different arguments to cover different scenarios.
509 |
510 | This suite also demonstrates using setups and teardowns in
511 | different levels.
512 |
513 |
514 |
515 |
516 |
517 |
518 | browser=${BROWSER}
519 | headless=${HEADLESS}
520 | BrowserControl
521 | Setter
522 | Create a new playwright Browser with specified options.
523 | {"browser": "chromium", "headless": false, "chromiumSandbox": false, "devtools": false, "handleSIGHUP": true, "handleSIGINT": true, "handleSIGTERM": true, "slowMo": 0.0, "timeout": 30000.0, "tracesDir": "/Users/lennart/Documents/Development/Studium/Robocon/robotframework-perfbot/example/browser/traces/temp/8925eea4-f23d-4120-9caf-bed9acd5c383"}
524 | Successfully created browser with options: {"browser":"chromium","headless":false,"chromiumSandbox":false,"devtools":false,"handleSIGHUP":true,"handleSIGINT":true,"handleSIGTERM":true,"slowMo":0,"timeout":30000,"tracesDir":"/Users/lennart/Documents/Development/Studium/Robocon/robotframework-perfbot/example/browser/traces/temp/8925eea4-f23d-4120-9caf-bed9acd5c383"}
525 |
526 |
527 |
528 | ${LOGIN URL}
529 | BrowserControl
530 | Setter
531 | Open a new Page.
532 | Successfully initialized new page object and opened url: http://localhost:7272/
533 | No context was open. New context was automatically opened when this page is created.
534 |
535 |
536 |
537 |
538 |
539 |
540 | equals
541 | Login Page
542 | Assertion
543 | Getter
544 | PageContent
545 | Returns the title of the current page.
546 | Title: 'Login Page'
547 |
548 |
549 |
550 |
551 |
552 | demo
553 |
554 | id=username_field
555 | ${username}
556 | PageContent
557 | Setter
558 | Clears and fills the given ``txt`` into the text field found by ``selector``.
559 | Fills the text 'demo' in the given field.
560 |
561 |
562 |
563 |
564 |
565 | mode
566 |
567 | id=password_field
568 | ${password}
569 | PageContent
570 | Setter
571 | Clears and fills the given ``txt`` into the text field found by ``selector``.
572 | Fills the text 'mode' in the given field.
573 |
574 |
575 |
576 |
577 |
578 |
579 | id=login_button
580 | PageContent
581 | Setter
582 | Simulates mouse click on the element found by ``selector``.
583 | Clicks the element 'id=login_button'.
584 |
585 |
586 |
587 |
588 |
589 |
590 | *=
591 | ${WELCOME URL}
592 | Assertion
593 | Getter
594 | PageContent
595 | Returns the current URL.
596 |
597 |
598 |
599 | equals
600 | Welcome Page
601 | Assertion
602 | Getter
603 | PageContent
604 | Returns the title of the current page.
605 | Title: 'Welcome Page'
606 |
607 |
608 |
609 |
610 |
611 | BrowserControl
612 | Setter
613 | Closes the current browser.
614 | Closed browser
615 |
616 |
617 |
618 |
619 | A test suite with a single test for valid login.
620 |
621 | This test has a workflow that is created using keywords in
622 | the imported resource file.
623 |
624 |
625 |
626 |
627 |
628 |
629 | All Tests
630 |
631 |
632 |
633 |
634 | Tests
635 | Tests.Invalid Login
636 | Tests.Valid Login
637 |
638 |
639 |
640 |
641 |
642 |
--------------------------------------------------------------------------------
/example/perfbot-graphics/boxplot-01-04-2025-13-35-36-026603.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/perfbot-graphics/boxplot-01-04-2025-13-35-36-026603.png
--------------------------------------------------------------------------------
/example/perfbot-graphics/boxplot-01-04-2025-13-35-36-462290.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/perfbot-graphics/boxplot-01-04-2025-13-35-36-462290.png
--------------------------------------------------------------------------------
/example/perfbot-graphics/boxplot-05-19-2023-18-01-09-186797.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/perfbot-graphics/boxplot-05-19-2023-18-01-09-186797.png
--------------------------------------------------------------------------------
/example/perfbot-graphics/boxplot-05-19-2023-18-01-09-531645.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/perfbot-graphics/boxplot-05-19-2023-18-01-09-531645.png
--------------------------------------------------------------------------------
/example/robot-exec-times.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/example/robot-exec-times.db
--------------------------------------------------------------------------------
/example/sut/html/demo.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | color: black;
4 | background: #DDDDDD;
5 | }
6 | #container {
7 | width: 30em;
8 | height: 15em;
9 | margin: 5em auto;
10 | background: white;
11 | border: 1px solid gray;
12 | padding: 0.5em 2em;
13 | }
14 |
--------------------------------------------------------------------------------
/example/sut/html/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Error Page
5 |
6 |
7 |
8 |
9 |
Error Page
10 |
Login failed. Invalid user name and/or password.
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/sut/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Login Page
5 |
6 |
15 |
16 |
17 |
18 |
Login Page
19 |
Please input your user name and password and click the login button.
20 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/example/sut/html/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Welcome Page
5 |
6 |
7 |
8 |
9 |
Welcome Page
10 |
Login succeeded. Now you can logout.
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/sut/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """Simple HTTP server for Robot Framework web testing demo.
4 |
5 | Usage: server.py [port]
6 |
7 | This server serves HTML pages under `html` directory. Server is started simply
8 | by running this script from the command line or double-clicking it in a file
9 | manager. In the former case the server can be shut down with Ctrl-C and in the
10 | latter case by closing the opened window.
11 |
12 | By default the server uses port 7272, but a custom port can be given as
13 | an argument from the command line.
14 | """
15 |
16 | from __future__ import print_function
17 |
18 | from os import chdir
19 | from os.path import abspath, dirname, join
20 | try:
21 | from SocketServer import ThreadingMixIn
22 | from BaseHTTPServer import HTTPServer
23 | from SimpleHTTPServer import SimpleHTTPRequestHandler
24 | except ImportError:
25 | from socketserver import ThreadingMixIn
26 | from http.server import SimpleHTTPRequestHandler, HTTPServer
27 |
28 |
29 | ROOT = join(dirname(abspath(__file__)), 'html')
30 | PORT = 7272
31 |
32 |
33 | class DemoServer(ThreadingMixIn, HTTPServer):
34 | allow_reuse_address = True
35 |
36 | def __init__(self, port=PORT):
37 | HTTPServer.__init__(self, ('localhost', int(port)),
38 | SimpleHTTPRequestHandler)
39 |
40 | def serve(self, directory=ROOT):
41 | chdir(directory)
42 | print('Demo server starting on port %d.' % self.server_address[1])
43 | try:
44 | server.serve_forever()
45 | except KeyboardInterrupt:
46 | server.server_close()
47 | print('Demo server stopped.')
48 |
49 |
50 | if __name__ == '__main__':
51 | import sys
52 | try:
53 | server = DemoServer(*sys.argv[1:])
54 | except (TypeError, ValueError):
55 | print(__doc__)
56 | else:
57 | server.serve()
58 |
--------------------------------------------------------------------------------
/example/tests/invalid_login.robot:
--------------------------------------------------------------------------------
1 | *** Settings ***
2 | Documentation A test suite containing tests related to invalid login.
3 | ...
4 | ... These tests are data-driven by their nature. They use a single
5 | ... keyword, specified with Test Template setting, that is called
6 | ... with different arguments to cover different scenarios.
7 | ...
8 | ... This suite also demonstrates using setups and teardowns in
9 | ... different levels.
10 | Suite Setup Open Browser To Login Page
11 | Suite Teardown Close Browser
12 | Test Setup Go To Login Page
13 | Test Template Login With Invalid Credentials Should Fail
14 | Resource resource.robot
15 |
16 | *** Test Cases *** USER NAME PASSWORD
17 | Invalid Username invalid ${VALID PASSWORD}
18 | Invalid Password ${VALID USER} invalid
19 | Invalid Username And Password invalid whatever
20 | Empty Username ${EMPTY} ${VALID PASSWORD}
21 | Empty Password ${VALID USER} ${EMPTY}
22 | Empty Username And Password ${EMPTY} ${EMPTY}
23 |
24 | *** Keywords ***
25 | Login With Invalid Credentials Should Fail
26 | [Arguments] ${username} ${password}
27 | Input Username ${username}
28 | Input Password ${password}
29 | Submit Credentials
30 | Login Should Have Failed
31 |
32 | Login Should Have Failed
33 | Get Url *= ${ERROR URL}
34 | Get Title equals Error Page
35 |
--------------------------------------------------------------------------------
/example/tests/resource.robot:
--------------------------------------------------------------------------------
1 | *** Settings ***
2 | Library Browser
3 |
4 | *** Variables ***
5 | ${SERVER} localhost:7272
6 | ${BROWSER} chromium
7 | ${HEADLESS} false
8 | ${DELAY} 0
9 | ${VALID USER} demo
10 | ${VALID PASSWORD} mode
11 | ${LOGIN URL} http://${SERVER}/
12 | ${WELCOME URL} http://${SERVER}/welcome.html
13 | ${ERROR URL} http://${SERVER}/error.html
14 |
15 | *** Keywords ***
16 | Open Browser To Login Page
17 | New Browser browser=${BROWSER} headless=${HEADLESS}
18 | New Page ${LOGIN URL}
19 |
20 | Login Page Should Be Open
21 | Get Title equals Login Page
22 |
23 | Go To Login Page
24 | New Page ${LOGIN URL}
25 |
26 | Input Username
27 | [Arguments] ${username}
28 | Fill Text id=username_field ${username}
29 |
30 | Input Password
31 | [Arguments] ${password}
32 | Fill Text id=password_field ${password}
33 |
34 | Submit Credentials
35 | Click id=login_button
36 |
37 | Welcome Page Should Be Open
38 | Get Url *= ${WELCOME URL}
39 | Get Title equals Welcome Page
40 |
41 |
--------------------------------------------------------------------------------
/example/tests/valid_login.robot:
--------------------------------------------------------------------------------
1 | *** Settings ***
2 | Documentation A test suite with a single test for valid login.
3 | ...
4 | ... This test has a workflow that is created using keywords in
5 | ... the imported resource file.
6 | Resource resource.robot
7 | Library Screenshot
8 |
9 | *** Test Cases ***
10 | Valid Login
11 | Open Browser To Login Page
12 | Login Page Should Be Open
13 | Input Username demo
14 | Input Password mode
15 | Submit Credentials
16 | Welcome Page Should Be Open
17 | [Teardown] Close Browser
--------------------------------------------------------------------------------
/perfbot/PerfEvalResultModifier.py:
--------------------------------------------------------------------------------
1 | # see https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#toc-entry-532
2 | from robot.api import ResultVisitor
3 | from robot.api.logger import info, debug, trace, console
4 | import os
5 | from datetime import datetime
6 | from robot.result.model import TestSuite, Body, Keyword
7 | from .PersistenceService import PersistenceService
8 | from .Sqlite3PersistenceService import Sqlite3PersistenceService
9 | from .PerfEvalVisualizer import PerfEvalVisualizer
10 | from .model import JoinedPerfTestResult, Keywordrun
11 | from typing import List
12 | import socket
13 |
14 | # Constants
15 | DEFAULT_MAX_DEVIATION_FROM_LAST_RUNS = 1.0
16 | DEFAULT_LAST_N_RUNS = None
17 | DEFAULT_DATABASE_TECHNOLOGY = "sqlite3"
18 | DEFAULT_DATABASE_PATH = "robot-exec-times.db"
19 | DEFAULT_BOXPLOT_FOLDER_REL_PATH = "perfbot-graphics/"
20 | DEFAULT_STAT_FUNCTION = "avg"
21 | TEXT_PERF_ANALYSIS_TABLE_HEADING = "*Summary of Tests Performance*\n\n| =Testcase= | =Elapsed= | =Avg= | =Min= | =Max= | =Evaluated test runs= | =Deviation from avg= |\n"
22 | TEXT_PERF_ANALYSIS_TABLE_ROW = "| {name} | {elapsedtime} | {avg} | {min} | {max} | {count} | {devn} % |\n"
23 | TEXT_PERF_ANALYSIS_BOXPLOT = ""
24 | TEXT_PERF_ANALYSIS_FOOTNOTE = ""
25 | TEXT_PERF_ERROR_MESSAGE = "PerfError: Test run lasted {calced_devn:.2f} % than the average runs in the past and is thus above the maximum threshold of {max_devn:.2f} % (original test status was {old_test_status})."
26 |
27 |
28 | class PerfEvalResultModifier(ResultVisitor):
29 | """Diese Klasse übernimmt die eigentliche Verarbeitungslogik nach dem Aufruf durch rebot oder von robot mit der Option prerebotmodifier.
30 |
31 | :class ResultVisitor: Basisklasse aus der robot.api von der diese Klasse erbt, welche das Iterieren über die Testergebnisse ermöglicht.
32 | :raises NotImplementedError: Einige Parameter sind nur mit default-Werten zulässig.
33 | """
34 | ROBOT_LISTENER_API_VERSION = 2
35 |
36 |
37 | perf_results_list_of_testsuite: List[JoinedPerfTestResult] = []
38 |
39 | body_items_of_testsuite = []
40 |
41 | #TODO: Globales und Suite-Timeout aus Testfällen berücksichtigen
42 | def __init__(self, stat_func: str=DEFAULT_STAT_FUNCTION,
43 | devn: float=DEFAULT_MAX_DEVIATION_FROM_LAST_RUNS, last_n_runs: int=DEFAULT_LAST_N_RUNS, db: str=DEFAULT_DATABASE_TECHNOLOGY,
44 | db_path: str=DEFAULT_DATABASE_PATH, boxplot: bool=True, boxplot_folder: str=DEFAULT_BOXPLOT_FOLDER_REL_PATH, testbreaker:bool=False, readonly=False, keywordstats:bool=True):
45 | """Es sind keine Parameter für den Aufruf nötig. Es lässt sich aber eine Vielzahl von Einstellung über folgende Parameter vornehmen:
46 |
47 | :param stat_func: Angabe, welche statistische Funktion zur Auswertung genutzt wird, defaults to DEFAULT_STAT_FUNCTION
48 | :type stat_func: str, optional
49 | :param devn: Angabe, ab welcher prozentualen Abweichung der Testbreaker auslösen soll, defaults to DEFAULT_MAX_DEVIATION_FROM_LAST_RUNS
50 | :type devn: float, optional
51 | :param last_n_runs: Angabe, wie viele letzten Testergebnisse analysierte werden, defaults to DEFAULT_LAST_N_RUNS
52 | :type last_n_runs: int, optional
53 | :param db: Angabe, welches Persistenz-Variante bzw. Datenbank genutzt wird, defaults to DEFAULT_DATABASE_TECHNOLOGY
54 | :type db: str, optional
55 | :param db_path: Angabe, wo die Datenbank gespeichert ist, defaults to DEFAULT_DATABASE_PATH
56 | :type db_path: str, optional
57 | :param boxplot: Angabe, ob die Historie der Testlaufzeiten in einem Boxplot grafisch aufbereitet werden soll, defaults to True
58 | :type boxplot: bool, optional
59 | :param boxplot_folder: Ordner, wo die referenzierten Bilder abliegen, defaults to DEFAULT_BOXPLOT_FOLDER_REL_PATH
60 | :type boxplot_folder: str, optional
61 | :param testbreaker: Angabe, ob Testfälle bei schlechter Performanz (abhängig von devn) auf FAIL gesetzt werden sollen, defaults to False
62 | :type testbreaker: bool, optional
63 | :param readonly: Angabe, ob nur lesend auf die persitierten Daten zugegriffen werden soll, defaults to False
64 | :type readonly: bool, optional
65 | :param keywordstats: Angabe, ob die Schlüsselwort-Ebene persistiert werden soll, defaults to True
66 | :type keywordstats: bool, optional
67 | :raises NotImplementedError: Einige Parameter (stat_func, last_n_runs, db) sind nur mit default-Werten zulässig und somit nicht veränderbar.
68 | """
69 | self.stat_func = stat_func
70 | if not self.stat_func == DEFAULT_STAT_FUNCTION:
71 | raise NotImplementedError("Only Avg as statistical function supported yet.")
72 |
73 | self.max_deviation= devn
74 |
75 | self.last_n_runs = last_n_runs
76 | if not self.last_n_runs == DEFAULT_LAST_N_RUNS:
77 | raise NotImplementedError("No limit supported yet.")
78 |
79 | self.db_technology = db
80 | if not self.db_technology == DEFAULT_DATABASE_TECHNOLOGY:
81 | raise NotImplementedError("Only Sqlite3 as database technology supported yet.")
82 | self.db_path = db_path
83 | self.persistenceService: PersistenceService = Sqlite3PersistenceService(db_path)
84 |
85 | self.boxplot_activated = boxplot
86 | if self.boxplot_activated:
87 | self.visualizer = PerfEvalVisualizer(boxplot_folder)
88 | else:
89 | self.visualizer = None
90 |
91 | self.testbreaker_activated = testbreaker
92 | self.readonly = readonly
93 | self.keywordstats = keywordstats
94 |
95 | if not self.readonly:
96 | try:
97 | self.persistenceService.insert_test_execution(socket.gethostname())
98 | except:
99 | self.persistenceService.insert_test_execution("NO HOSTNAME")
100 |
101 |
102 |
103 | def start_suite(self, suite: TestSuite):
104 | """Geerbte Methode aus robot.api.ResultVisitor wird an dieser Stelle überschrieben,
105 | um folgende Aktionen beim Aufruf jeder Testsuite durchzuführen:
106 |
107 | - Wegschreiben der Ausführungsergebnisse aller Tests der Testsuite
108 | - Performanzstatiskten abrufen und für HTML aufbereiten
109 | - weitere Daten für Boxplot holen und Boxplot genieren
110 |
111 |
112 | :param suite: übergebene TestSuite inkl. aller Tests
113 | :type suite: TestSuite (siehe robot.api)
114 | """
115 | if not suite.suites:
116 | testcase_perf_stats = self.persistenceService.select_testcase_stats_filtered_by_suitename(suite.longname)
117 |
118 | joined_test_results: List[JoinedPerfTestResult] = self._eval_perf_of_tests(suite.tests, testcase_perf_stats)
119 | text: str = self._get_perf_result_table(joined_test_results)
120 |
121 | self.perf_results_list_of_testsuite = joined_test_results
122 |
123 | if self.boxplot_activated:
124 | testruns = self.persistenceService.select_testcase_runs_filtered_by_suitename(suite.longname)
125 |
126 | if len(testruns) == 0:
127 | text+= "\n *Box-Plot* \n\n No historical data to generate the Boxplot"
128 | else:
129 | rel_path_boxplot = self.visualizer.generate_boxplot_of_tests(testruns,suite.tests)
130 | text+= "\n *Box-Plot* \n\n ["+ rel_path_boxplot + "| Boxplot ]"
131 |
132 |
133 | suite.metadata["Performance Analysis"] = text
134 |
135 | if not suite.suites and not self.readonly:
136 | self.persistenceService.insert_multiple_testcase_runs(suite.tests)
137 |
138 |
139 | def visit_test(self, test):
140 | """Geerbte Methode aus robot.api.ResultVisitor wird an dieser Stelle überschrieben,
141 | um im Testbreaker-Modus die Testfälle bei schlechter Performanz auf FAIL zu setzen.
142 | Zudem wird über alle Keywords traversiert und ihre Laufzeiten gebündelt archiviert.
143 |
144 | :param test: übergebener Testfall
145 | :type test: TestCase (siehe robot.api)
146 | """
147 | if self.testbreaker_activated:
148 | for perf_result in self.perf_results_list_of_testsuite:
149 | if perf_result.longname == test.longname:
150 | calced_devn = perf_result.devn
151 | break
152 | if calced_devn:
153 | if calced_devn >self.max_deviation*100:
154 | old_test_status = test.status
155 | test.status = 'FAIL'
156 | test.message = "PerfError: Test run lasted " + f'{calced_devn:.2f}' + " % than the average runs in the past and is thus above the maximum threshold of " + f'{self.max_deviation*100:.2f}' + " % (original test status was "+ str(old_test_status) + ")."
157 |
158 | if not self.readonly and self.keywordstats:
159 | self.body_items_of_test= []
160 | counter = 0
161 | if test.setup:
162 | counter = self._recursive_keywords_traversal(test.setup,test.longname,0, counter)
163 |
164 | for bodyItem in test.body:
165 | if isinstance(bodyItem,Keyword):
166 | counter = self._recursive_keywords_traversal(bodyItem,test.longname,0, counter)
167 | if test.teardown:
168 | counter = self._recursive_keywords_traversal(test.teardown,test.longname,0, counter)
169 |
170 |
171 | self.persistenceService.insert_multiple_keyword_runs(self.body_items_of_test)
172 |
173 |
174 | def _recursive_keywords_traversal(self, bodyItem: Body, testcase_longname: str, level: int, counter: int):
175 | """Rekursiver Besuch aller Schlüsselwörter druch Pre-Order Traversal.
176 | Besuchte Schlüsselwörter werden in einer globalen Liste gechacht, bevor sie in die persistiert werden.
177 |
178 | :param bodyItem: i. d. R. das eigentliche Schlüsselwort
179 | :type bodyItem: Body
180 | :param testcase_longname: Testfall im Rahmen dessen dieser Schlüsselwort aufgerufen wurde.
181 | :type testcase_longname: str
182 | :param level: Baumtiefe (dient später zur Unterscheidung zwischen High- und Low-Level-Keywords)
183 | :type level: int
184 | :param counter: Nummer des Elternkontens im Pre-Order
185 | :type counter: int
186 | :return: liefert die neue Nummer gemäß Pre-Order
187 | :rtype: int
188 | """
189 |
190 | if isinstance(bodyItem,Keyword):
191 | level+=1
192 | counter+=1
193 | if isinstance(bodyItem.parent, Keyword):
194 | parentname = bodyItem.parent.kwname
195 | else:
196 | parentname = "NO KEYWORD"
197 |
198 | self.body_items_of_test.append(Keywordrun(bodyItem.kwname,bodyItem.name,testcase_longname, parentname,bodyItem.libname,str(bodyItem.starttime),str(bodyItem.elapsedtime),bodyItem.status,level,counter))
199 | for children in bodyItem.body:
200 | counter = self._recursive_keywords_traversal(children,testcase_longname,level, counter)
201 | return counter
202 |
203 |
204 | def _eval_perf_of_tests(self, tests, perfstats) -> List[JoinedPerfTestResult]:
205 | """Interne Methode zum Zusammenbauen der Daten zur aktuellen Ausführung und zur Performanzstatistik.
206 |
207 | :param tests: Liste von Testfällen
208 | :type tests: List[TestCase] (siehe Robot.result.model)
209 | :param perfstats: Liste von mehreren Testfällen und deren Statistikkennzahlen
210 | :type perfstats: List[TestPerfStats] (siehe model.py)
211 | :return: Liste der Testfälle mit Daten zur aktuellen Ausführung und Statistik
212 | :rtype: List[JoinedPerfTestResult] (siehe model.py)
213 | """
214 |
215 | #TODO: Eval-by=avg
216 | joined_stat_results = []
217 | for t in tests:
218 | isInStats = False
219 | for ps in perfstats:
220 | if ps[1] == t.longname:
221 | joined_test = JoinedPerfTestResult(name=t.name,longname=t.longname,elapsedtime=t.elapsedtime,avg=ps[2],min=ps[3],max=ps[4],count=ps[5],devn=((t.elapsedtime-ps[2])/ps[2])*100)
222 | joined_stat_results.append(joined_test)
223 | perfstats.remove(ps)
224 | isInStats = True
225 | break
226 | if not isInStats:
227 | joined_test = JoinedPerfTestResult(name=t.name,longname=t.longname,elapsedtime=t.elapsedtime,avg=None,min=None,max=None,count=None,devn=None)
228 | joined_stat_results.append(joined_test)
229 | return joined_stat_results
230 |
231 |
232 | def _get_perf_result_table(self, joined_perf_result_list: List[JoinedPerfTestResult]):
233 | """Interne Methode zum Erzeugen der formatierten Tabelle `Performance Analysis`.
234 |
235 | :param joined_perf_result_list: iste der Testfälle mit Daten zur aktuellen Ausführung und Statistik
236 | :type joined_perf_result_list: List[JoinedPerfTestResult]
237 | :return: formatierter Text bzw. Tabelle
238 | :rtype: str
239 | """
240 | text: str = TEXT_PERF_ANALYSIS_TABLE_HEADING
241 | for t in joined_perf_result_list:
242 | text+= TEXT_PERF_ANALYSIS_TABLE_ROW.format(name=t.name,elapsedtime=self._format_time_string(t.elapsedtime),avg=self._format_time_string(t.avg) if t.avg is not None else "NO STATS",min=self._format_time_string(t.min) if t.min is not None else "NO STATS",max=self._format_time_string(t.max) if t.max is not None else "NO STATS",count=t.count if t.count is not None else "NO STATS",devn=f'{t.devn:.2f}' if t.devn is not None else "NO STATS")
243 | return text
244 |
245 | def _format_time_string(self, val):
246 | return datetime.fromtimestamp(int(val) / 1e3).strftime("%M:%S.%f")[:-3]
247 |
--------------------------------------------------------------------------------
/perfbot/PerfEvalVisualizer.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import matplotlib.pyplot as plt
3 | from datetime import datetime
4 | import io
5 | from pathlib import Path
6 | import seaborn as sns
7 |
8 | class PerfEvalVisualizer:
9 | """Diese Klasse übernimmt die visuelle Aufbereitung von Performanzdaten der Testfälle.
10 |
11 | :return: _description_
12 | :rtype: _type_
13 | """
14 |
15 | def __init__(self, boxplot_folder=None):
16 | self.boxplot_folder = boxplot_folder
17 |
18 | def generate_boxplot_of_tests(self, hist_tests, act_tests):
19 | """Wrapper zum Aufruf von :py:meth:`~PerfEvalVisualizer.generate_boxplot()`.
20 | :param hist_tests: historische Daten als Liste
21 | :type hist_tests: list
22 | :param act_tests: aktuelle Daten als list
23 | :type act_tests: list
24 | :return: Pfad zur Bilddatei
25 | :rtype: str
26 | """
27 | hist = pd.DataFrame(hist_tests, columns =["id" ,"name", "longname", "starttime" , "elapsedtime" , "status"], copy=True)
28 |
29 | t_list = []
30 | for t in act_tests:
31 | t_json = {
32 | "name": t.name,
33 | "longname": t.longname,
34 | "elapsedtime": t.elapsedtime
35 | }
36 | t_list.append(t_json)
37 | act = pd.DataFrame(t_list, columns =["name", "longname", "elapsedtime"], copy=True)
38 |
39 | return self.generate_boxplot(hist, act, format="png")
40 |
41 | def generate_boxplot(self, hist_results: pd.DataFrame, act_results: pd.DataFrame, x="elapsedtime", y='name', xlabel="Duration (s)",ylabel="Testcase", heading='Box-Plot of the test duration times', format="svg"):
42 | """Generische Generation des Boxplots aus pd.DataFrames.
43 | Kontrekt wird ein Box-Plot erzeugt, darauf eine Punktwolke aller Werte und durch einen orangen Punkt die aktuelle Laufzeit.
44 |
45 | :param hist_results: Historische Daten aus denen der Boxplot generiert wird
46 | :type hist_results: pd.DataFrame
47 | :param act_results: Aktuelle Daten, die aktuelle Laufzeit im Boxplot makieren
48 | :type act_results: pd.DataFrame
49 | :param x: Spaltenname der x-Achse in den DataFrames, defaults to "elapsedtime"
50 | :type x: str, optional
51 | :param y: Spaltenname der y-Achse in den DataFrames, defaults to 'name'
52 | :type y: str, optional
53 | :param xlabel: Beschriftung der x-Achse, defaults to "Duration (s)"
54 | :type xlabel: str, optional
55 | :param ylabel: Beschriftung der y-Achse, defaults to "Testcase"
56 | :type ylabel: str, optional
57 | :param heading: Titel, defaults to 'Box-Plot of the test duration times'
58 | :type heading: str, optional
59 | :param format: Dateformat, ob ein Pfad zur Bilddatei oder ein SVG als String zurückgegeben wird, defaults to "svg"
60 | :type format: str, optional
61 | :return: Pfad zur Bilddatei oder SVG-String
62 | :rtype: str
63 | """
64 | sns.set_theme(style="whitegrid", context="notebook")
65 | hist = pd.DataFrame(hist_results, copy=True)
66 | hist[x] = hist[x].astype(int) / 1000
67 | boxplot = sns.boxplot(x=x, y=y, data=hist)
68 |
69 | boxplot.set_xlabel(xlabel)
70 | boxplot.set_ylabel(ylabel)
71 | boxplot.figure.suptitle(heading, fontsize=14, fontweight='bold')
72 | boxplot.set_title("")
73 |
74 | sns.stripplot(ax=boxplot,x=x, y=y, data=hist, color="grey")
75 |
76 | if True:
77 | act = pd.DataFrame(act_results, copy=True)
78 | act[x] = act[x].astype(int) / 1000
79 | plt.plot(act[x], act[y],'o', color='orange', zorder=10)
80 |
81 | match format:
82 | case "svg":
83 | f = io.StringIO()
84 | boxplot.figure.savefig(f, format = "svg", bbox_inches="tight")
85 | plt.clf()
86 | return f.getvalue()
87 |
88 | case "png":
89 | Path(self.boxplot_folder).mkdir(parents=True, exist_ok=True)
90 | pathname = self.boxplot_folder + "boxplot" + datetime.now().strftime("-%m-%d-%Y-%H-%M-%S-%f") + ".png"
91 | try:
92 | plt.savefig(pathname, bbox_inches="tight")
93 | plt.clf()
94 | except:
95 | print("An execption occured")
96 | print("Boxplot generiert: " + pathname)
97 | return pathname
98 | case _:
99 | raise KeyError("Wrong Format of Boxplot generation.")
100 |
--------------------------------------------------------------------------------
/perfbot/PersistenceService.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod, ABCMeta
2 | from robot.result.model import TestCase
3 | from .model import *
4 | from typing import List
5 |
6 | class PersistenceService:
7 | """Abstrakte Klasse, um die eigentliche Implementierung,
8 | wie die Testlaufergebnisse gespeichert bzw. abgerufen werden zu verschleiern.
9 | """
10 | __metaclass__ = ABCMeta
11 | #TODO: Doku wiederherstellen
12 |
13 | @abstractmethod
14 | def insert_test_execution(self, host_name):
15 | pass
16 |
17 | @abstractmethod
18 | def insert_testcase_run(self, testrun):
19 | pass
20 |
21 | @abstractmethod
22 | def insert_multiple_testcase_runs(self, testruns):
23 | pass
24 |
25 | @abstractmethod
26 | def insert_keyword_run(self, keywordrun):
27 | pass
28 |
29 | @abstractmethod
30 | def insert_multiple_keyword_runs(self, keywordruns):
31 | pass
32 |
33 | @abstractmethod
34 | def select_testcase_runs_filtered_by_testname(self, testcase_longname):
35 | pass
36 |
37 | @abstractmethod
38 | def select_testcase_runs_filtered_by_suitename(self, suite_longname):
39 | pass
40 |
41 | @abstractmethod
42 | def select_testcase_stats_filtered_by_testname(self, testcase_longname):
43 | pass
44 |
45 | @abstractmethod
46 | def select_testcase_stats_filtered_by_suitename(self, suite_longname):
47 | pass
48 |
49 | @abstractmethod
50 | def select_keyword_runs_filtered_by_testname(self, testcase_longname):
51 | pass
52 |
53 | @abstractmethod
54 | def select_keyword_runs_filtered_by_suitename(self, suite_longname):
55 | pass
56 |
57 | @abstractmethod
58 | def select_global_keyword_stats(self):
59 | pass
60 |
61 | @abstractmethod
62 | def select_positional_keyword_stats(self, testcase_filter=None):
63 | pass
64 |
65 | @abstractmethod
66 | def select_keyword_runs_filtered_by_positional_keyword(self, testcase_longname, keyword_longname, stepcounter):
67 | pass
--------------------------------------------------------------------------------
/perfbot/Sqlite3PersistenceService.py:
--------------------------------------------------------------------------------
1 | #import PersistenceService
2 | import sqlite3
3 | from .PersistenceService import PersistenceService, Testrun, TestPerfStats, StoredTestrun, Keywordrun
4 | from robot.result.model import TestCase, TestSuite, Keyword
5 | from typing import List
6 | from os.path import dirname, join
7 |
8 | SQL_CREATE_TABLES = "CREATE TABLE IF NOT EXISTS test_execution ( id integer PRIMARY KEY AUTOINCREMENT, imported_at text DEFAULT CURRENT_TIMESTAMP, hostname text ); CREATE TABLE IF NOT EXISTS testcase ( id integer PRIMARY KEY AUTOINCREMENT, name text, longname text, suitename text, UNIQUE(longname) ); CREATE TABLE IF NOT EXISTS keyword ( id integer PRIMARY KEY AUTOINCREMENT, name text, longname text, libname text, UNIQUE(longname) ); CREATE TABLE IF NOT EXISTS testcase_run ( id integer PRIMARY KEY AUTOINCREMENT, testcase_id integer REFERENCES testcase(id) ON DELETE CASCADE NOT NULL, test_execution_id integer REFERENCES test_execution(id) ON DELETE CASCADE NOT NULL, starttime text NOT NULL, elapsedtime text NOT NULL, status text NOT NULL ); CREATE TABLE IF NOT EXISTS keyword_run ( id integer PRIMARY KEY AUTOINCREMENT, testcase_run_id integer REFERENCES testcase_run(id) ON DELETE CASCADE NOT NULL, keyword_id integer REFERENCES keyword(id) ON DELETE CASCADE NOT NULL, starttime text NOT NULL, elapsedtime text NOT NULL, status text NOT NULL, keyword_level integer, stepcounter integer, parent_keyword_longname text ); CREATE VIEW IF NOT EXISTS testcase_run_view AS SELECT testcase.name, testcase.longname, testcase_run.starttime, testcase_run.elapsedtime, testcase_run.status, test_execution.id, test_execution.hostname FROM testcase_run INNER JOIN testcase ON testcase_run.testcase_id = testcase.id INNER JOIN test_execution ON testcase_run.test_execution_id = test_execution.id; CREATE VIEW IF NOT EXISTS keyword_run_view AS SELECT testcase.name as testcase_name, testcase.longname as testcase_longname, testcase.suitename, keyword.name as kw_name, keyword.longname as kw_longname, keyword.libname, keyword_run.starttime, keyword_run.elapsedtime, keyword_run.status, keyword_run.keyword_level, keyword_run.stepcounter, keyword_run.parent_keyword_longname, test_execution.id, test_execution.hostname FROM keyword_run INNER JOIN keyword ON keyword_run.keyword_id = keyword.id INNER JOIN testcase_run ON keyword_run.testcase_run_id = testcase_run.id INNER JOIN testcase ON testcase_run.testcase_id = testcase.id INNER JOIN test_execution ON testcase_run.test_execution_id = test_execution.id;"
9 |
10 | SQL_INSERT_TEST_EXECUTION = "INSERT INTO test_execution (hostname) VALUES (?);"
11 | SQL_SELECT_TEST_EXECUTION = "SELECT max(id) FROM test_execution VALUES (?,?,?)"
12 | SQL_INSERT_OR_IGNORE_TESTCASE = "INSERT OR IGNORE INTO testcase (name, longname, suitename) VALUES (?,?,?);"
13 | SQL_INSERT_TESTCASE_RUN = "INSERT INTO testcase_run (testcase_id, test_execution_id, starttime, elapsedtime, status) VALUES ((SELECT id FROM testcase WHERE longname = ?), (SELECT max(id) FROM test_execution), ?, ?,?);"
14 | SQL_INSERT_OR_IGNORE_KEYWORD = "INSERT OR IGNORE INTO keyword (name, longname, libname) VALUES (?,?,?)"
15 | SQL_INSERT_KEYWORD_RUN = "INSERT INTO keyword_run (testcase_run_id, keyword_id, starttime, elapsedtime, status, keyword_level, stepcounter, parent_keyword_longname) VALUES ((SELECT max(testcase_run.id) FROM testcase_run INNER JOIN testcase ON testcase_run.testcase_id = testcase.id WHERE testcase.longname =?), (SELECT id FROM keyword WHERE keyword.longname = ?),?,?,?,?,?,?)"
16 | SQL_SELECT_TESTCASE_RUNS_FILTERED_BY_TESTCASE = "SELECT id ,name, longname, starttime , elapsedtime , status FROM testcase_run_view WHERE longname = ?"
17 | SQL_SELECT_TESTCASE_RUNS_FILTERED_BY_TESTSUITE = "SELECT id ,name, longname, starttime , elapsedtime , status FROM testcase_run_view WHERE longname LIKE ?"
18 | SQL_SELECT_KEYWORD_RUNS_FILTERED_BY_TESTCASE = "SELECT * FROM keyword_run_view WHERE testcase_longname = ?"
19 | SQL_SELECT_KEYWORD_RUNS_FILTERED_BY_POSITIONAL_KEYWORD = "SELECT * FROM keyword_run_view WHERE testcase_longname = ? and kw_longname= ? and stepcounter= ?"
20 | SQL_SELECT_KEYWORD_RUNS_FILTERED_BY_TESTSUITE = "SELECT * FROM keyword_run_view WHERE testcase_longname LIKE ?"
21 | SQL_SELECT_TESTCASE_RUN_STATS_OF_TESTCASE = "SELECT name, longname, AVG(elapsedtime) as avg, MIN(elapsedtime) as min, MAX(elapsedtime) as max, count(elapsedtime) as count FROM testcase_run_view WHERE longname = ?"
22 | SQL_SELECT_TESTCASE_RUN_STATS_OF_TESTSUITE = "SELECT name, longname, AVG(elapsedtime) as avg, MIN(elapsedtime) as min, MAX(elapsedtime) as max, count(elapsedtime) as count FROM (SELECT * FROM testcase_run_view WHERE longname like ? AND status='PASS') AS TEMP GROUP BY longname;"
23 | SQL_SELECT_KEYWORD_RUN_STATS_GLOBAL = "SELECT kw_name, kw_longname, libname, avg(elapsedtime) as avg, min(elapsedtime) as min, max(elapsedtime) as max, count(elapsedtime) as count FROM keyword_run_view GROUP BY kw_longname;"
24 | SQL_SELECT_KEYWORD_RUN_STATS_POSITIONAL = "SELECT kw_name, kw_longname, testcase_longname, parent_keyword_longname, libname, keyword_level, stepcounter, avg(elapsedtime) as avg, min(elapsedtime) as min, max(elapsedtime) as max, count(elapsedtime) as count FROM keyword_run_view GROUP BY testcase_longname, kw_longname, stepcounter;"
25 | SQL_SELECT_KEYWORD_RUN_STATS_POSITIONAL_FILTERED_BY_TESTCASE = "SELECT kw_name, kw_longname, testcase_longname, parent_keyword_longname, libname, keyword_level, stepcounter, avg(elapsedtime) as avg, min(elapsedtime) as min, max(elapsedtime) as max, count(elapsedtime) as count FROM keyword_run_view WHERE testcase_longname = ? GROUP BY testcase_longname, kw_longname, stepcounter;"
26 |
27 | class Sqlite3PersistenceService(PersistenceService):
28 | """Persistierung der Testergebnisse erfolgt in einer lokalen Sqlite3-Datei.
29 |
30 | :param PersistenceService: Abstrakte Basisklasse, die die Methoden vorgibt.
31 | """
32 |
33 | def __init__(self, db_name: str):
34 | self.con = sqlite3.connect(db_name)
35 | self.cur = self.con.cursor()
36 | #TODO: Relevativer Pfad notwendig
37 | # with open(join(dirname(__file__), 'schema.sql', 'r') as sql_file:
38 | # sql_script = sql_file.read()
39 |
40 | self.cur.executescript(SQL_CREATE_TABLES)
41 | self.con.commit()
42 |
43 | def insert_test_execution(self, host_name):
44 | #TODO: Get start- und endtime of testexecution
45 | self.cur.execute(SQL_INSERT_TEST_EXECUTION, (host_name,))
46 | self.con.commit()
47 |
48 |
49 | def insert_testcase_run(self, test: TestCase):
50 | #TODO: Only works if test_execution is inserted before / no multiuser insert
51 | self.cur.execute(SQL_INSERT_OR_IGNORE_TESTCASE, (test.name, test.longname, test.parent.name));
52 | self.cur.execute(SQL_INSERT_TESTCASE_RUN, (test.longname, test.starttime, test.elapsedtime, test.status));
53 | self.con.commit()
54 |
55 | def insert_multiple_testcase_runs(self, testruns):
56 | temp_cases = []
57 | temp_runs = []
58 | for t in testruns:
59 | temp_cases.append((t.name, t.longname, t.parent.name))
60 | temp_runs.append((t.longname, t.starttime, t.elapsedtime, t.status))
61 | self.cur.executemany(SQL_INSERT_OR_IGNORE_TESTCASE, temp_cases)
62 | self.cur.executemany(SQL_INSERT_TESTCASE_RUN, temp_runs)
63 | self.con.commit()
64 |
65 | def insert_keyword_run(self, keywordrun: Keywordrun):
66 | #TODO: Voraussetzung: letzter Testlauf des Testfall ist geinserted und hat höchste ID
67 | self.cur.execute(SQL_INSERT_OR_IGNORE_KEYWORD, (keywordrun.name, keywordrun.longname, keywordrun.libname))
68 | self.cur.execute(SQL_INSERT_KEYWORD_RUN, (keywordrun.testcase_longname,keywordrun.longname,keywordrun.starttime,keywordrun.elapsedTime,keywordrun.status, keywordrun.keyword_level,keywordrun.counter,keywordrun.parent_keyword_longname))
69 | self.con.commit()
70 |
71 | def insert_multiple_keyword_runs(self, keywordruns):
72 | temp_keywords = []
73 | temp_runs = []
74 | for keywordrun in keywordruns:
75 | temp_keywords.append((keywordrun.name, keywordrun.longname, keywordrun.libname))
76 | temp_runs.append((keywordrun.testcase_longname,keywordrun.longname,keywordrun.starttime,keywordrun.elapsedTime,keywordrun.status, keywordrun.keyword_level,keywordrun.counter,keywordrun.parent_keyword_longname))
77 | self.cur.executemany(SQL_INSERT_OR_IGNORE_KEYWORD, temp_keywords)
78 | self.cur.executemany(SQL_INSERT_KEYWORD_RUN, temp_runs)
79 | self.con.commit()
80 |
81 | def select_testcase_runs_filtered_by_testname(self, testcase_longname):
82 | self.cur.execute(SQL_SELECT_TESTCASE_RUNS_FILTERED_BY_TESTCASE, (str(testcase_longname),))
83 | return self.cur.fetchall()
84 |
85 | def select_testcase_runs_filtered_by_suitename(self, suite_longname):
86 | self.cur.execute(SQL_SELECT_TESTCASE_RUNS_FILTERED_BY_TESTSUITE, (str(suite_longname + "%"),))
87 | return self.cur.fetchall()
88 |
89 | def select_testcase_stats_filtered_by_testname(self, testcase_longname):
90 | self.cur.execute(SQL_SELECT_TESTCASE_RUN_STATS_OF_TESTCASE, (str(testcase_longname),))
91 | return self.cur.fetchall()
92 |
93 | def select_testcase_stats_filtered_by_suitename(self, suite_longname) -> List[TestPerfStats]:
94 | self.cur.execute(SQL_SELECT_TESTCASE_RUN_STATS_OF_TESTSUITE, (str(suite_longname + "%"),))
95 | return self.cur.fetchall()
96 |
97 | def select_keyword_runs_filtered_by_testname(self, testcase_longname):
98 | self.cur.execute(SQL_SELECT_KEYWORD_RUNS_FILTERED_BY_TESTCASE, (str(testcase_longname),))
99 | return self.cur.fetchall()
100 |
101 | def select_keyword_runs_filtered_by_positional_keyword(self, testcase_longname, keyword_longname, stepcounter):
102 | self.cur.execute(SQL_SELECT_KEYWORD_RUNS_FILTERED_BY_POSITIONAL_KEYWORD, (testcase_longname,keyword_longname, stepcounter))
103 | return self.cur.fetchall()
104 |
105 | def select_keyword_runs_filtered_by_suitename(self, suite_longname):
106 | self.cur.execute(SQL_SELECT_KEYWORD_RUNS_FILTERED_BY_TESTCASE, (str(suite_longname + "%"),))
107 | return self.cur.fetchall()
108 |
109 | def select_global_keyword_stats(self):
110 | self.cur.execute(SQL_SELECT_KEYWORD_RUN_STATS_GLOBAL)
111 | return self.cur.fetchall()
112 |
113 |
114 | def select_positional_keyword_stats(self, testcase_filter=None):
115 | if testcase_filter:
116 | self.cur.execute(SQL_SELECT_KEYWORD_RUN_STATS_POSITIONAL_FILTERED_BY_TESTCASE,(testcase_filter,))
117 | return self.cur.fetchall()
118 | else:
119 | self.cur.execute(SQL_SELECT_KEYWORD_RUN_STATS_POSITIONAL)
120 | return self.cur.fetchall()
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/perfbot/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/perfbot/__init__.py
--------------------------------------------------------------------------------
/perfbot/model.py:
--------------------------------------------------------------------------------
1 | from typing import NamedTuple
2 | from robot.result.model import TestCase
3 |
4 | """Zentrale Datei für das Datenmodell von perfbot.
5 |
6 | Dieses Datenmodell ergänzt nur das vorhandene Datenmodell robot.model bzw. robot.result.model
7 | an notwendigen Stellen. Ansonsten wird das Datenmodell von robot genutzt.
8 | """
9 |
10 | class JoinedPerfTestResult(NamedTuple):
11 | """Vereint die relevanten Daten des TestCase aus den Robot-Datenmodell und
12 | den Statistiken zu diesem Testfall, die durch perfbot ermittelt wurden.
13 |
14 | :param NamedTuple: Basisklasse
15 | """
16 | name: str
17 | longname: str
18 | elapsedtime: any
19 | avg: any
20 | min: any
21 | max: any
22 | count: any
23 | devn: any
24 |
25 | class Testrun(NamedTuple):
26 | """Stellt die Repräsentation eines Testlaufs eines Testfalls dar.
27 | (So wie es der PersistenceService zurück gibt.)
28 | """
29 | name: any
30 | longname: any
31 | starttime: any
32 | elapsedtime: any
33 | status: any
34 |
35 | @staticmethod
36 | def from_robot_testCase(test: TestCase):
37 | """Möglichkeit der Erzeugung basierend auf dem Robot-Datenmodell
38 |
39 | :param test: TestCase (siehe robot.result.model)
40 | :type test: TestCase
41 | :return: erzeugtes Objekt vom Typ Testrun
42 | :rtype: Testrun
43 | """
44 | return Testrun(test.name, test.longname, str(test.starttime), test.elapsedtime, test.status)
45 |
46 | def get_values_as_tuple(self):
47 | return tuple(self.__dict__.values())
48 |
49 | class Keywordrun(NamedTuple):
50 | name: str
51 | longname: str
52 | testcase_longname: str
53 | parent_keyword_longname: any
54 | libname: any
55 | starttime: any
56 | elapsedTime: any
57 | status: any
58 | keyword_level: int
59 | counter: int
60 |
61 | class Keywordrun_stats(Keywordrun):
62 | avg: any
63 | min: any
64 | max: any
65 | count: any
66 |
67 | class StoredTestrun(Testrun):
68 | id: any
69 |
70 |
71 | class TestPerfStats:
72 | """Stellt Statistikkennzahlen eines Testfalls dar. Die Kennzahlen fassen i. d. R. mehrere Tetläufe des Testfalls zusammen.
73 | Die Testdaten
74 | (So wie es der PersistenceService zurück gibt.)
75 | """
76 | name = None
77 | longname = None
78 | avg = None
79 | min = None
80 | max = None
81 | count = None
82 |
83 | def __init__(self, name, longname, avg, min, max, count):
84 | self.name = name
85 | self.longname = longname
86 | self.avg = avg
87 | self.min = min
88 | self.max = max
89 | self.count = count
--------------------------------------------------------------------------------
/perfbot/perfbot.py:
--------------------------------------------------------------------------------
1 | from .PerfEvalResultModifier import PerfEvalResultModifier
2 |
3 | """Hier ist der Einstiegspunkt von perfbot:
4 |
5 | Perfbot ermittelt Performance-Veränderungen anhand von bestehenden
6 | automatisierten UI-Tests. Es erweitert dabei das
7 | [Robot Framework](http://www.robotframework.org)
8 | um die Möglichkeit, Test-Laufzeiten in einer Datenbank zu
9 | speichern und mit den archivierten Laufzeiten der Vergangenheit zu vergleichen.
10 | Das Ergebnisse der Performance-Analyse werden in die Robot-Testresults
11 | (`log.html` / `report.html`) integriert.
12 | """
13 |
14 | class perfbot(PerfEvalResultModifier):
15 | """Dies ist nur ein Wrapper, damit der Aufruf mit dem Parameter --prerebotmodifier perfbot/perfbot.py aufgerufen werden kann.
16 |
17 | :param PerfEvalResultModifier: Basisklasse in der die eigentliche Logik stattfindet.
18 | """
19 | pass
20 |
21 | def main():
22 | print("Please start with --prerobotmodifier option of rebot or robot")
--------------------------------------------------------------------------------
/perfbot/requirements.txt:
--------------------------------------------------------------------------------
1 | matplotlib==3.6.3
2 | pandas==1.5.3
3 | robotframework==6.0.2
4 | seaborn==0.12.2
5 |
--------------------------------------------------------------------------------
/perfbot/schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS test_execution (
2 | id integer PRIMARY KEY AUTOINCREMENT,
3 | imported_at text DEFAULT CURRENT_TIMESTAMP,
4 | hostname text
5 | );
6 |
7 | CREATE TABLE IF NOT EXISTS testcase (
8 | id integer PRIMARY KEY AUTOINCREMENT,
9 | name text,
10 | longname text,
11 | suitename text,
12 | UNIQUE(longname)
13 | );
14 |
15 | CREATE TABLE IF NOT EXISTS keyword (
16 | id integer PRIMARY KEY AUTOINCREMENT,
17 | name text,
18 | longname text,
19 | libname text,
20 | UNIQUE(longname)
21 | );
22 |
23 | CREATE TABLE IF NOT EXISTS testcase_run (
24 | id integer PRIMARY KEY AUTOINCREMENT,
25 | testcase_id integer REFERENCES testcase(id) ON DELETE CASCADE NOT NULL,
26 | test_execution_id integer REFERENCES test_execution(id) ON DELETE CASCADE NOT NULL,
27 | starttime text NOT NULL,
28 | elapsedtime text NOT NULL,
29 | status text NOT NULL
30 | );
31 |
32 | CREATE TABLE IF NOT EXISTS keyword_run (
33 | id integer PRIMARY KEY AUTOINCREMENT,
34 | testcase_run_id integer REFERENCES testcase_run(id) ON DELETE CASCADE NOT NULL,
35 | keyword_id integer REFERENCES keyword(id) ON DELETE CASCADE NOT NULL,
36 | starttime text NOT NULL,
37 | elapsedtime text NOT NULL,
38 | status text NOT NULL,
39 | keyword_level integer,
40 | stepcounter integer,
41 | parent_keyword_longname text
42 | );
43 |
44 | CREATE VIEW IF NOT EXISTS testcase_run_view AS
45 | SELECT testcase.name, testcase.longname, testcase_run.starttime, testcase_run.elapsedtime, testcase_run.status, test_execution.id, test_execution.hostname
46 | FROM testcase_run
47 | INNER JOIN testcase ON testcase_run.testcase_id = testcase.id
48 | INNER JOIN test_execution ON testcase_run.test_execution_id = test_execution.id;
49 |
50 |
51 | CREATE VIEW IF NOT EXISTS keyword_run_view AS
52 | SELECT testcase.name as testcase_name, testcase.longname as testcase_longname, testcase.suitename, keyword.name as kw_name, keyword.longname as kw_longname, keyword.libname, keyword_run.starttime, keyword_run.elapsedtime, keyword_run.status, keyword_run.keyword_level, keyword_run.stepcounter, keyword_run.parent_keyword_longname, test_execution.id, test_execution.hostname
53 | FROM keyword_run
54 | INNER JOIN keyword ON keyword_run.keyword_id = keyword.id
55 | INNER JOIN testcase_run ON keyword_run.testcase_run_id = testcase_run.id
56 | INNER JOIN testcase ON testcase_run.testcase_id = testcase.id
57 | INNER JOIN test_execution ON testcase_run.test_execution_id = test_execution.id;
--------------------------------------------------------------------------------
/perfbot/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # see https://github.com/ctb/SomePackage
2 | # implement a basic test under somepackage.tests
3 | import unittest
4 |
5 | def get_suite():
6 | "Return a unittest.TestSuite."
7 | import perfbot.tests
8 |
9 | loader = unittest.TestLoader()
10 | suite = loader.loadTestsFromModule(perfbot.tests)
11 | return suite
12 |
--------------------------------------------------------------------------------
/perfbot/tests/__main__.py:
--------------------------------------------------------------------------------
1 | # RUNME as 'python -m somepackage.tests.__main__'
2 | import unittest
3 | import perfbot.tests
4 |
5 | def main():
6 | "Run all of the tests when run as a module with -m."
7 | suite = perfbot.tests.get_suite()
8 | runner = unittest.TextTestRunner()
9 | runner.run(suite)
10 |
11 | if __name__ == '__main__':
12 | main()
--------------------------------------------------------------------------------
/perfbot/tests/golden.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Test 1
11 | Logs the given message with the given level.
12 | Test 1
13 |
14 |
15 |
16 | ${not really in source}
17 | tag not in source
18 |
19 | Log on ${TEST NAME}
20 | TRACE
21 | Logs the given message with the given level.
22 |
23 |
24 |
25 |
26 |
27 | ${x}
28 | not in source
29 |
30 | not in source
31 |
32 | ${x}
33 | Logs the given message with the given level.
34 | not in source
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | not going here
45 | Fails the test with the given message and optionally alters its tags.
46 |
47 |
48 |
49 |
50 |
51 |
52 | Not in source.
53 |
54 |
55 |
56 |
57 |
58 |
59 | Test case documentation
60 | t1
61 |
62 |
63 | Normal test cases
64 | My Value
65 |
66 |
67 |
68 |
69 | All Tests
70 |
71 |
72 | t1
73 |
74 |
75 | Normal
76 |
77 |
78 |
79 | Error in file 'normal.html' in table 'Settings': Resource file 'nope' does not exist.
80 |
81 |
--------------------------------------------------------------------------------
/perfbot/tests/goldenTwice.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Test 1
12 | Logs the given message with the given level.
13 | Test 1
14 |
15 |
16 |
17 | ${not really in source}
18 | tag not in source
19 |
20 | Log on ${TEST NAME}
21 | TRACE
22 | Logs the given message with the given level.
23 |
24 |
25 |
26 |
27 |
28 | ${x}
29 | not in source
30 |
31 | not in source
32 |
33 | ${x}
34 | Logs the given message with the given level.
35 | not in source
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | not going here
46 | Fails the test with the given message and optionally alters its tags.
47 |
48 |
49 |
50 |
51 |
52 |
53 | Not in source.
54 |
55 |
56 |
57 |
58 |
59 |
60 | Test case documentation
61 | t1
62 |
63 |
64 | Normal test cases
65 | My Value
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | Test 1
76 | Logs the given message with the given level.
77 | Test 1
78 |
79 |
80 |
81 | ${not really in source}
82 | tag not in source
83 |
84 | Log on ${TEST NAME}
85 | TRACE
86 | Logs the given message with the given level.
87 |
88 |
89 |
90 |
91 |
92 | ${x}
93 | not in source
94 |
95 | not in source
96 |
97 | ${x}
98 | Logs the given message with the given level.
99 | not in source
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | not going here
110 | Fails the test with the given message and optionally alters its tags.
111 |
112 |
113 |
114 |
115 |
116 |
117 | Not in source.
118 |
119 |
120 |
121 |
122 |
123 |
124 | Test case documentation
125 | t1
126 |
127 |
128 | Normal test cases
129 | My Value
130 |
131 |
132 |
133 |
134 |
135 |
136 | All Tests
137 |
138 |
139 | t1
140 |
141 |
142 | Normal & Normal
143 | Normal & Normal.Normal
144 | Normal & Normal.Normal
145 |
146 |
147 |
148 | Error in file 'normal.html' in table 'Settings': Resource file 'nope' does not exist.
149 | Error in file 'normal.html' in table 'Settings': Resource file 'nope' does not exist.
150 |
151 |
--------------------------------------------------------------------------------
/perfbot/tests/run.py:
--------------------------------------------------------------------------------
1 | # RUNME as 'python -m somepackage.tests.run'
2 | import unittest
3 | import perfbot.tests
4 |
5 | def main():
6 | "Run all of the tests when run as a module with -m."
7 | suite = perfbot.tests.get_suite()
8 | runner = unittest.TextTestRunner()
9 | runner.run(suite)
10 |
11 | if __name__ == '__main__':
12 | main()
--------------------------------------------------------------------------------
/perfbot/tests/test_PerfEvalVisualizer.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from ..PerfEvalVisualizer import *
3 | import os
4 |
5 |
6 | class TestSqlite3PersistenceService(unittest.TestCase):
7 | NAME_OF_TEST_BOXPLOT_FOLDER = "temp"
8 |
9 | def setUp(self):
10 | self.evalVisualizer = PerfEvalVisualizer(os.path.dirname(__file__) + "/" + self.NAME_OF_TEST_BOXPLOT_FOLDER)
11 | self.hist_tests = [(1," Invalid Username", "Tests.Invalid Login.Invalid Username", "20230427 09:57:55.116",666,"PASS"),
12 | (2," Invalid Username", "Tests.Invalid Login.Invalid Username", "20230427 09:58:14.637",400,"PASS"),
13 | (1," Invalid Username", "Tests.Invalid Login.Invalid Username", "20230427 09:57:58.631",3593,"PASS"),
14 | (2," Valid Login", "Tests.Valid Login.Valid Login", "20230427 09:57:55.116",3361,"PASS")]
15 | self.act_tests = [(1," Invalid Username", "Tests.Invalid Login.Invalid Username", "20230427 09:59:02.255",398,"PASS"),
16 | (2," Valid Login", "Tests.Valid Login.Valid Login", "20230427 09:59:04.598",3730,"PASS")]
17 |
18 | def tearDown(self):
19 | # folder = os.path.dirname(__file__) + "/" + self.NAME_OF_TEST_BOXPLOT_FOLDER
20 | # Remove folder
21 | pass
22 |
23 | def test_boxplot(self):
24 | hist = pd.DataFrame(self.hist_tests, columns =["id" ,"name", "longname", "starttime" , "elapsedtime" , "status"], copy=True)
25 | act = pd.DataFrame(self.act_tests, columns =["id" ,"name", "longname", "starttime" , "elapsedtime" , "status"], copy=True)
26 |
27 | svg_string: str = self.evalVisualizer.generate_boxplot(hist_results=hist,act_results=act)
28 | self.assertTrue("" in svg_string)
--------------------------------------------------------------------------------
/perfbot/tests/test_PersistenceService.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from ..Sqlite3PersistenceService import *
3 | import os, glob
4 | from robot.result import ExecutionResult
5 | from robot.result.model import TestCase, TestSuite
6 |
7 |
8 | # Beispieldaten entnommen aus: https://github.com/robotframework/robotframework/tree/master/utest/result
9 | RESULT_1 = ExecutionResult(os.path.join(os.path.dirname(__file__), 'golden.xml'))
10 | RESULT_2 = ExecutionResult(os.path.join(os.path.dirname(__file__), 'goldenTwice.xml'))
11 |
12 |
13 |
14 | class TestSqlite3PersistenceService(unittest.TestCase):
15 | NAME_OF_TEST_DB = "perfbot_db_for_unittesting.db"
16 |
17 | def setUp(self):
18 | self.persistenceService: PersistenceService = Sqlite3PersistenceService(os.path.dirname(__file__) + "/" + self.NAME_OF_TEST_DB)
19 | self.assertIsNotNone(self.persistenceService)
20 | self.assertIsInstance(self.persistenceService, PersistenceService)
21 |
22 | def tearDown(self):
23 | f = os.path.dirname(__file__) + "/" + self.NAME_OF_TEST_DB
24 | os.remove(f)
25 | pass
26 |
27 |
28 | def test_insert_and_select_testcase(self):
29 | self.persistenceService.insert_test_execution("Test-Hostname")
30 | self.persistenceService.insert_testcase_run(RESULT_1.suite.tests[0])
31 | result = self.persistenceService.select_testcase_runs_filtered_by_testname(RESULT_1.suite.tests[0].longname)
32 | self.assertTrue(len(result) == 1)
33 |
34 | def test_multiple_insert_and_select_testcase(self):
35 | # Testdaten enthalten nur 1 Tesfall pro Suite
36 | suite: TestSuite = RESULT_2.suite
37 | self.persistenceService.insert_test_execution("Test-Hostname")
38 | self.persistenceService.insert_multiple_testcase_runs( suite.suites[0].tests)
39 | result = self.persistenceService.select_testcase_stats_filtered_by_suitename(suite.suites[0].longname)
40 | self.assertTrue(len(result) == 1)
--------------------------------------------------------------------------------
/res/architektur_high_level.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/res/architektur_high_level.png
--------------------------------------------------------------------------------
/res/example-test-suite-summary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/res/example-test-suite-summary.png
--------------------------------------------------------------------------------
/res/example-testbreaker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/res/example-testbreaker.png
--------------------------------------------------------------------------------
/res/legende.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/res/legende.png
--------------------------------------------------------------------------------
/res/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/res/logo.png
--------------------------------------------------------------------------------
/res/perfbot_hochkant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/perfroboter/robotframework-perfbot/1db8f7e35f3f1b1d537da5e9dcbf907f1e0ce803/res/perfbot_hochkant.png
--------------------------------------------------------------------------------
/res/perfbot_laufzeitsicht_ueberblick.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name='perfbot',
5 | version="1.0.0",
6 | description='Perfbot',
7 | long_description='Performance Analysis for Robot Framework',
8 | classifiers=[
9 | 'Framework :: Robot Framework',
10 | 'Programming Language :: Python',
11 | 'Topic :: Software Development :: Testing',
12 | ],
13 | keywords='robotframework performance',
14 | author='Lennart Potthoff',
15 | author_email='git@circuit-break.in',
16 | url='https://github.com/perfroboter/robotframework-perfbot',
17 | license = 'MIT',
18 |
19 | packages=find_packages(),
20 | include_package_data= True,
21 | zip_safe=False,
22 |
23 | install_requires=[
24 | 'robotframework',
25 | #'jinja2', # Only for robotmetrics-Extension
26 | 'matplotlib',
27 | 'pandas',
28 | 'seaborn'
29 | ],
30 | entry_points={
31 | 'console_scripts': [
32 | 'perfbot=perfbot.perfbot:main'
33 | ]
34 | },
35 | test_suite="perfbot.tests",
36 | )
37 |
--------------------------------------------------------------------------------
/tests/Testplan.md:
--------------------------------------------------------------------------------
1 | # Testplan
2 |
3 | ## Teststrategie
4 |
5 | Die Qualität von Perfbot soll einerseits durch statische und dynamische Tests beurteilt werden. Alle Tests sind dabei automatisiert, um bei jeder Änderung die Regressionstestsuite (Unit- und Integrationstests) durchzuführen.
6 |
7 | ## Statische Code-Analyse
8 |
9 | ### Komplexitätsanalyse mit radon
10 |
11 | Mittels Radon wird die Komplexität des Codes analysiert. Die Aussage über die Komplexität soll genutzt werden, um einerseits dort, wo es sinnvoll ist, die Komplexität zu reduzieren, anderseits gibt es Orientierung, wo mehr Unit-Tests sinnvoll sind.
12 | ```bash
13 | python3 -m radon cc perfbot/* -a -s
14 | ```
15 | Je komplexer der Code, desto mehr Unit-Tests.
16 |
17 | ## Unittests
18 |
19 | Der Pythoncode wird mittels Unittests (Python-Modul `unittest`) getestet. Die Tests sind entsprechend der Python-Klassen im Ordner `perfbot\tests` abgelegt.
20 |
21 | ```bash
22 | python3 -m setup.py install #Ggf. erst neu bauen
23 | python3 -m setup.py test
24 | ```
25 |
26 | ## Integrationstest (Smoke-Test)
27 |
28 | Der Integrationstest im Sinne eines Smoke-Tests führt die Robot-Testfälle des Selenium-Beispiels (`example`) inkl. Ausführung von Perfbot durch und prüft im Anschluss, ob in der `log.html` die Informationen von Perfbot zu finden sind.
29 | Dabei werden drei Durchläufe gemacht, um sowohl die initiale Anlage der Datenbank, als auch den Rückgriff auf historischen Daten zu prüfen.
30 |
31 | ```bash
32 | # Ausführung aus Root-Ordner des Repos, damit Pfade im Skript korrekt sind
33 | # Hinweis im Skript wird Robot mit dem Befehl python3 -m robot gestartet
34 | python3 -m robot -o itest-output.xml -l itest-log.html -r itest-report.html tests/itests/Integrationstest.robot
35 | ```
--------------------------------------------------------------------------------
/tests/itests/Integrationstest.robot:
--------------------------------------------------------------------------------
1 | *** Settings ***
2 | Suite Setup Vorbereiten
3 | Documentation Integrationstest zur Perfbot. Startet die Beispieltests und prüft die log.html und report.html. Vorm Starten den Ablageort der LOG_HTML anpassen.
4 | Library Process
5 | Library SeleniumLibrary
6 | Library OperatingSystem
7 | Suite Teardown Aufraeumen
8 |
9 | *** Variables ***
10 | ${BROWSER} Chrome
11 | ${START_SUT} python3 example/sut/server.py
12 | ${RUN_ROBOT} python3 -m robot --prerebotmodifier perfbot.perfbot:devn=0.1:db_path="tests/itests/temp/test.db":boxplot=True:testbreaker=True:boxplot_folder="tests/itests/temp/" -o tempoutput.xml -l templog.html -r tempreport.html example/tests
13 |
14 | *** Test Cases ***
15 | Perfbot im ersten Durchlauf testen
16 | Beispiel mit Robot testen
17 | Vorhandensein der Dateien pruefen
18 | Log-Datei pruefen testflauf_anzahl=NO STATS
19 | # beim ersten Durchlauf gibt es noch keinen Boxplot
20 | Close Browser
21 | Perfbot im zweiten Durchlauf testen
22 | Beispiel mit Robot testen
23 | Vorhandensein der Dateien pruefen
24 | Log-Datei pruefen testflauf_anzahl=1
25 | Boxplot in Log-Datei und lokal pruefen
26 | Close Browser
27 | Perfbot im dritten Durchlauf testen
28 | Beispiel mit Robot testen
29 | Vorhandensein der Dateien pruefen
30 | Log-Datei pruefen testflauf_anzahl=2
31 | Boxplot in Log-Datei und lokal pruefen
32 | Close Browser
33 |
34 | *** Keywords ***
35 | Vorbereiten
36 | Remove Directory tests/itests/temp recursive=True
37 | Create Directory tests/itests/temp
38 | Beispiel SUT starten
39 | ${pwd}= Run Process pwd shell=yes
40 | Log pwd: ${pwd.stdout}
41 | Set Global Variable ${LOG_HTML} file://${pwd.stdout}/templog.html
42 | Log Ablageort der LOG-HTML ermittelt: ${LOG_HTML}
43 | Beispiel SUT starten
44 | Start Process ${START_SUT} shell=yes alias=sut
45 | Beispiel mit Robot testen
46 | ${result}= Run Process ${RUN_ROBOT} shell=yes
47 | Vorhandensein der Dateien pruefen
48 | File Should Exist tests/itests/temp/test.db
49 | File Should Exist tempoutput.xml
50 | File Should Exist templog.html
51 | File Should Exist tempreport.html
52 | Log-Datei pruefen
53 | [Arguments] ${logdatei}=${LOG_HTML} ${metadata_feld}=Performance Analysis: ${titel_in_tabelle}= Deviation from avg ${erster_testfall}=Invalid Username ${testflauf_anzahl}=1
54 | Open Browser ${logdatei} ${BROWSER}
55 | Title Should Be Tests Log
56 | Click Element css:div#s1-s1
57 | Page Should Contain Element css:div#s1-s1 table
58 | Table Should Contain locator=css:div#s1-s1 table expected=${metadata_feld}
59 | ${element}= GetWebElement locator=xpath://*[@id="s1-s1"]/div[2]/table/tbody/tr[3]/td/table
60 | Element Should Contain ${element} ${titel_in_tabelle}
61 | Table Cell Should Contain locator=${element} row=2 column=1 expected=${erster_testfall}
62 | Table Cell Should Contain locator=${element} row=2 column=6 expected=${testflauf_anzahl}
63 |
64 | Boxplot in Log-Datei und lokal pruefen
65 | Click Image //*[@id="s1-s1"]/div[2]/table/tbody/tr[3]/td/p[3]/img
66 | ${pic}= Get Element Attribute //*[@id="s1-s1"]/div[2]/table/tbody/tr[3]/td/p[3]/img src
67 | Log Boxplot saved under: ${pic}
68 | ${file}= Evaluate '${pic}'.replace('file://','')
69 | File Should Exist ${file}
70 | Aufraeumen
71 | Terminate All Processes kill=True
72 | Remove Directory tests/itests/temp recursive=True
73 | Remove File tempoutput.xml
74 | Remove File templog.html
75 | Remove File tempreport.html
76 |
--------------------------------------------------------------------------------