├── .gitignore ├── BUILD.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── atests ├── README ├── dbbot │ ├── command_line_arguments.txt │ └── sql_database.txt ├── libraries │ └── RobotSqliteDatabase.py ├── resources │ └── database.txt └── testdata │ ├── invalid_output.xml │ ├── multiple │ └── test_output.xml │ └── one_suite │ ├── output_latter.xml │ └── test_output.xml ├── dbbot ├── __init__.py ├── __main__.py ├── logger.py ├── reader │ ├── __init__.py │ ├── database_writer.py │ ├── reader_options.py │ └── robot_results_parser.py ├── robot_database.py └── run.py ├── doc ├── create_database_diagram_from_database_file.py ├── robot_database.md └── robot_database.png ├── examples └── failbot │ ├── README.md │ ├── bin │ └── failbot │ ├── failbot │ ├── __init__.py │ ├── database_reader.py │ ├── html_writer.py │ └── writer_options.py │ └── templates │ ├── layout.html │ ├── row.html │ └── table.html ├── setup.py └── tools └── migrate27to28 /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | *.pyc 3 | *.db* 4 | output.xml 5 | log.html 6 | report.html 7 | build 8 | *.swp 9 | dist 10 | MANIFEST 11 | -------------------------------------------------------------------------------- /BUILD.rst: -------------------------------------------------------------------------------- 1 | Releasing DbBot 2 | =============== 3 | 4 | #. Update __version__ in `dbbot/__init__.py` to release version (remove 5 | '-devel' suffix) 6 | #. Commit, push, add git tag with version number and push tags 7 | #. Upload to PyPi with: `python setup.py sdist upload` 8 | #. Check that page in PyPi looks good and `pip install dbbot` works. 9 | #. Change __version__ to 'x.x-devel' in `dbbot/__init__.py`, commit and 10 | push 11 | #. Send emails to: `announce`__- and `devel`__-lists. Tweet and add news to 12 | Confluence. 13 | 14 | __ mailto:robot-announcements@mlist.emea.nsn-intra.net 15 | __ mailto:robot-devel@mlist.emea.nsn-intra.net 16 | 17 | Directory structure 18 | ------------------- 19 | 20 | +-----------+------------------------------------------------------------------+ 21 | | Directory | Description | 22 | +===========+==================================================================+ 23 | | atests | Robot Framework-powered acceptance tests for DbBot. Also has | 24 | | | some test data in the `testdata` directory. | 25 | +-----------+------------------------------------------------------------------+ 26 | | dbbot | Source code files of DbBot. | 27 | +-----------+------------------------------------------------------------------+ 28 | | doc | Technical documentation about the database schema and utilities | 29 | | | to generate it. | 30 | +-----------+------------------------------------------------------------------+ 31 | | examples | Examples that are using the DbBot created database and extending | 32 | | | the 'dbbot' modules. | 33 | +-----------+------------------------------------------------------------------+ 34 | | tools | Additional scripts eg. converting databases generated with | 35 | | | Robot Framework 2.7 to 2.8. | 36 | +-----------+------------------------------------------------------------------+ 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **This project is currently not maintained.** 2 | 3 | DbBot 4 | ===== 5 | 6 | DbBot is a Python script to serialize `Robot Framework`_ output files into 7 | a SQLite database. This way the future `Robot Framework`_ related tools and 8 | plugins will have a unified storage for the test run results. 9 | 10 | Requirements 11 | ------------ 12 | 13 | - `Python`__ 2.6 or newer installed 14 | - `Robot Framework`_ 2.7 or newer installed 15 | 16 | `Robot Framework`_ version 2.7.4 or later is recommended as versions prior to 17 | 2.7.4 do not support storing total elapsed time for test runs or tags. 18 | 19 | How it works 20 | ------------ 21 | 22 | The script takes one or more `output.xml` files as input, initializes the 23 | database schema, and stores the respective results into a database 24 | (`robot\_results.db` by default, can be changed with options `-b` or 25 | `--database`). If database file is already existing, it will insert the new 26 | results into that database. 27 | 28 | Installation 29 | ------------ 30 | 31 | This tool is installed with pip with command: 32 | 33 | :: 34 | 35 | $ pip install dbbot 36 | 37 | Alternatively you can download the `source distribution`__, extract it and 38 | install using: 39 | 40 | :: 41 | 42 | $ python setup.py install 43 | 44 | What is stored 45 | -------------- 46 | 47 | Both the test data (names, content) and test statistics (how many did pass or 48 | fail, possible errors occurred, how long it took to run, etc.) related to 49 | suites and test cases are stored by default. However, keywords and related 50 | data are not stored as it might take order of magnitude longer for massive 51 | test runs. You can choose to store keywords and related data by using `-k` or 52 | `--also-keywords` flag. 53 | 54 | Usage examples 55 | -------------- 56 | 57 | Typical usage with a single output.xml file: 58 | 59 | :: 60 | 61 | python -m dbbot.run atest/testdata/one_suite/output.xml 62 | 63 | If the database does not already exist, it's created. Otherwise the test 64 | results are just inserted into the existing database. Only new results are 65 | inserted. 66 | 67 | The default database is a file named `robot_results.db`. 68 | 69 | Additional options are: 70 | 71 | +-------------------+---------------------------+--------------------------+ 72 | | Short format | Long format | Description | 73 | +===================+===========================+==========================+ 74 | | `-k` | `--also-keywords` | Parse also suites' and | 75 | | | | tests' keywords | 76 | +-------------------+---------------------------+--------------------------+ 77 | | `-v` | `--verbose` | Print output to the | 78 | | | | console. | 79 | +-------------------+---------------------------+--------------------------+ 80 | | `-b DB_FILE_PATH` | `--database=DB_FILE_PATH` | SQLite database for test | 81 | | | | run results | 82 | +-------------------+---------------------------+--------------------------+ 83 | | `-d` | `--dry-run` | Do everything except | 84 | | | | store the results. | 85 | +-------------------+---------------------------+--------------------------+ 86 | 87 | 88 | Specifying custom database name: 89 | 90 | :: 91 | 92 | $ python -m dbbot.run -b my_own_database.db atest/testdata/one_suite/output.xml 93 | 94 | Parsing test run results with keywords and related data included: 95 | 96 | :: 97 | 98 | python -m dbbot.run -k atest/testdata/one_suite/output.xml 99 | 100 | Giving multiple test run result files at the same time: 101 | 102 | :: 103 | 104 | python -m dbbot.run atest/testdata/one_suite/output.xml atest/testdata/one_suite/output_latter.xml 105 | 106 | Database 107 | -------- 108 | 109 | You can inspect the created database using the `sqlite3`_ command-line tool: 110 | 111 | .. code:: sqlite3 112 | 113 | $ sqlite3 robot_results.db 114 | 115 | sqlite> .tables 116 | arguments suite_status test_run_errors tests 117 | keyword_status suites test_run_status 118 | keywords tag_status test_runs 119 | messages tags test_status 120 | 121 | sqlite> SELECT count(), tests.id, tests.name 122 | FROM tests, test_status 123 | WHERE tests.id == test_status.test_id AND 124 | test_status.status == "FAIL" 125 | GROUP BY tests.name; 126 | 127 | Please note that when database is initialized, no indices are created by 128 | DbBot. This is to avoid slowing down the inserts. You might want to add 129 | indices to the database by hand to speed up certain queries in your own 130 | scripts. 131 | 132 | For information about the database schema, see `doc/robot_database.md`__. 133 | 134 | Migrating from Robot Framework 2.7 to 2.8 135 | ----------------------------------------- 136 | 137 | In Robot Framework 2.8, output.xml has changed slightly. Due this, the 138 | databases created with 2.7 need to migrated to be 2.8 compatible. 139 | 140 | To migrate the existing database, issue the following script: 141 | 142 | :: 143 | 144 | python tools/migrate27to28 -b 145 | 146 | Use case example: Most failing tests 147 | ------------------------------------ 148 | 149 | One of the common use cases for DbBot is to get a report of the most commonly 150 | failing suites, tests and keywords. There's an example for this purpose in 151 | `examples/FailBot/bin/failbot`. 152 | 153 | Failbot is a Python script used to produce a summary web page of the failing 154 | suites, tests and keywords, using the information stored in the DbBot 155 | database. Please adjust (the barebone) HTML templates in 156 | `examples/FailBot/templates` to your needs. 157 | 158 | Writing your own scripts 159 | ------------------------ 160 | 161 | Please take a look at the modules in `examples/FailBot/failbot` as an example 162 | on how to build on top of the classes provided by DbBot to satisfy your own 163 | scripting needs. 164 | 165 | License 166 | ------- 167 | 168 | DbBot is released under the `Apache License, Version 2.0`__. 169 | 170 | See LICENSE.TXT for details. 171 | 172 | __ https://www.python.org/ 173 | __ https://pypi.python.org/pypi/dbbot 174 | __ https://github.com/robotframework/DbBot/blob/master/doc/robot_database.md 175 | __ http://www.tldrlegal.com/license/apache-license-2.0 176 | .. _`Robot Framework`: http://www.robotframework.org 177 | .. _`pip`: http://www.pip-installer.org 178 | .. _`sqlite3`: https://www.sqlite.org/sqlite.html 179 | -------------------------------------------------------------------------------- /atests/README: -------------------------------------------------------------------------------- 1 | All tests are contained under `dbbot`-folder. 2 | 3 | $ pybot dbbot 4 | 5 | 6 | -------------------------------------------------------------------------------- /atests/dbbot/command_line_arguments.txt: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library OperatingSystem 3 | Resource ../resources/database.txt 4 | 5 | *** Variables *** 6 | ${valid_output} ${CURDIR}${/}..${/}testdata${/}one_suite${/}test_output.xml 7 | ${invalid_output} ${CURDIR}${/}..${/}testdata${/}invalid_output.xml 8 | ${not_existing_file} ${CURDIR}${/}..${/}testdata${/}not_existing.xml 9 | 10 | *** Test Cases *** 11 | 12 | Without arguments 13 | Run With ${EMPTY} 14 | Exits With Misused Arguments 15 | 16 | With -h 17 | Run With -h 18 | Prints Help 19 | Exits With Success 20 | 21 | With --help 22 | Run With --help 23 | Prints Help 24 | Exits With Success 25 | 26 | With invalid option 27 | Run With --invalid 28 | Prints No Such Option --invalid 29 | Exits With Misused Arguments 30 | 31 | With an existing file 32 | Run with ${valid_output} 33 | Exits With Success 34 | [Teardown] Remove Database 35 | 36 | With multiple existing files 37 | Run with ${valid_output} ${valid_output} 38 | Exits With Success 39 | [Teardown] Remove Database 40 | 41 | With no file names 42 | Run With ${EMPTY} 43 | Prints Input Files Required 44 | Exits With Misused Arguments 45 | 46 | With not existing file 47 | Run With ${not_existing_file} 48 | Prints File ${not_existing_file} Not Exists 49 | Exits With Misused Arguments 50 | 51 | With one of the files not existing 52 | Run with ${valid_output} ${not_existing_file} 53 | Exits With Misused Arguments 54 | 55 | With an invalid XML file 56 | Run With ${invalid_output} 57 | Prints Parse Error In ${invalid_output} 58 | Exits With Error 59 | [Teardown] Remove Database 60 | 61 | With --database and database name 62 | [Setup] Remove Database ${own_database} 63 | ${rc} ${output}= Run With --database=${own_database} ${valid_output} 64 | Should Create Database ${own_database} 65 | Should Not Create Default Database 66 | Exits With Success 67 | [Teardown] Remove Database ${own_database} 68 | 69 | With --database and no database name 70 | [Setup] Remove Database 71 | ${rc} ${output}= Run With --database 72 | Prints --database Requires Argument 73 | Should Not Create Default Database 74 | Exits With Misused Arguments 75 | 76 | With -b and database name 77 | [Setup] Remove Database ${own_database} 78 | ${rc} ${output}= Run With -b ${own_database} ${valid_output} 79 | Should Create Database ${own_database} 80 | Should Not Create Default Database 81 | Exits With Success 82 | [Teardown] Remove Database ${own_database} 83 | 84 | With -b and no database file name 85 | [Setup] Remove Database 86 | ${rc} ${output}= Run With -b 87 | Prints -b Requires Argument 88 | Should Not Create Default Database 89 | Exits With Misused Arguments 90 | 91 | With -v 92 | ${rc} ${output}= Run With -v ${valid_output} 93 | Is Verbose 94 | Exits With Success 95 | 96 | With --verbose 97 | ${rc} ${output}= Run With --verbose ${valid_output} 98 | Is Verbose 99 | Exits With Success 100 | 101 | With -d 102 | [Setup] Remove Database 103 | ${rc} ${output}= Run With -d ${valid_output} 104 | Should Not Create Default Database 105 | Exits With Success 106 | 107 | With --dry-run 108 | [Setup] Remove Database 109 | ${rc} ${output}= Run With --dry-run ${valid_output} 110 | Should Not Create Default Database 111 | Exits With Success 112 | 113 | With -k 114 | ${rc} ${output}= Run With -k ${valid_output} 115 | Exits With Success 116 | 117 | With --also-keywords 118 | ${rc} ${output}= Run With --also-keywords ${valid_output} 119 | Exits With Success 120 | 121 | 122 | *** Keywords *** 123 | 124 | Exits With ${value} 125 | Should Be Equal As Integers ${TEST RC} ${value} 126 | 127 | Exits With Success 128 | Should Not Contain ${TEST OUTPUT} error: 129 | Should Be Equal As Integers ${TEST RC} 0 130 | 131 | Exits With Error 132 | Should Contain ${TEST OUTPUT} : error: 133 | Should Be Equal As Integers ${TEST RC} 1 134 | 135 | Exits With Misused Arguments 136 | Should Contain ${TEST OUTPUT} ${program_name}: error: 137 | Should Be Equal As Integers ${TEST RC} 2 138 | 139 | Exits With Help 140 | Prints Help 141 | Should Be Equal As Integers ${TEST RC} 1 142 | 143 | Prints Help 144 | Should Contain ${TEST OUTPUT} Options: 145 | 146 | Prints Usage 147 | Should Contain ${TEST OUTPUT} Usage: ${program_name} [options] 148 | 149 | Prints No Such Option ${option} 150 | Prints Usage 151 | Should Contain ${TEST OUTPUT} ${program_name}: error: no such option: ${option} 152 | 153 | Prints Input Files Required 154 | Prints Usage 155 | Should Contain ${TEST OUTPUT} ${program_name}: error: at least one input file is required 156 | 157 | Prints ${option} Requires Argument 158 | Prints Usage 159 | Should Contain ${TEST OUTPUT} ${program_name}: error: ${option} option requires an argument 160 | 161 | Prints File ${filename} Not Exists 162 | Should Contain ${TEST OUTPUT} ${program_name}: error: file "${filename}" does not exist 163 | 164 | Prints Parse Error In ${filename} 165 | Should Contain ${TEST OUTPUT} dbbot: error: Invalid XML: Reading XML source '${filename}' failed: 166 | 167 | Is Verbose 168 | Should Contain ${TEST OUTPUT} Database | 169 | Should Contain ${TEST OUTPUT} Parser | 170 | 171 | Run With ${arguments} 172 | ${rc} ${output}= Run And Return Rc And Output ${program_path} ${arguments} 173 | Set Test Variable ${TEST RC} ${rc} 174 | Set Test Variable ${TEST OUTPUT} ${output} 175 | -------------------------------------------------------------------------------- /atests/dbbot/sql_database.txt: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library OperatingSystem 3 | Library ../libraries/RobotSqliteDatabase.py 4 | Resource ../resources/database.txt 5 | Test Teardown Disconnect And Cleanup 6 | 7 | *** Variables *** 8 | 9 | ${test_run} ${CURDIR}${/}..${/}testdata${/}one_suite${/}test_output.xml 10 | ${latter_test_run} ${CURDIR}${/}..${/}testdata${/}one_suite${/}output_latter.xml 11 | ${test_run_with_subsuites} ${CURDIR}${/}..${/}testdata${/}multiple${/}test_output.xml 12 | 13 | *** Test Cases *** 14 | 15 | Single test run results 16 | [Setup] Parse Without Keywords ${test_run} 17 | Should Have 1 Test Runs 18 | 19 | Single test run multiple times 20 | [Setup] Parse Without Keywords ${test_run} ${test_run} 21 | Should Have 1 Test Runs 22 | 23 | Single test run without keywords 24 | [Setup] Parse Without Keywords ${test_run} 25 | Should Have Suites And Tests 26 | Should Not Store Keywords 27 | 28 | Single test run with keywords 29 | [Setup]  Parse With Keywords ${test_run} 30 | Should Have Suites And Tests 31 | Should Have Keywords 32 | 33 | Single test run with subsuites 34 | [Setup]  Parse With Keywords ${test_run_with_subsuites} 35 | Should Have 1 Test Runs 36 | Should Have 2 Test Run Statuses 37 | Should Have 0 Test Run Errors 38 | Should Have 4 Suites 39 | Should Have 4 Suite Statuses 40 | Should Have 50 Tests 41 | Should Have 50 Test Statuses 42 | Should Have 150 Tags 43 | Should Have 3 Tag Statuses 44 | Should Have 41 Keywords 45 | Should Have 381 Keyword Statuses 46 | Should Have 139 Arguments 47 | Should Have 176 Messages 48 | 49 | Multiple test runs with the same root suite 50 | [Setup]  Parse With Keywords ${test_run} ${latter_test_run} 51 | Should Have ${2*1} Test Runs 52 | Should Have ${2*2} Test Run Statuses 53 | Should Have 0 Test Run Errors 54 | Should Have 1 Suites 55 | Should Have 2 Suite Statuses 56 | Should Have 19 Tests 57 | Should Have ${2*19} Test Statuses 58 | Should Have 57 Tags 59 | Should Have ${2*3} Tag Statuses 60 | Should Have 39 Keywords 61 | Should Have ${2*216} Keyword Statuses 62 | Should Have 131 Arguments 63 | Should Have 99 Messages 64 | [Teardown] Disconnect And Cleanup 65 | 66 | *** Keywords *** 67 | 68 | Parse Without Keywords ${files} 69 | Remove Database 70 | Run ${program_path} ${files} 71 | Connect To Database ${default_database} 72 | 73 | Parse With Keywords ${files} 74 | Remove Database 75 | Run ${program_path} ${files} --also-keywords 76 | Connect To Database ${default_database} 77 | 78 | Disconnect And Cleanup 79 | Close Connection 80 | Remove Database ${default_database} 81 | 82 | Should Have ${n} Test Runs 83 | Row Count Is Equal To ${n} test_runs 84 | 85 | Should Have ${n} Test Run Statuses 86 | Row Count Is Equal To ${n} test_run_status 87 | 88 | Should Have ${n} Test Run Errors 89 | Row Count Is Equal To ${n} test_run_errors 90 | 91 | Should Have ${n} Suites 92 | Row Count Is Equal To ${n} suites 93 | 94 | Should Have ${n} Suite Statuses 95 | Row Count Is Equal To ${n} suite_status 96 | 97 | Should Have ${n} Tests 98 | Row Count Is Equal To ${n} tests 99 | 100 | Should Have ${n} Test Statuses 101 | Row Count Is Equal To ${n} test_status 102 | 103 | Should Have ${n} Tags 104 | Row Count Is Equal To ${n} tags 105 | 106 | Should Have ${n} Tag Statuses 107 | Row Count Is Equal To ${n} tag_status 108 | 109 | Should Have ${n} Keywords 110 | Row Count Is Equal To ${n} keywords 111 | 112 | Should Have ${n} Keyword Statuses 113 | Row Count Is Equal To ${n} keyword_status 114 | 115 | Should Have ${n} Arguments 116 | Row Count Is Equal To ${n} arguments 117 | 118 | Should Have ${n} Messages 119 | Row Count Is Equal To ${n} messages 120 | 121 | Should Have Suites and Tests 122 | Should Have 1 Test Runs 123 | Should Have 2 Test Run Statuses 124 | Should Have 0 Test Run Errors 125 | Should Have 1 Suites 126 | Should Have 1 Suite Statuses 127 | Should Have 19 Tests 128 | Should Have 19 Test Statuses 129 | Should Have 57 Tags 130 | Should Have 3 Tag Statuses 131 | 132 | Should Have Keywords 133 | Should Have 39 Keywords 134 | Should Have 216 Keyword Statuses 135 | Should Have 131 Arguments 136 | Should Have 87 Messages 137 | 138 | Should Not Store Keywords 139 | Should Have 0 Keywords 140 | Should Have 0 Keyword Statuses 141 | Should Have 0 Arguments 142 | Should Have 0 Messages 143 | -------------------------------------------------------------------------------- /atests/libraries/RobotSqliteDatabase.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class RobotSqliteDatabase: 5 | 6 | def __init__(self): 7 | self._connection = None 8 | 9 | def connect_to_database(self, db_file_path): 10 | self._connection = sqlite3.connect(db_file_path) 11 | 12 | def close_connection(self): 13 | self._connection.close() 14 | 15 | def row_count_is_equal_to(self, count, db_table_name): 16 | actual_count = self._number_of_rows_in(db_table_name) 17 | if not actual_count == int(count): 18 | raise AssertionError('Expected to have %s rows but was %s' % 19 | (count, actual_count)) 20 | 21 | def _number_of_rows_in(self, db_table_name): 22 | cursor = self._execute('SELECT count() FROM %s' % db_table_name) 23 | return cursor.fetchone()[0] 24 | 25 | def _execute(self, sql_statement): 26 | return self._connection.execute(sql_statement) 27 | -------------------------------------------------------------------------------- /atests/resources/database.txt: -------------------------------------------------------------------------------- 1 | *** Variables *** 2 | ${default_database} robot_results.db 3 | ${own_database} my_database.db 4 | ${program name}= run.py 5 | ${program_path}= ${CURDIR}${/}..${/}..${/}dbbot${/}${program name} 6 | 7 | *** Keywords *** 8 | Should Create Database 9 | [Arguments] ${database} 10 | File Should Exist ${database} 11 | 12 | Should Not Create Default Database 13 | File Should Not Exist ${default_database} 14 | 15 | Remove Database 16 | [Arguments]  ${database}=${default_database} 17 | Remove File ${database} 18 | -------------------------------------------------------------------------------- /atests/testdata/invalid_output.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${EMPTY} 8 | standard_libraries/builtin/call_method.txt 9 | 10 | 11 | 12 | 13 | ${options} 14 | @{data list} 15 | 16 | 17 | 18 | 19 | @{data list} 20 | 21 | 22 | Makes a variable available everywhere within the scope of the current suite. 23 | 24 | $SUITE 25 | ${NONE} 26 | 27 | ${SUITE} = None 28 | 29 | 30 | 31 | 32 | 33 | @{data list} 34 | 35 | ${name} = call_method 36 | 37 | 38 | 39 | 40 | 41 | ${name} 42 | 43 | 44 | Joins the given path part(s) to the given base path. 45 | 46 | ${OUTPUTDIR} 47 | output 48 | ${name} 49 | 50 | ${OUTDIR} = /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method 51 | 52 | 53 | 54 | Makes a variable available globally in all tests and suites. 55 | 56 | $OUTDIR 57 | ${OUTDIR.encode('ascii', 'ignore').replace('?', '_') .replace('*', '_')} 58 | 59 | ${OUTDIR} = /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method 60 | 61 | 62 | 63 | Creates the specified directory. 64 | 65 | ${OUTDIR} 66 | 67 | Created directory '<a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method</a>' 68 | 69 | 70 | 71 | Makes a variable available everywhere within the scope of the current suite. 72 | 73 | $OUTFILE 74 | ${OUTDIR}${/}output.xml 75 | 76 | ${OUTFILE} = /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/output.xml 77 | 78 | 79 | 80 | Makes a variable available everywhere within the scope of the current suite. 81 | 82 | $STDOUT_FILE 83 | ${OUTDIR}${/}stdout.txt 84 | 85 | ${STDOUT_FILE} = /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stdout.txt 86 | 87 | 88 | 89 | Makes a variable available everywhere within the scope of the current suite. 90 | 91 | $STDERR_FILE 92 | ${OUTDIR}${/}stderr.txt 93 | 94 | ${STDERR_FILE} = /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stderr.txt 95 | 96 | 97 | 98 | Makes a variable available everywhere within the scope of the current suite. 99 | 100 | $SYSLOG_FILE 101 | ${OUTDIR}${/}syslog.txt 102 | 103 | ${SYSLOG_FILE} = /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/syslog.txt 104 | 105 | 106 | 107 | Sets an environment variable to a specified value. 108 | 109 | ROBOT_SYSLOG_FILE 110 | ${SYSLOG_FILE} 111 | 112 | Environment variable 'ROBOT_SYSLOG_FILE' set to value '/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/syslog.txt' 113 | 114 | 115 | 116 | 117 | 118 | 119 | Joins the given path part(s) to the given base path. 120 | 121 | ${ROBOTPATH} 122 | run.py 123 | 124 | ${robot} = /Users/asyrjasalo/eficode/robot/robotframework/src/robot/run.py 125 | 126 | 127 | 128 | Makes a variable available everywhere within the scope of the current suite. 129 | 130 | $ROBOT 131 | ${INTERPRETER} ${robot} 132 | 133 | ${ROBOT} = python /Users/asyrjasalo/eficode/robot/robotframework/src/robot/run.py 134 | 135 | 136 | 137 | Joins the given path part(s) to the given base path. 138 | 139 | ${ROBOTPATH} 140 | rebot.py 141 | 142 | ${rebot} = /Users/asyrjasalo/eficode/robot/robotframework/src/robot/rebot.py 143 | 144 | 145 | 146 | Makes a variable available everywhere within the scope of the current suite. 147 | 148 | $REBOT 149 | ${INTERPRETER} ${rebot} 150 | 151 | ${REBOT} = python /Users/asyrjasalo/eficode/robot/robotframework/src/robot/rebot.py 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | Joins given paths with base and returns resulted paths. 160 | 161 | ${DATADIR} 162 | @{data list} 163 | 164 | @{data list} = [ /Users/asyrjasalo/eficode/robot/robotframework/atest/testdata/standard_libraries/builtin/call_method.txt ] 165 | 166 | 167 | 168 | Catenates the given items together and returns the resulted string. 169 | 170 | @{data list} 171 | 172 | ${data string} = /Users/asyrjasalo/eficode/robot/robotframework/atest/testdata/standard_libraries/builtin/call_method.txt 173 | 174 | 175 | ${data string} = /Users/asyrjasalo/eficode/robot/robotframework/atest/testdata/standard_libraries/builtin/call_method.txt 176 | 177 | 178 | 179 | Catenates the given items together and returns the resulted string. 180 | 181 | --MonitorMarkers OFF 182 | ${user options} 183 | --variable interpreter:${INTERPRETER} 184 | --pythonpath ${LIBPATH1} 185 | --pythonpath ${LIBPATH2} 186 | 187 | ${options} = --MonitorMarkers OFF --variable interpreter:python --pythonpath /Users/asyrjasalo/eficode/robot/robotframework/atest/resources/../testresources/testlibs --pythonpath /Users/asyrjasalo/eficode/robot/r... 188 | 189 | 190 | 191 | 192 | 193 | ${ROBOT} 194 | ${options} 195 | ${data string} 196 | 197 | 198 | Uses `Remove File` to remove multiple files one-by-one. 199 | 200 | ${OUTFILE} 201 | ${OUTDIR}/*.xml 202 | ${OUTDIR}/*.html 203 | 204 | File '<a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/output.xml">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/output.xml</a>' does not exist 205 | File '<a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/*.xml">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/*.xml</a>' does not exist 206 | File '<a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/*.html">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/*.html</a>' does not exist 207 | 208 | 209 | 210 | Catenates the given items together and returns the resulted string. 211 | 212 | ${runner} 213 | --monitorcolors OFF 214 | --outputdir ${OUTDIR} 215 | --output ${OUTFILE} 216 | --report NONE 217 | --log NONE 218 | ${options} 219 | ${data string} 220 | 1>${STDOUTFILE} 221 | 2>${STDERRFILE} 222 | 223 | ${cmd} = python /Users/asyrjasalo/eficode/robot/robotframework/src/robot/run.py --monitorcolors OFF --outputdir /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method --output /... 224 | 225 | 226 | 227 | Runs the given command in the system and returns the return code. 228 | 229 | ${cmd} 230 | 231 | Running command 'python /Users/asyrjasalo/eficode/robot/robotframework/src/robot/run.py --monitorcolors OFF --outputdir /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method --output /Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/output.xml --report NONE --log NONE --MonitorMarkers OFF --variable interpreter:python --pythonpath /Users/asyrjasalo/eficode/robot/robotframework/atest/resources/../testresources/testlibs --pythonpath /Users/asyrjasalo/eficode/robot/robotframework/atest/resources/../testresources/listeners /Users/asyrjasalo/eficode/robot/robotframework/atest/testdata/standard_libraries/builtin/call_method.txt 1>/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stdout.txt 2>/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stderr.txt' 232 | ${rc} = 3 233 | 234 | 235 | 236 | Logs the given message with the given level. 237 | 238 | <a href="file://${OUTDIR}">${OUTDIR}</a> 239 | HTML 240 | 241 | <a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method</a> 242 | 243 | 244 | 245 | Logs the given message with the given level. 246 | 247 | <a href="file://${OUTFILE}">${OUTFILE}</a> 248 | HTML 249 | 250 | <a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/output.xml">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/output.xml</a> 251 | 252 | 253 | 254 | Logs the given message with the given level. 255 | 256 | <a href="file://${STDOUTFILE}">${STDOUTFILE}</a> 257 | HTML 258 | 259 | <a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stdout.txt">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stdout.txt</a> 260 | 261 | 262 | 263 | Logs the given message with the given level. 264 | 265 | <a href="file://${STDERRFILE}">${STDERRFILE}</a> 266 | HTML 267 | 268 | <a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stderr.txt">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/stderr.txt</a> 269 | 270 | 271 | 272 | Logs the given message with the given level. 273 | 274 | <a href="file://${SYSLOGFILE}">${SYSLOGFILE}</a> 275 | HTML 276 | 277 | <a href="file:///Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/syslog.txt">/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/syslog.txt</a> 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | ${OUTFILE} 288 | 289 | ${SUITE} = Call Method 290 | ${STATISTICS} = <robot.model.statistics.Statistics object at 0x10a952990> 291 | ${ERRORS} = <robot.result.executionerrors.ExecutionErrors object at 0x10a94cc50> 292 | Processing output '/Users/asyrjasalo/eficode/robot/robotframework/atest/results/python/output/call_method/output.xml' 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | ${TEST NAME} 302 | 303 | 304 | 305 | 306 | ${SUITE} 307 | ${name} 308 | 309 | ${test} = Call Method 310 | 311 | 312 | 313 | Verifies that test's status and message are as expected. 314 | 315 | ${test} 316 | ${status} 317 | ${message} 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | jybot 326 | pybot 327 | regression 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | ${TEST NAME} 336 | 337 | 338 | 339 | 340 | ${SUITE} 341 | ${name} 342 | 343 | ${test} = Call Method Returns 344 | 345 | 346 | 347 | Verifies that test's status and message are as expected. 348 | 349 | ${test} 350 | ${status} 351 | ${message} 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | jybot 360 | pybot 361 | regression 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | ${TEST NAME} 370 | 371 | 372 | 373 | 374 | ${SUITE} 375 | ${name} 376 | 377 | ${test} = Call Method From Module 378 | 379 | 380 | 381 | Verifies that test's status and message are as expected. 382 | 383 | ${test} 384 | ${status} 385 | ${message} 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | jybot 394 | pybot 395 | regression 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | ${TEST NAME} 404 | 405 | 406 | 407 | 408 | ${SUITE} 409 | ${name} 410 | 411 | ${test} = Call Non Existing Method 412 | 413 | 414 | 415 | Verifies that test's status and message are as expected. 416 | 417 | ${test} 418 | ${status} 419 | ${message} 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | jybot 428 | pybot 429 | regression 430 | 431 | 432 | 433 | Robot Framework acceptance tests 434 | 435 | python 436 | darwin 437 | 438 | 439 | 440 | 441 | 442 | Critical Tests 443 | All Tests 444 | 445 | 446 | regression 447 | jybot NOT pybot 448 | pybot NOT jybot 449 | 450 | 451 | Call Method 452 | 453 | 454 | 455 | -------------------------------------------------------------------------------- /dbbot/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = '0.2-devel' 16 | 17 | from .logger import Logger 18 | from .robot_database import RobotDatabase 19 | 20 | -------------------------------------------------------------------------------- /dbbot/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from dbbot.run import DbBot 15 | 16 | DbBot().run() 17 | -------------------------------------------------------------------------------- /dbbot/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | class Logger(object): 15 | 16 | def __init__(self, header, stream): 17 | self._header = header 18 | self._stream = stream 19 | 20 | def __call__(self, message): 21 | if self._stream: 22 | self._stream.write(' %-8s | %s\n' % (self._header, message)) 23 | -------------------------------------------------------------------------------- /dbbot/reader/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from .database_writer import DatabaseWriter 15 | from .reader_options import ReaderOptions 16 | from .robot_results_parser import RobotResultsParser 17 | -------------------------------------------------------------------------------- /dbbot/reader/database_writer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from dbbot import RobotDatabase 15 | 16 | 17 | class DatabaseWriter(RobotDatabase): 18 | 19 | def __init__(self, db_file_path, verbose_stream): 20 | super(DatabaseWriter, self).__init__(db_file_path, verbose_stream) 21 | self._init_schema() 22 | 23 | def _init_schema(self): 24 | self._verbose('- Initializing database schema') 25 | self._create_table_test_runs() 26 | self._create_table_test_run_status() 27 | self._create_table_test_run_errors() 28 | self._create_table_tag_status() 29 | self._create_table_suites() 30 | self._create_table_suite_status() 31 | self._create_table_tests() 32 | self._create_table_test_status() 33 | self._create_table_keywords() 34 | self._create_table_keyword_status() 35 | self._create_table_messages() 36 | self._create_table_tags() 37 | self._create_table_arguments() 38 | 39 | def _create_table_test_runs(self): 40 | self._create_table('test_runs', { 41 | 'hash': 'TEXT NOT NULL', 42 | 'imported_at': 'DATETIME NOT NULL', 43 | 'source_file': 'TEXT', 44 | 'started_at': 'DATETIME', 45 | 'finished_at': 'DATETIME', 46 | }, ('hash',)) 47 | 48 | def _create_table_test_run_status(self): 49 | self._create_table('test_run_status', { 50 | 'test_run_id': 'INTEGER NOT NULL REFERENCES test_runs', 51 | 'name': 'TEXT NOT NULL', 52 | 'elapsed': 'INTEGER', 53 | 'failed': 'INTEGER NOT NULL', 54 | 'passed': 'INTEGER NOT NULL' 55 | }, ('test_run_id', 'name')) 56 | 57 | def _create_table_test_run_errors(self): 58 | self._create_table('test_run_errors', { 59 | 'test_run_id': 'INTEGER NOT NULL REFERENCES test_runs', 60 | 'level': 'TEXT NOT NULL', 61 | 'timestamp': 'DATETIME NOT NULL', 62 | 'content': 'TEXT NOT NULL' 63 | }, ('test_run_id', 'level', 'content')) 64 | 65 | def _create_table_tag_status(self): 66 | self._create_table('tag_status', { 67 | 'test_run_id': 'INTEGER NOT NULL REFERENCES test_runs', 68 | 'name': 'TEXT NOT NULL', 69 | 'critical': 'INTEGER NOT NULL', 70 | 'elapsed': 'INTEGER', 71 | 'failed': 'INTEGER NOT NULL', 72 | 'passed': 'INTEGER NOT NULL', 73 | }, ('test_run_id', 'name')) 74 | 75 | def _create_table_suites(self): 76 | self._create_table('suites', { 77 | 'suite_id': 'INTEGER REFERENCES suites', 78 | 'xml_id': 'TEXT NOT NULL', 79 | 'name': 'TEXT NOT NULL', 80 | 'source': 'TEXT', 81 | 'doc': 'TEXT' 82 | }, ('name', 'source')) 83 | 84 | def _create_table_suite_status(self): 85 | self._create_table('suite_status', { 86 | 'test_run_id': 'INTEGER NOT NULL REFERENCES test_runs', 87 | 'suite_id': 'INTEGER NOT NULL REFERENCES suites', 88 | 'elapsed': 'INTEGER NOT NULL', 89 | 'failed': 'INTEGER NOT NULL', 90 | 'passed': 'INTEGER NOT NULL', 91 | 'status': 'TEXT NOT NULL' 92 | }, ('test_run_id', 'suite_id')) 93 | 94 | def _create_table_tests(self): 95 | self._create_table('tests', { 96 | 'suite_id': 'INTEGER NOT NULL REFERENCES suites', 97 | 'xml_id': 'TEXT NOT NULL', 98 | 'name': 'TEXT NOT NULL', 99 | 'timeout': 'TEXT', 100 | 'doc': 'TEXT' 101 | }, ('suite_id', 'name')) 102 | 103 | def _create_table_test_status(self): 104 | self._create_table('test_status', { 105 | 'test_run_id': 'INTEGER NOT NULL REFERENCES test_runs', 106 | 'test_id': 'INTEGER NOT NULL REFERENCES tests', 107 | 'status': 'TEXT NOT NULL', 108 | 'elapsed': 'INTEGER NOT NULL' 109 | }, ('test_run_id', 'test_id')) 110 | 111 | def _create_table_keywords(self): 112 | self._create_table('keywords', { 113 | 'suite_id': 'INTEGER REFERENCES suites', 114 | 'test_id': 'INTEGER REFERENCES tests', 115 | 'keyword_id': 'INTEGER REFERENCES keywords', 116 | 'name': 'TEXT NOT NULL', 117 | 'type': 'TEXT NOT NULL', 118 | 'timeout': 'TEXT', 119 | 'doc': 'TEXT' 120 | }, ('name', 'type')) 121 | 122 | def _create_table_keyword_status(self): 123 | self._create_table('keyword_status', { 124 | 'test_run_id': 'INTEGER NOT NULL REFERENCES test_runs', 125 | 'keyword_id': 'INTEGER NOT NULL REFERENCES keywords', 126 | 'status': 'TEXT NOT NULL', 127 | 'elapsed': 'INTEGER NOT NULL' 128 | }) 129 | 130 | def _create_table_messages(self): 131 | self._create_table('messages', { 132 | 'keyword_id': 'INTEGER NOT NULL REFERENCES keywords', 133 | 'level': 'TEXT NOT NULL', 134 | 'timestamp': 'DATETIME NOT NULL', 135 | 'content': 'TEXT NOT NULL' 136 | }, ('keyword_id', 'level', 'content')) 137 | 138 | def _create_table_tags(self): 139 | self._create_table('tags', { 140 | 'test_id': 'INTEGER NOT NULL REFERENCES tests', 141 | 'content': 'TEXT NOT NULL' 142 | }, ('test_id', 'content')) 143 | 144 | def _create_table_arguments(self): 145 | self._create_table('arguments', { 146 | 'keyword_id': 'INTEGER NOT NULL REFERENCES keywords', 147 | 'content': 'TEXT NOT NULL' 148 | }, ('keyword_id', 'content')) 149 | 150 | def _create_table(self, table_name, columns, unique_columns=()): 151 | definitions = ['id INTEGER PRIMARY KEY'] 152 | for column_name, properties in columns.items(): 153 | definitions.append('%s %s' % (column_name, properties)) 154 | if unique_columns: 155 | unique_column_names = ', '.join(unique_columns) 156 | definitions.append('CONSTRAINT unique_%s UNIQUE (%s)' % ( 157 | table_name, unique_column_names) 158 | ) 159 | sql_statement = 'CREATE TABLE IF NOT EXISTS %s (%s)' % (table_name, ', '.join(definitions)) 160 | self._connection.execute(sql_statement) 161 | 162 | def rename_table(self, old_name, new_name): 163 | sql_statement = 'ALTER TABLE %s RENAME TO %s' % (old_name, new_name) 164 | self._connection.execute(sql_statement) 165 | 166 | def drop_table(self, table_name): 167 | sql_statement = 'DROP TABLE %s' % table_name 168 | self._connection.execute(sql_statement) 169 | 170 | def copy_table(self, from_table, to_table, columns_to_copy): 171 | column_names = ', '.join(columns_to_copy) 172 | sql_statement = 'INSERT INTO %s(%s) SELECT %s FROM %s' % ( 173 | to_table, 174 | column_names, 175 | column_names, 176 | from_table 177 | ) 178 | self._connection.execute(sql_statement) 179 | 180 | def fetch_id(self, table_name, criteria): 181 | sql_statement = 'SELECT id FROM %s WHERE ' % table_name 182 | sql_statement += ' AND '.join('%s=?' % key for key in criteria.keys()) 183 | res = self._connection.execute(sql_statement, criteria.values()).fetchone() 184 | if not res: 185 | raise Exception('Query did not yield id, even though it should have.' 186 | '\nSQL statement was:\n%s\nArguments were:\n%s' % (sql_statement, criteria.values())) 187 | return res[0] 188 | 189 | def insert(self, table_name, criteria): 190 | sql_statement = self._format_insert_statement(table_name, criteria.keys()) 191 | cursor = self._connection.execute(sql_statement, criteria.values()) 192 | return cursor.lastrowid 193 | 194 | def insert_or_ignore(self, table_name, criteria): 195 | sql_statement = self._format_insert_statement(table_name, criteria.keys(), 'IGNORE') 196 | self._connection.execute(sql_statement, criteria.values()) 197 | 198 | def insert_many_or_ignore(self, table_name, column_names, values): 199 | sql_statement = self._format_insert_statement(table_name, column_names, 'IGNORE') 200 | self._connection.executemany(sql_statement, values) 201 | 202 | def _format_insert_statement(self, table_name, column_names, on_conflict='ABORT'): 203 | return 'INSERT OR %s INTO %s (%s) VALUES (%s)' % ( 204 | on_conflict, 205 | table_name, 206 | ','.join(column_names), 207 | ','.join('?' * len(column_names)) 208 | ) 209 | 210 | def commit(self): 211 | self._verbose('- Committing changes into database') 212 | self._connection.commit() 213 | 214 | -------------------------------------------------------------------------------- /dbbot/reader/reader_options.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from optparse import OptionParser 15 | from os.path import exists 16 | 17 | 18 | DEFAULT_DB_NAME = 'robot_results.db' 19 | 20 | class ReaderOptions(object): 21 | 22 | def __init__(self): 23 | self._parser = OptionParser() 24 | self._add_parser_options() 25 | self._options, self._files = self._get_validated_options() 26 | 27 | def _add_parser_options(self): 28 | options = [ 29 | ('-d', '--dry-run', {'action': 'store_true', 30 | 'default': False, 31 | 'dest': 'dry_run', 32 | 'help': 'do everything except store results into disk'}), 33 | 34 | ('-k', '--also-keywords', {'action':'store_true', 35 | 'default': False, 36 | 'dest': 'include_keywords', 37 | 'help': 'parse also suites\' and tests\' keywords'}), 38 | 39 | ('-v', '--verbose', {'action': 'store_true', 40 | 'default': False, 41 | 'dest': 'be_verbose', 42 | 'help': 'be verbose about the operation'}), 43 | 44 | ('-b', '--database', {'dest': 'db_file_path', 45 | 'default': DEFAULT_DB_NAME, 46 | 'help': 'path to the SQLite database for test run results'}) 47 | ] 48 | for option in options: 49 | self._parser.add_option(option[0], option[1], **option[2]) 50 | 51 | def _get_validated_options(self): 52 | options, files = self._parser.parse_args() 53 | self._check_files(files) 54 | return options, files 55 | 56 | def _check_files(self, files): 57 | if not files or len(files) < 1: 58 | self._parser.error('at least one input file is required') 59 | for file_path in files: 60 | if not exists(file_path): 61 | self._parser.error('file "%s" does not exist' % file_path) 62 | 63 | def _exit_with_help(self): 64 | self._parser.print_help() 65 | exit(1) 66 | 67 | @property 68 | def db_file_path(self): 69 | return self._options.db_file_path 70 | 71 | @property 72 | def be_verbose(self): 73 | return self._options.be_verbose 74 | 75 | @property 76 | def file_paths(self): 77 | return self._files 78 | 79 | @property 80 | def dry_run(self): 81 | return self._options.dry_run 82 | 83 | @property 84 | def include_keywords(self): 85 | return self._options.include_keywords 86 | -------------------------------------------------------------------------------- /dbbot/reader/robot_results_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from __future__ import with_statement 15 | from datetime import datetime 16 | from hashlib import sha1 17 | from robot.api import ExecutionResult 18 | from sqlite3 import IntegrityError 19 | 20 | 21 | from dbbot import Logger 22 | 23 | 24 | class RobotResultsParser(object): 25 | 26 | def __init__(self, include_keywords, db, verbose_stream): 27 | self._verbose = Logger('Parser', verbose_stream) 28 | self._include_keywords = include_keywords 29 | self._db = db 30 | 31 | def xml_to_db(self, xml_file): 32 | self._verbose('- Parsing %s' % xml_file) 33 | test_run = ExecutionResult(xml_file, include_keywords=self._include_keywords) 34 | hash = self._hash(xml_file) 35 | try: 36 | test_run_id = self._db.insert('test_runs', { 37 | 'hash': hash, 38 | 'imported_at': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'), 39 | 'source_file': test_run.source, 40 | 'started_at': self._format_robot_timestamp(test_run.suite.starttime) if test_run.suite.starttime else 'NULL', 41 | 'finished_at': self._format_robot_timestamp(test_run.suite.endtime) if test_run.suite.starttime else 'NULL' 42 | }) 43 | except IntegrityError: 44 | test_run_id = self._db.fetch_id('test_runs', { 45 | 'source_file': test_run.source, 46 | 'started_at': self._format_robot_timestamp(test_run.suite.starttime), 47 | 'finished_at': self._format_robot_timestamp(test_run.suite.endtime) 48 | }) 49 | self._parse_errors(test_run.errors.messages, test_run_id) 50 | self._parse_statistics(test_run.statistics, test_run_id) 51 | self._parse_suite(test_run.suite, test_run_id) 52 | 53 | def _hash(self, xml_file): 54 | block_size = 68157440 55 | hasher = sha1() 56 | with open(xml_file, 'rb') as f: 57 | chunk = f.read(block_size) 58 | while len(chunk) > 0: 59 | hasher.update(chunk) 60 | chunk = f.read(block_size) 61 | return hasher.hexdigest() 62 | 63 | def _parse_errors(self, errors, test_run_id): 64 | self._db.insert_many_or_ignore('test_run_errors', 65 | ('test_run_id', 'level', 'timestamp', 'content'), 66 | [(test_run_id, error.level, self._format_robot_timestamp(error.timestamp), error.message) 67 | for error in errors] 68 | ) 69 | 70 | def _parse_statistics(self, statistics, test_run_id): 71 | self._parse_test_run_statistics(statistics.total, test_run_id) 72 | self._parse_tag_statistics(statistics.tags, test_run_id) 73 | 74 | def _parse_test_run_statistics(self, test_run_statistics, test_run_id): 75 | self._verbose('`--> Parsing test run statistics') 76 | [self._parse_test_run_stats(stat, test_run_id) for stat in test_run_statistics] 77 | 78 | def _parse_tag_statistics(self, tag_statistics, test_run_id): 79 | self._verbose(' `--> Parsing tag statistics') 80 | [self._parse_tag_stats(stat, test_run_id) for stat in tag_statistics.tags.values()] 81 | 82 | def _parse_tag_stats(self, stat, test_run_id): 83 | self._db.insert_or_ignore('tag_status', { 84 | 'test_run_id': test_run_id, 85 | 'name': stat.name, 86 | 'critical': stat.critical, 87 | 'elapsed': getattr(stat, 'elapsed', None), 88 | 'failed': stat.failed, 89 | 'passed': stat.passed 90 | }) 91 | 92 | def _parse_test_run_stats(self, stat, test_run_id): 93 | self._db.insert_or_ignore('test_run_status', { 94 | 'test_run_id': test_run_id, 95 | 'name': stat.name, 96 | 'elapsed': getattr(stat, 'elapsed', None), 97 | 'failed': stat.failed, 98 | 'passed': stat.passed 99 | }) 100 | 101 | def _parse_suite(self, suite, test_run_id, parent_suite_id=None): 102 | self._verbose('`--> Parsing suite: %s' % suite.name) 103 | try: 104 | suite_id = self._db.insert('suites', { 105 | 'suite_id': parent_suite_id, 106 | 'xml_id': suite.id, 107 | 'name': suite.name, 108 | 'source': suite.source, 109 | 'doc': suite.doc 110 | }) 111 | except IntegrityError: 112 | suite_id = self._db.fetch_id('suites', { 113 | 'name': suite.name, 114 | 'source': suite.source 115 | }) 116 | self._parse_suite_status(test_run_id, suite_id, suite) 117 | self._parse_suites(suite, test_run_id, suite_id) 118 | self._parse_tests(suite.tests, test_run_id, suite_id) 119 | self._parse_keywords(suite.keywords, test_run_id, suite_id, None) 120 | 121 | def _parse_suite_status(self, test_run_id, suite_id, suite): 122 | self._db.insert_or_ignore('suite_status', { 123 | 'test_run_id': test_run_id, 124 | 'suite_id': suite_id, 125 | 'passed': suite.statistics.all.passed, 126 | 'failed': suite.statistics.all.failed, 127 | 'elapsed': suite.elapsedtime, 128 | 'status': suite.status 129 | }) 130 | 131 | def _parse_suites(self, suite, test_run_id, parent_suite_id): 132 | [self._parse_suite(subsuite, test_run_id, parent_suite_id) for subsuite in suite.suites] 133 | 134 | def _parse_tests(self, tests, test_run_id, suite_id): 135 | [self._parse_test(test, test_run_id, suite_id) for test in tests] 136 | 137 | def _parse_test(self, test, test_run_id, suite_id): 138 | self._verbose(' `--> Parsing test: %s' % test.name) 139 | try: 140 | test_id = self._db.insert('tests', { 141 | 'suite_id': suite_id, 142 | 'xml_id': test.id, 143 | 'name': test.name, 144 | 'timeout': test.timeout, 145 | 'doc': test.doc 146 | }) 147 | except IntegrityError: 148 | test_id = self._db.fetch_id('tests', { 149 | 'suite_id': suite_id, 150 | 'name': test.name 151 | }) 152 | self._parse_test_status(test_run_id, test_id, test) 153 | self._parse_tags(test.tags, test_id) 154 | self._parse_keywords(test.keywords, test_run_id, None, test_id) 155 | 156 | def _parse_test_status(self, test_run_id, test_id, test): 157 | self._db.insert_or_ignore('test_status', { 158 | 'test_run_id': test_run_id, 159 | 'test_id': test_id, 160 | 'status': test.status, 161 | 'elapsed': test.elapsedtime 162 | }) 163 | 164 | def _parse_tags(self, tags, test_id): 165 | self._db.insert_many_or_ignore('tags', ('test_id', 'content'), 166 | [(test_id, tag) for tag in tags] 167 | ) 168 | 169 | def _parse_keywords(self, keywords, test_run_id, suite_id, test_id, keyword_id=None): 170 | if self._include_keywords: 171 | [self._parse_keyword(keyword, test_run_id, suite_id, test_id, keyword_id) 172 | for keyword in keywords] 173 | 174 | def _parse_keyword(self, keyword, test_run_id, suite_id, test_id, keyword_id): 175 | try: 176 | keyword_id = self._db.insert('keywords', { 177 | 'suite_id': suite_id, 178 | 'test_id': test_id, 179 | 'keyword_id': keyword_id, 180 | 'name': keyword.name, 181 | 'type': keyword.type, 182 | 'timeout': keyword.timeout, 183 | 'doc': keyword.doc 184 | }) 185 | except IntegrityError: 186 | keyword_id = self._db.fetch_id('keywords', { 187 | 'name': keyword.name, 188 | 'type': keyword.type 189 | }) 190 | self._parse_keyword_status(test_run_id, keyword_id, keyword) 191 | self._parse_messages(keyword.messages, keyword_id) 192 | self._parse_arguments(keyword.args, keyword_id) 193 | self._parse_keywords(keyword.keywords, test_run_id, None, None, keyword_id) 194 | 195 | def _parse_keyword_status(self, test_run_id, keyword_id, keyword): 196 | self._db.insert_or_ignore('keyword_status', { 197 | 'test_run_id': test_run_id, 198 | 'keyword_id': keyword_id, 199 | 'status': keyword.status, 200 | 'elapsed': keyword.elapsedtime 201 | }) 202 | 203 | def _parse_messages(self, messages, keyword_id): 204 | self._db.insert_many_or_ignore('messages', ('keyword_id', 'level', 'timestamp', 'content'), 205 | [(keyword_id, message.level, self._format_robot_timestamp(message.timestamp), 206 | message.message) for message in messages] 207 | ) 208 | 209 | def _parse_arguments(self, args, keyword_id): 210 | self._db.insert_many_or_ignore('arguments', ('keyword_id', 'content'), 211 | [(keyword_id, arg) for arg in args] 212 | ) 213 | 214 | def _format_robot_timestamp(self, timestamp): 215 | return datetime.strptime(timestamp, '%Y%m%d %H:%M:%S.%f') 216 | -------------------------------------------------------------------------------- /dbbot/robot_database.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Nokia Solutions and Networks 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import sqlite3 15 | 16 | from .logger import Logger 17 | 18 | 19 | class RobotDatabase(object): 20 | 21 | def __init__(self, db_file_path, verbose_stream): 22 | self._verbose = Logger('Database', verbose_stream) 23 | self._connection = self._connect(db_file_path) 24 | self._configure() 25 | 26 | def _connect(self, db_file_path): 27 | self._verbose('- Establishing database connection') 28 | return sqlite3.connect(db_file_path) 29 | 30 | def _configure(self): 31 | self._set_pragma('page_size', 4096) 32 | self._set_pragma('cache_size', 10000) 33 | self._set_pragma('synchronous', 'NORMAL') 34 | self._set_pragma('journal_mode', 'WAL') 35 | 36 | def _set_pragma(self, name, value): 37 | sql_statement = 'PRAGMA %s=%s' % (name, value) 38 | self._connection.execute(sql_statement) 39 | 40 | def close(self): 41 | self._verbose('- Closing database connection') 42 | self._connection.close() 43 | -------------------------------------------------------------------------------- /dbbot/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2013-2014 Nokia Solutions and Networks 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | import os 16 | import sys 17 | 18 | sys.path.append(os.path.abspath(__file__ + '/../..')) 19 | from dbbot.reader import DatabaseWriter, ReaderOptions, RobotResultsParser 20 | from robot.errors import DataError 21 | 22 | 23 | class DbBot(object): 24 | 25 | def __init__(self): 26 | self._options = ReaderOptions() 27 | verbose_stream = sys.stdout if self._options.be_verbose else None 28 | # '' for temporary database i.e. deleted after the connection is closed 29 | # see: http://www.sqlite.org/inmemorydb.html, section 'Temporary Databases' 30 | database_path = '' if self._options.dry_run else self._options.db_file_path 31 | self._db = DatabaseWriter(database_path, verbose_stream) 32 | self._parser = RobotResultsParser( 33 | self._options.include_keywords, 34 | self._db, 35 | verbose_stream 36 | ) 37 | 38 | def run(self): 39 | try: 40 | for xml_file in self._options.file_paths: 41 | self._parser.xml_to_db(xml_file) 42 | self._db.commit() 43 | except DataError, message: 44 | sys.stderr.write('dbbot: error: Invalid XML: %s\n\n' % message) 45 | exit(1) 46 | finally: 47 | self._db.close() 48 | 49 | 50 | if __name__ == '__main__': 51 | DbBot().run() 52 | -------------------------------------------------------------------------------- /doc/create_database_diagram_from_database_file.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sqlite3 3 | import sys 4 | 5 | from os.path import abspath, basename, dirname, join, splitext 6 | 7 | try: 8 | import pydot 9 | except ImportError: 10 | print '''You need to have pydot and Graphviz installed. 11 | 12 | To install Graphviz: 13 | http://www.graphviz.org/Download.php 14 | 15 | OR: 16 | 17 | Use your OS's package manager. 18 | 19 | To install pydot: 20 | $ pip install pydot 21 | ''' 22 | 23 | DOT_DEFAULTS = { 24 | 'graph_type': 'digraph', 25 | 'compound': 'true', 26 | 'rankdir': 'LR' 27 | } 28 | 29 | GRAPH_DEFAULTS = { 30 | 'fontsize': '12.0', 31 | 'fontname': 'times-bold', 32 | 'style': 'filled', 33 | 'fillcolor': 'azure2' 34 | } 35 | 36 | NODE_DEFAULTS = { 37 | 'shape': 'box', 38 | 'fontsize': '8.0', 39 | 'style': 'filled', 40 | 'fillcolor': 'white' 41 | } 42 | 43 | EDGE_DEFAULTS = { 44 | 'fontsize': '8.0' 45 | } 46 | 47 | def print_usage(): 48 | print '''Usage: python create_database_diagram_from_database_file.py [databasefile] 49 | ''' 50 | 51 | def get_indeces(string): 52 | paren_index = string.find('(') 53 | return (string.rfind(',', 0, paren_index) + 2, 54 | string.find(')', paren_index, len(string)) + 1) 55 | 56 | def handle_constraint_fields(string, fields): 57 | if '(' in string: 58 | start_index, end_index = get_indeces(string) 59 | fields.append(string[start_index:end_index]) 60 | string = string[0:start_index] + string[end_index:] 61 | string, fields = handle_constraint_fields(string, fields) 62 | return string, fields 63 | 64 | def split_foreign_keys_and_rest(string): 65 | fks = [] 66 | string, fields = handle_constraint_fields(string, []) 67 | for token in string.split(', '): 68 | if not token: 69 | continue 70 | if token.find('REFERENCES') == -1: 71 | l = fields 72 | else: 73 | l = fks 74 | l.append(token) 75 | return {'fields': fields, 'fks':fks} 76 | 77 | 78 | def get_schema(db_file): 79 | with sqlite3.connect(db_file) as conn: 80 | c = conn.cursor() 81 | return [row[0] for row in c.execute("SELECT sql FROM sqlite_master WHERE type='table'")] 82 | 83 | def organize_rows(sql_lines): 84 | data = {} 85 | for line in sql_lines: 86 | if line.startswith('CREATE TABLE'): 87 | m = re.match('CREATE TABLE (\w+) \((.*)\)', line) 88 | if m: 89 | data[m.group(1)] = split_foreign_keys_and_rest(m.group(2)) 90 | return data 91 | 92 | def populate_nodes_and_clusters(data, graph): 93 | for table_name, value in data.iteritems(): 94 | node = pydot.Node('\n'.join(value['fields'])) 95 | cluster = pydot.Cluster(table_name, label=table_name) 96 | cluster.add_node(node) 97 | graph.add_subgraph(cluster) 98 | data[table_name]['node'] = node 99 | data[table_name]['cluster'] = cluster 100 | 101 | def populate_edges(data, graph): 102 | for table_data in data.itervalues(): 103 | for fk in table_data['fks']: 104 | target = fk.split(' REFERENCES ')[1] 105 | target_name = fk.split()[0] 106 | graph.add_edge(pydot.Edge(table_data['node'], 107 | data[target]['node'], 108 | label=target_name, 109 | ltail=table_data['cluster'].get_name(), 110 | lhead=data[target]['cluster'].get_name())) 111 | 112 | def create_graph(data): 113 | graph = pydot.Dot(**DOT_DEFAULTS) 114 | graph.set_graph_defaults(**GRAPH_DEFAULTS) 115 | graph.set_node_defaults(**NODE_DEFAULTS) 116 | graph.set_edge_defaults(**EDGE_DEFAULTS) 117 | populate_nodes_and_clusters(data, graph) 118 | populate_edges(data, graph) 119 | return graph 120 | 121 | if __name__ == '__main__': 122 | if len(sys.argv) < 2: 123 | print_usage() 124 | sys.exit(1) 125 | file_name = sys.argv[1] 126 | data = organize_rows(get_schema(file_name)) 127 | file_name = splitext(basename(file_name))[0] 128 | graph = create_graph(data) 129 | graph.write_png('%s.png' % file_name) 130 | print 'DONE' 131 | 132 | 133 | -------------------------------------------------------------------------------- /doc/robot_database.md: -------------------------------------------------------------------------------- 1 | Robot Database Schema Description 2 | ================================= 3 | 4 | Notes: 5 | 6 | * Types are SQLite3-combatible datatypes (http://www.sqlite.org/datatype3.html) 7 | 8 | * All DATETIMEs are saved in format %Y-%m-%d %H:%M:%S.%f with %f being number of 9 | microseconds (micro: 10e-6) having length of 6 e.g: 10 | 2013-04-23 12:35:18.730000 11 | 12 | 13 | test_runs 14 | --------- 15 | 16 | column | type | not null | description 17 | ------------|----------|----------|------------ 18 | id | INTEGER | X | primary key 19 | source_file | TEXT | | absolute path to the original output.xml file 20 | started_at | DATETIME | | when was the root suite started at 21 | finished_at | DATETIME | | when was the root suite finished at 22 | imported_at | DATETIME | X | when was the output.xml serialized into database 23 | hash | TEXT | X | a SHA1 hash of the source file 24 | 25 | Row is unique if the combination of following is unique: 26 | hash 27 | 28 | test_run_status 29 | --------------- 30 | 31 | column | type | not null | description 32 | ------------|----------|----------|------------ 33 | id | INTEGER | X | primary key 34 | test_run_id | INTEGER | X | FOREIGN KEY to test_runs 35 | name | TEXT | X | 'total' or 'critical' 36 | elapsed | INTEGER | | number of milliseconds took to run 37 | failed | INTEGER | X | number of tests failed 38 | passed | INTEGER | X | number of tests passed 39 | 40 | Row is unique if the combination of following is unique: 41 | test_run_id, name 42 | 43 | 44 | test_run_errors 45 | --------------- 46 | 47 | column | type | not null | description 48 | ------------|----------|----------|------------ 49 | id | INTEGER | X | primary key 50 | test_run_id | INTEGER | X | FOREIGN KEY to test_runs 51 | level | TEXT | X | one of the following: TRACE/DEBUG/INFO/WARN/ERROR/FAIL 52 | timestamp | DATETIME | X | timestamp of the error 53 | content | TEXT | X | the actual error message 54 | 55 | Row is unique if the combination of following is unique: 56 | test_run_id, level, content 57 | 58 | 59 | tag_status 60 | --------------- 61 | 62 | column | type | not null | description 63 | ------------|----------|----------|------------ 64 | id | INTEGER | X | primary key 65 | test_run_id | INTEGER | X | FOREIGN KEY to test_runs 66 | name | TEXT | X | name of the tag 67 | critical | INTEGER | X | 0 if not critical, 1 if critical (SQLite has no booleans) 68 | elapsed | INTEGER | | number of milliseconds took to run 69 | failed | INTEGER | X | number of tests failed 70 | passed | INTEGER | X | number of tests passed 71 | 72 | Row is unique if the combination of following is unique: 73 | test_run_id, name 74 | 75 | 76 | suites 77 | ------ 78 | 79 | column | type | not null | description 80 | ------------|----------|----------|------------ 81 | id | INTEGER | X | primary key 82 | suite_id | INTEGER | | FOREIGN KEY to parent suite if has one 83 | xml_id | TEXT | X | suite id attribute in the xml file, e.g. 's1' or 's1-s1' 84 | name | TEXT | | full name of the suite 85 | source | TEXT | | absolute path to the suite ran 86 | doc | TEXT | X | optional suite documentation, otherwise '' 87 | 88 | A row is unique if the combination of following is unique: 89 | name, source 90 | 91 | 92 | suite_status 93 | ------------ 94 | 95 | column | type | not null | description 96 | ------------|----------|----------|------------ 97 | id | INTEGER | X | primary key 98 | test_run_id | INTEGER | X | FOREIGN KEY to the test_run 99 | suite_id | INTEGER | X | FOREIGN KEY to the suite 100 | elapsed | INTEGER | X | number of milliseconds took to run 101 | failed | INTEGER | X | number of tests failed 102 | passed | INTEGER | X | number of tests passed 103 | status | TEXT | X | either 'PASS' or 'FAIL' 104 | 105 | A row is unique if the combination of following is unique: 106 | test_run_id, suite_id 107 | 108 | 109 | tests 110 | ----- 111 | 112 | column | type | not null | description 113 | ------------|----------|----------|------------ 114 | id | INTEGER | X | primary key 115 | suite_id | INTEGER | X | FOREIGN KEY to the suite 116 | xml_id | TEXT | X | test id attribute in the xml file, e.g. 's1-t1' or 's1-s1-t1' 117 | name | TEXT | X | full name of the test 118 | timeout | TEXT | | '' by default 119 | doc | TEXT | | optional test documentation, otherwise '' 120 | 121 | A row is unique if the combination of following is unique: 122 | suite_id, name 123 | 124 | 125 | test_status 126 | ----------- 127 | 128 | column | type | not null | description 129 | ------------|----------|----------|------------ 130 | id | INTEGER | X | primary key 131 | test_run_id | INTEGER | X | FOREIGN KEY to the test run 132 | test_id | INTEGER | X | FOREIGN KEY to the test 133 | status | TEXT | X | either 'PASS' or 'FAIL' 134 | elapsed | INTEGER | X | number of milliseconds took to run 135 | 136 | A row is unique if the combination of following is unique: 137 | test_run_id, test_id 138 | 139 | 140 | keywords 141 | -------- 142 | 143 | column | type | not null | description 144 | ------------|----------|----------|------------ 145 | id | INTEGER | X | primary key 146 | suite_id | INTEGER | | FOREIGN KEY to the suite if is suite keyword 147 | test_id | INTEGER | | FOREIGN KEY to the test if is test keyword 148 | keyword_id | INTEGER | | FOREIGN KEY to the parent keyword if is sub-keyword 149 | name | TEXT | X | full name of the keyword 150 | type | TEXT | X | usually '', either 'setup' or 'teardown' for suite keywords 151 | timeout | TEXT | | '' by default 152 | doc | TEXT | | optional keyword documentation, otherwise '' 153 | 154 | A row is unique if the combination of following is unique: 155 | name, type 156 | 157 | 158 | keyword_status 159 | -------------- 160 | 161 | column | type | not null | description 162 | ------------|----------|----------|------------ 163 | id | INTEGER | X | primary key 164 | test_run_id | INTEGER | X | FOREIGN KEY to the test run 165 | keyword_id | INTEGER | X | FOREIGN KEY to the keyword 166 | status | TEXT | X | either 'PASS' or 'FAIL' 167 | elapsed | INTEGER | X | number of milliseconds keyword took to run 168 | 169 | A row is unique has no unique constraints. 170 | 171 | messages 172 | -------------- 173 | 174 | column | type | not null | description 175 | ------------|----------|----------|------------ 176 | id | INTEGER | X | primary key 177 | keyword_id | INTEGER | X | FOREIGN KEY to the keyword 178 | level | TEXT | X | one of the following: TRACE/DEBUG/INFO/WARN/ERROR/FAIL 179 | timestamp | DATETIME | X | timestamp of the message 180 | content | TEXT | X | textual content of the message 181 | 182 | A row is unique if the combination of following is unique: 183 | keyword_id, level, content 184 | 185 | 186 | tags 187 | ---- 188 | 189 | column | type | not null | description 190 | ------------|----------|----------|------------ 191 | id | INTEGER | X | primary key 192 | test_id | INTEGER | X | FOREIGN KEY to the test 193 | content | TEXT | X | name of the tag 194 | 195 | A row is unique if the combination of following is unique: 196 | test_id, content 197 | 198 | 199 | arguments 200 | --------- 201 | 202 | column | type | not null | description 203 | ------------|----------|----------|------------ 204 | id | INTEGER | X | primary key 205 | keyword_id | INTEGER | X | FOREIGN KEY to the keyword 206 | content | TEXT | X | textual content 207 | 208 | A row is unique if the combination of following is unique: 209 | keyword_id, content 210 | -------------------------------------------------------------------------------- /doc/robot_database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/DbBot/542d803b14075f2c472853e69102d4cf67b011de/doc/robot_database.png -------------------------------------------------------------------------------- /examples/failbot/README.md: -------------------------------------------------------------------------------- 1 | FailBot 2 | ======= 3 | 4 | FailBot is a Python script used to produce a summary web page about the failing 5 | suites, tests and keywords, using the information stored in a DbBot database. 6 | 7 | Please adjust the (barebone) HTML pages in directory 'templates' to your needs. 8 | 9 | 10 | Requirements 11 | ------------ 12 | * Python 2.6 or newer installed 13 | * DbBot 14 | * DbBot produced database, e.g. robot_results.db 15 | 16 | 17 | Setup 18 | ----- 19 | 20 | Please make sure you have DbBot installed somewhere and it's root is in your 21 | PYTHONPATH. 22 | 23 | With Bash: 24 | 25 | export PYTHONPATH=$PYTHONPATH:/path/to/DbBot 26 | 27 | You may also want to add this line to your .bash_profile to avoid running 28 | the command in every new shell. 29 | 30 | On Windows: 31 | 32 | set PYTHONPATH=%PYTHONPATH%;C:\path\to\DbBot 33 | 34 | 35 | Usage 36 | ----- 37 | The executable is 'failbot' in directory 'bin'. Run it from command-line: 38 | 39 | bin/failbot [options] 40 | 41 | Required options are: 42 | 43 | Short format | Long format | Description 44 | --------------- |-------------------------| ------------------------------------------ 45 | -o | --output | Output HTML file name 46 | 47 | Additional options are: 48 | 49 | Short format | Long format | Description 50 | --------------- |-------------------------| ------------------------------------------ 51 | -v | --verbose | Be verbose about the operation 52 | -b DB_FILE_PATH | --database=DB_FILE_PATH | DbBot database having the test run results (robot_results.db by default) 53 | 54 | On Windows environments, you might need to rename the executable to have the 55 | '.py' file extension ('bin/failbot' -> 'bin/failbot.py'). 56 | 57 | 58 | Usage examples 59 | -------------- 60 | 61 | The output HTML filename is always required: 62 | 63 | failbot -o index.html 64 | 65 | You might want to output the html file directly into your public_html: 66 | 67 | failbot -o /home//public_html/index.html 68 | 69 | If -b/--database is not given, file 'robot_results.db' is used by default. 70 | 71 | With a non-default named database: 72 | 73 | failbot -f atest/testdata/one_suite/output.xml -b path/to/my_own_database.db 74 | 75 | 76 | Directory structure 77 | ------------------- 78 | 79 | Directory | Description 80 | ----------|------------ 81 | bin | Contains the executable. You may want to append this to your PATH. 82 | templates | HTML templates used to produced the result HTML page. 83 | failbot | Contains the modules used by FailBot. 84 | -------------------------------------------------------------------------------- /examples/failbot/bin/failbot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from os.path import abspath, dirname, join 5 | 6 | 7 | 8 | try: 9 | import dbbot 10 | except ImportError, message: 11 | sys.exit('Please make sure you have DbBot installed.\n\n' 12 | 'See more: https://pypi.python.org/pypi/dbbot') 13 | 14 | sys.path.insert(0, abspath(join(dirname(abspath(__file__)), '..'))) 15 | from failbot import DatabaseReader, HtmlWriter, WriterOptions 16 | 17 | class FailBot(object): 18 | 19 | def __init__(self): 20 | self._options = WriterOptions() 21 | verbose_stream = sys.stdout if self._options.be_verbose else None 22 | self._db = DatabaseReader(self._options.db_file_path, verbose_stream) 23 | self._writer = HtmlWriter( 24 | self._db, 25 | self._options.output_file_path, 26 | verbose_stream 27 | ) 28 | 29 | def run(self): 30 | try: 31 | self._writer.produce() 32 | finally: 33 | self._db.close() 34 | 35 | 36 | if __name__ == '__main__': 37 | FailBot().run() 38 | -------------------------------------------------------------------------------- /examples/failbot/failbot/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | # default dbbot root 5 | sys.path.append(os.path.abspath(__file__ + '/../../../../')) 6 | 7 | from .database_reader import DatabaseReader 8 | from .writer_options import WriterOptions 9 | from .html_writer import HtmlWriter 10 | -------------------------------------------------------------------------------- /examples/failbot/failbot/database_reader.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | from dbbot import RobotDatabase 4 | 5 | 6 | class DatabaseReader(RobotDatabase): 7 | 8 | def __init__(self, db_file_path, verbose_stream): 9 | super(DatabaseReader, self).__init__(db_file_path, verbose_stream) 10 | self._connection.row_factory = sqlite3.Row 11 | 12 | def most_failed_suites(self): 13 | sql_statement = ''' 14 | SELECT count() as count, suites.id, suites.name, suites.source 15 | FROM suites, suite_status 16 | WHERE suites.id == suite_status.suite_id AND 17 | suite_status.status == "FAIL" 18 | GROUP BY suites.source 19 | ''' 20 | return self._fetch_by(sql_statement) 21 | 22 | def most_failed_tests(self): 23 | sql_statement = ''' 24 | SELECT count() as count, tests.id, tests.name, tests.suite_id 25 | FROM tests, test_status 26 | WHERE tests.id == test_status.test_id AND 27 | test_status.status == "FAIL" 28 | GROUP BY tests.name, tests.suite_id 29 | ''' 30 | return self._fetch_by(sql_statement) 31 | 32 | def most_failed_keywords(self): 33 | sql_statement = ''' 34 | SELECT count() as count, keywords.name, keywords.type 35 | FROM keywords, keyword_status 36 | WHERE keywords.id == keyword_status.keyword_id AND 37 | keyword_status.status == "FAIL" 38 | GROUP BY keywords.name, keywords.type 39 | ''' 40 | return self._fetch_by(sql_statement) 41 | 42 | def failed_tests_for_suite(self, suite_id): 43 | sql_statement = ''' 44 | SELECT count() as count, tests.id, tests.name, tests.suite_id 45 | FROM tests, test_status 46 | WHERE tests.id == test_status.test_id AND 47 | tests.suite_id == ? AND 48 | test_status.status == "FAIL" 49 | GROUP BY tests.name 50 | ''' 51 | return self._fetch_by(sql_statement, [suite_id]) 52 | 53 | def failed_keywords_for_test(self, test_id): 54 | sql_statement = ''' 55 | SELECT count() as count, keywords.name, keywords.type 56 | FROM keywords, keyword_status 57 | WHERE keywords.id == keyword_status.keyword_id AND 58 | keywords.test_id == ? AND 59 | keyword_status.status == "FAIL" 60 | GROUP BY keywords.name, keywords.type 61 | ''' 62 | return self._fetch_by(sql_statement, [test_id]) 63 | 64 | def _fetch_by(self, sql_statement, values=[]): 65 | return self._connection.execute(sql_statement, values).fetchall() 66 | 67 | -------------------------------------------------------------------------------- /examples/failbot/failbot/html_writer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from string import Template 3 | from xml.sax.saxutils import escape 4 | 5 | from dbbot import Logger 6 | 7 | 8 | class HtmlWriter(object): 9 | template_path = os.path.abspath(__file__ + '../../../templates') 10 | 11 | # escape() only takes care of &, < and > 12 | additional_html_escapes = { 13 | '"': """, 14 | "'": "'" 15 | } 16 | 17 | def __init__(self, db, output_file_path, verbose_stream): 18 | self._verbose = Logger('HTML', verbose_stream) 19 | self._db = db 20 | self._output_file_path = output_file_path 21 | self._init_layouts() 22 | 23 | def _init_layouts(self): 24 | self._verbose('- Loading HTML templates') 25 | self._full_layout = self._read_template('layout.html') 26 | self._table_layout = self._read_template('table.html') 27 | self._row_layout = self._read_template('row.html') 28 | 29 | def _read_template(self, filename): 30 | with open(os.path.join(self.template_path, filename), 'r') as file: 31 | content = file.read() 32 | return Template(content) 33 | 34 | def produce(self): 35 | self._verbose('- Producing summaries from database') 36 | output_html = self._full_layout.substitute({ 37 | 'most_failed_suites': self._table_of_most_failed_suites(), 38 | 'most_failed_tests': self._table_of_most_failed_tests(), 39 | 'most_failed_keywords': self._table_of_most_failed_keywords() 40 | }) 41 | self._write_file(self._output_file_path, output_html) 42 | 43 | def _write_file(self, filename, content): 44 | self._verbose('- Writing %s' % filename) 45 | with open(filename, 'w') as file: 46 | file.write(content) 47 | 48 | def _table_of_most_failed_suites(self): 49 | return self._format_table(self._db.most_failed_suites()) 50 | 51 | def _table_of_most_failed_tests(self): 52 | return self._format_table(self._db.most_failed_tests()) 53 | 54 | def _table_of_most_failed_keywords(self): 55 | return self._format_table(self._db.most_failed_keywords()) 56 | 57 | def _format_table(self, rows): 58 | return self._table_layout.substitute({ 59 | 'rows': ''.join([self._format_row(row) for row in rows]) 60 | }) 61 | 62 | def _format_row(self, item): 63 | return self._row_layout.substitute({ 64 | 'name': self._escape(item['name']), 65 | 'count': item['count'] 66 | }) 67 | 68 | def _escape(self, text): 69 | return escape(text, self.additional_html_escapes) 70 | -------------------------------------------------------------------------------- /examples/failbot/failbot/writer_options.py: -------------------------------------------------------------------------------- 1 | from os.path import exists 2 | from sys import argv 3 | 4 | from dbbot.reader.reader_options import ReaderOptions 5 | 6 | 7 | class WriterOptions(ReaderOptions): 8 | 9 | @property 10 | def output_file_path(self): 11 | return self._target_file 12 | 13 | def _get_validated_options(self): 14 | self._parser.set_usage('%prog [options] outfile') 15 | if len(argv) < 2: 16 | self._exit_with_help() 17 | options, target_files = super(WriterOptions, self)._get_validated_options() 18 | self._target_file = target_files.pop() 19 | if not exists(options.db_file_path): 20 | self._parser.error('database "%s" does not exists' % options.db_file_path) 21 | return options, self._target_file 22 | 23 | def _check_files(self, files): 24 | if not files: 25 | self._parser.error('output file not given') 26 | for path in files: 27 | open(path, 'a').close() -------------------------------------------------------------------------------- /examples/failbot/templates/layout.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Test run results 7 | 8 | 9 |

Failed Suites

10 | $most_failed_suites 11 |

Failed Tests

12 | $most_failed_tests 13 |

Failed Keywords

14 | $most_failed_keywords 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/failbot/templates/row.html: -------------------------------------------------------------------------------- 1 | 2 | $name 3 | $count 4 | 5 | -------------------------------------------------------------------------------- /examples/failbot/templates/table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | $rows 7 |
NameTimes
8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | from os.path import abspath, dirname, join 5 | import re 6 | 7 | NAME = 'dbbot' 8 | CLASSIFIERS = """ 9 | Development Status :: 4 - Beta 10 | License :: OSI Approved :: Apache Software License 11 | Operating System :: OS Independent 12 | Programming Language :: Python 13 | Topic :: Software Development :: Testing 14 | """.strip().splitlines() 15 | CURDIR = dirname(abspath(__file__)) 16 | with open(join(CURDIR, NAME, '__init__.py')) as f: 17 | VERSION = re.search("\n__version__ = '(.*)'\n", f.read()).group(1) 18 | with open(join(CURDIR, 'README.rst')) as f: 19 | README = f.read() 20 | 21 | setup( 22 | name = NAME, 23 | version = VERSION, 24 | author = 'Robot Framework Developers', 25 | author_email = 'robotframework@gmail.com', 26 | url = 'https://github.com/robotframework/DbBot', 27 | download_url = 'https://pypi.python.org/pypi/dbbot', 28 | license = 'Apache License 2.0', 29 | description = 'A tool for serializing Robot Framework test run ' 30 | 'results into an sqlite3 database.', 31 | long_description = README, 32 | keywords = 'robotframework testing testautomation atdd', 33 | platforms = 'any', 34 | classifiers = CLASSIFIERS, 35 | packages = ['dbbot', 'dbbot.reader'], 36 | install_requires = ['robotframework'] 37 | ) 38 | -------------------------------------------------------------------------------- /tools/migrate27to28: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2013-2014 Nokia Solutions and Networks 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import sys 18 | 19 | sys.path.append(os.path.abspath(__file__ + '/../..')) 20 | 21 | from dbbot import CommandLineOptions 22 | from dbbot.reader import DatabaseWriter 23 | 24 | 25 | class MigratorOptions(CommandLineOptions): 26 | 27 | def _get_validated_options(self): 28 | if len(sys.argv) < 2: 29 | self._exit_with_help() 30 | options = super(MigratorOptions, self)._get_validated_options() 31 | if not os.path.exists(options.db_file_path): 32 | self._parser.error('database %s not exists' % options.db_file_path) 33 | return options 34 | 35 | 36 | class Migrate27to28(object): 37 | 38 | def __init__(self): 39 | self._options = MigratorOptions() 40 | verbose_stream = sys.stdout if self._options.be_verbose else None 41 | self._db = DatabaseWriter(self._options.db_file_path, verbose_stream) 42 | 43 | def run(self): 44 | try: 45 | self._run_migrations() 46 | self._db.commit() 47 | finally: 48 | self._db.close() 49 | 50 | def _run_migrations(self): 51 | self._migrate_test_runs() 52 | self._migrate_tests() 53 | self._migrate_keywords() 54 | 55 | def _migrate_test_runs(self): 56 | old_table_name = 'old_27_test_runs' 57 | self._db.rename_table('test_runs', old_table_name) 58 | self._db._create_table_test_runs() 59 | self._db.copy_table(old_table_name, 'test_runs', ( 60 | 'source_file', 61 | 'started_at', 62 | 'finished_at', 63 | 'imported_at' 64 | )) 65 | self._db.drop_table(old_table_name) 66 | 67 | def _migrate_tests(self): 68 | old_table_name = 'old_27_tests' 69 | self._db.rename_table('tests', old_table_name) 70 | self._db._create_table_tests() 71 | self._db.copy_table(old_table_name, 'tests', ( 72 | 'suite_id', 73 | 'xml_id', 74 | 'name', 75 | 'timeout', 76 | 'doc' 77 | )) 78 | self._db.drop_table(old_table_name) 79 | 80 | def _migrate_keywords(self): 81 | old_table_name = 'old_27_keywords' 82 | self._db.rename_table('keywords', old_table_name) 83 | self._db._create_table_keywords() 84 | self._db.copy_table(old_table_name, 'keywords', ( 85 | 'suite_id', 86 | 'test_id', 87 | 'keyword_id', 88 | 'name', 89 | 'type', 90 | 'timeout', 91 | 'doc' 92 | )) 93 | self._db.drop_table(old_table_name) 94 | 95 | 96 | if __name__ == '__main__': 97 | Migrate27to28().run() 98 | --------------------------------------------------------------------------------