├── .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 | ![High-Level-Architektur anhand der generischen TA-Architektur](res/architektur_high_level.png) 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 | ![BPMN-Diagramm (Überblick)](res/perfbot_laufzeitsicht_ueberblick.svg) 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 | ![BPMN-Diagramm (Details)](res/perfbot_laufzeitsicht_details.svg) 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 | ![](res/example-test-suite-summary.png) 67 | 68 | Screenshot 2: Testbreaker 69 | ![](res/example-testbreaker.png) 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 | ![](res/example-test-suite-summary.png) 102 | 103 | Screenshot 2: Testbreaker in Aktion am Beispiel des Beispiels Login-Page 104 | ![](res/example-testbreaker.png) 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 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
 
35 |
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 | Robot Frameworkrobot (Tests ausführen)rebot (Bericht generieren)PerfbotPerfbot (Performance analysieren)Fokus von PerfbotTestausführung starten*.robotoutput.xmlreport.htmllog.htmlTestergebnisse liegen vorHistorie der Testläufe -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------