diff --git a/.github/workflows/common_tests.yml b/.github/workflows/common_tests.yml new file mode 100644 index 00000000..b6729df8 --- /dev/null +++ b/.github/workflows/common_tests.yml @@ -0,0 +1,177 @@ +name: Common DB Tests + +on: [push, pull_request] + +env: + DB_NAME: db + DB_USER: db_user + DB_PASS: pass + DB_HOST: 127.0.0.1 + # port is set in the job + + # options for pyodbc only + DB_CHARSET: utf8mb4 + DB_DRIVER: "{MySQL ODBC 9.2 ANSI Driver}" + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - job_name: PostgreSQL + module_mode: standard + py_db_module: psycopg2 + pip_install: psycopg2 + db_port: 5432 + - job_name: oracledb + module_mode: standard + py_db_module: oracledb + pip_install: oracledb + db_port: 1521 + - job_name: SQLite + module_mode: custom + py_db_module: sqlite3 + pip_install: none + db_port: 0000 + - job_name: MySQL_pymysql + module_mode: standard + py_db_module: pymysql + pip_install: pymysql[rsa] + db_port: 3306 + - job_name: MySQL_pyodbc + module_mode: standard + py_db_module: pyodbc + pip_install: pyodbc + db_port: 3306 + + services: + postgres: + image: postgres:11 + env: + POSTGRES_DB: ${{ env.DB_NAME }} + POSTGRES_USER: ${{ env.DB_USER }} + POSTGRES_PASSWORD: ${{ env.DB_PASS }} + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + oracle: + image: gvenzl/oracle-free:latest + env: + ORACLE_PASSWORD: ${{ env.DB_PASS }} + ORACLE_DATABASE: ${{ env.DB_NAME }} + APP_USER: ${{ env.DB_USER }} + APP_USER_PASSWORD: ${{ env.DB_PASS }} + ports: + - 1521:1521 + # Provide healthcheck script options for startup + options: --health-cmd healthcheck.sh --health-interval 10s --health-timeout 5s --health-retries 10 + + mysql: + image: mysql + env: + MYSQL_ROOT_PASSWORD: ${{ env.DB_PASS }} + MYSQL_DATABASE: ${{ env.DB_NAME }} + MYSQL_USER: ${{ env.DB_USER }} + MYSQL_PASSWORD: ${{ env.DB_PASS }} + ports: + - 3306:3306 + + steps: + + - name: Install unixodbc + if: matrix.py_db_module == 'pyodbc' + run: sudo apt-get update && sudo apt-get install -y unixodbc + + - name: Install ODBC driver for PostgreSQL + if: matrix.py_db_module == 'pyodbc' + run: | + echo "*** apt-get install the driver" + sudo apt-get install --yes odbc-postgresql + echo '*** ls -l /usr/lib/x86_64-linux-gnu/odbc' + ls -l /usr/lib/x86_64-linux-gnu/odbc || true + echo '*** add full paths to Postgres .so files in /etc/odbcinst.ini' + sudo sed -i 's|Driver=psqlodbca.so|Driver=/usr/lib/x86_64-linux-gnu/odbc/psqlodbca.so|g' /etc/odbcinst.ini + sudo sed -i 's|Driver=psqlodbcw.so|Driver=/usr/lib/x86_64-linux-gnu/odbc/psqlodbcw.so|g' /etc/odbcinst.ini + sudo sed -i 's|Setup=libodbcpsqlS.so|Setup=/usr/lib/x86_64-linux-gnu/odbc/libodbcpsqlS.so|g' /etc/odbcinst.ini + + - name: Install ODBC driver for MySQL + if: matrix.py_db_module == 'pyodbc' + run: | + cd "$RUNNER_TEMP" + echo "*** download driver" + curl --silent --show-error --write-out "$CURL_OUTPUT_FORMAT" -O -L "https://dev.mysql.com/get/Downloads/Connector-ODBC/9.2/${MYSQL_DRIVER}" + ls -l "${MYSQL_DRIVER}" + echo "*** install the driver" + sudo dpkg -i "./${MYSQL_DRIVER}" + sudo apt-get install -f + env: + CURL_OUTPUT_FORMAT: '%{http_code} %{filename_effective} %{size_download} %{time_total}\n' + MYSQL_DRIVER: mysql-connector-odbc_9.2.0-1ubuntu24.04_amd64.deb + + - name: Check ODBC setup + if: matrix.py_db_module == 'pyodbc' + run: | + echo "*** odbcinst -j" + odbcinst -j + echo "*** cat /etc/odbcinst.ini" + cat /etc/odbcinst.ini + echo "*** cat /etc/odbc.ini" + cat /etc/odbc.ini + echo '*** ls -l /opt/microsoft/msodbcsql17/lib64' + ls -l /opt/microsoft/msodbcsql17/lib64 || true + echo '*** ls -l /usr/lib/x86_64-linux-gnu/odbc' + ls -l /usr/lib/x86_64-linux-gnu/odbc || true + + - name: Check out repository code + uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.8.14' + + - name: Setup Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install Development/Checked out version of DatabaseLibrary + run: | + pip install -e ${{ github.workspace }} + + - name: Setup Python DB module + if: matrix.pip_install != 'none' + + run: | + pip install ${{ matrix.pip_install }} + + - name: Tests for ${{ matrix.job_name }} + working-directory: ./test + run: >- + robot + -d results + --xunit result.xml + --loglevel DEBUG:INFO + --output output_${{ matrix.job_name }}.xml + --log log_${{ matrix.job_name }}.html + --report report_${{ matrix.job_name }}.html + -v DB_MODULE_MODE:${{ matrix.module_mode }} + -v DB_MODULE:${{ matrix.py_db_module }} + -v DB_NAME:${{ env.DB_NAME }} + -v DB_USER:${{ env.DB_USER }} + -v DB_PASS:${{ env.DB_PASS }} + -v DB_HOST:${{ env.DB_HOST }} + -v DB_PORT:${{ matrix.db_port }} + -v DB_CHARSET:${{env.DB_CHARSET}} + -v DB_DRIVER:"${{env.DB_DRIVER}}" + tests/common_tests + + - name: Upload Robot Logs + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: log-files-${{ matrix.job_name }} + path: ./test/results/ diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 00000000..a9d347e8 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'doc' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 00000000..b036e639 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,56 @@ +--- +# This workflow will install Python dependencies +# and run unit tests for given OSes + +name: Unit tests + +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: 'ubuntu-latest' + python-version: '3.8' + rf-version: '5.0.1' + - os: 'ubuntu-latest' + python-version: '3.9' + rf-version: '5.0.1' + - os: 'ubuntu-latest' + python-version: '3.10' + rf-version: '6.1.1' + - os: 'ubuntu-latest' + python-version: '3.11' + rf-version: '6.1.1' + - os: 'ubuntu-latest' + python-version: '3.12' + rf-version: '7.0.1' + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install robotframework==${{ matrix.rf-version }} coverage pytest + pip install . + + - name: Run unit tests with coverage + run: + coverage run -m pytest + + - name: Codecov + uses: codecov/codecov-action@v3 + with: + name: ${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.rf-version }} diff --git a/.gitignore b/.gitignore index 68d5d70f..b00d1b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,16 @@ +dist/ build/ +*.egg-info/ *.pyc .idea .py*/ -*.egg-info/ -test/log.html -test/my_db_test.db -test/output.xml -test/report.html -test/logs/ +**/my_db_test.db +logs +interactive_console_output.xml +log.html +output.xml +report.html +venv +.runNumber +.DS_Store +test/resources/ojdbc17.jar diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..be00c519 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) + +- repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black diff --git a/.project b/.project deleted file mode 100644 index 2e6cf0a3..00000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - robotframework-database-library - - - - - - org.python.pydev.PyDevBuilder - - - - - - org.python.pydev.pythonNature - - diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index a090aef5..00000000 --- a/.pydevproject +++ /dev/null @@ -1,10 +0,0 @@ - - - - -/opt/python-virtualenvs/robot/bin/python -python 2.6 - -/robotframework-database-library/src - - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7c841588..00000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: python -python: - - 2.7 - - 3.5 -sudo: false -env: - - DB=SQLite - - DB=Postgres - - DB=MySQL -matrix: - allow_failures: - - env: DB=MySQL -services: - - postgresql - - mysql -install: - - pip install -r requirements.txt - - python setup.py develop - - pip install -q flake8 -before_script: - - psql -c 'create database travis_ci_test;' -U postgres - - mysql -e 'create database my_db_test;' - - mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1';" -uroot -script: -# - flake8 src/ - - if [ $DB == 'SQLite' ]; then (cd test/ && pybot SQLite3_DB_Tests.robot); fi - - if [ $DB == 'Postgres' ]; then (cd test/ && pybot PostgreSQL_DB_Tests.robot); fi - - if [ $DB == 'MySQL' ]; then (cd test/ && pybot MySQL_DB_Tests.robot); fi diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..eeb930b9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.pylint", + "robocorp.robotframework-lsp", + "techer.open-in-browser", + "eamodio.gitlens" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..d9400425 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,104 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "robotframework-lsp", + "name": "Launch .robot file for DB", + "request": "launch", + "cwd": "${workspaceFolder}", + "target": "${file}", + "terminal": "integrated", + "args": [ + "--outputdir", + "logs", + "--loglevel", + "DEBUG:INFO", + "-v GLOBAL_DB_SELECTOR:${input:DB}", + ], + }, + { + "type": "robotframework-lsp", + "name": "Run all common tests for DB", + "request": "launch", + "cwd": "${workspaceFolder}", + "target": "${workspaceFolder}/test/tests/common_tests", + "terminal": "integrated", + "args": [ + "--outputdir", + "logs", + "--loglevel", + "DEBUG:INFO", + "-v GLOBAL_DB_SELECTOR:${input:DB}", + ], + }, + { + "type": "robotframework-lsp", + "name": "dryrun all tests", + "request": "launch", + "cwd": "${workspaceFolder}", + "target": "${workspaceFolder}/test", + "terminal": "integrated", + "args": [ + "--outputdir", + "logs", + "--loglevel", + "DEBUG:INFO", + "--dryrun" + ], + }, + { + "type": "robotframework-lsp", + "name": "dryrun .robot file", + "request": "launch", + "cwd": "${workspaceFolder}", + "target": "${file}", + "terminal": "integrated", + "args": [ + "--outputdir", + "logs", + "--loglevel", + "DEBUG:INFO", + "--dryrun" + ], + }, + { + "type": "robotframework-lsp", + "name": "Robot Framework: Launch template", + "request": "launch", + "cwd": "^\"\\${workspaceFolder}\"", + "target": "^\"\\${file}\"", + "terminal": "integrated", + "args": [ + "--outputdir", + "logs", + "--loglevel", + "DEBUG:INFO", + ], + }, + ], + "inputs": [ + { + "type": "pickString", + "id": "DB", + "description": "Database to run the tests for", + "options": [ + "PostgreSQL", + "oracledb", + "cx_Oracle", + "SQLite", + "IBM_DB2", + "Teradata", + "MySQL_pymysql", + "MySQL_pyodbc", + "Oracle_JDBC", + "MSSQL", + "Excel", + "Excel_RW", + ], + "default": "PostgreSQL" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..3bae5617 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "files.exclude": { + "**/.git": true, + "**/__pycache__": true + }, + "robot.lint.robocop.enabled": true, + "robot.lint.unusedKeyword":false, + "robot.interactiveConsole.arguments": [ + "--output", "${workspaceRoot}/logs/interactive_console.xml" + ], + "python.analysis.completeFunctionParens": true, + "cSpell.words": [ + "hana", + "hdbodbc", + "saphana", + "SERVERNODE" + ], +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..476b056c --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,35 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "Run Oracle DB in Docker", + "command": "docker run --rm --name oracle -d -p 1521:1521 -e ORACLE_PASSWORD=pass -e ORACLE_DATABASE=db -e APP_USER=db_user -e APP_USER_PASSWORD=pass gvenzl/oracle-free", + "problemMatcher": [] + }, + { + "type": "shell", + "label": "Run PostreSQL DB in Docker", + "command": "docker run --rm --name postgres -e POSTGRES_USER=db_user -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=db -p 5432:5432 -d postgres", + "problemMatcher": [] + }, + { + "type": "shell", + "label": "Run IBM DB2 in Docker", + "command": "docker run --rm -itd --name mydb2 --privileged=true -p 50000:50000 -e LICENSE=accept -e DB2INSTANCE=db_user -e DB2INST1_PASSWORD=pass -e DBNAME=db ibmcom/db2", + "problemMatcher": [] + }, + { + "type": "shell", + "label": "Run MySQL in Docker", + "command": "docker run --rm --name mysql -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_USER=db_user -e MYSQL_PASSWORD=pass -p 3306:3306 -d mysql", + "problemMatcher": [] + }, + { + "type": "shell", + "label": "Run MS SQL in Docker (don't forget the DB init!)", + "command": "docker run --rm --name mssql -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=MyPass1234! -p 1433:1433 -d mcr.microsoft.com/mssql/server", + "problemMatcher": [] + }, + ] + } \ No newline at end of file diff --git a/README.md b/README.md index 38891206..69819108 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,465 @@ -Robotframework-Database-Library -=============================== +# Robot Framework Database Library -Database Library contains utilities meant for Robot Framework's usage. This can allow you to query your database after an action has been made to verify the results. This is compatible* with any Database API Specification 2.0 module. +The Database Library for [Robot Framework](https://robotframework.org) allows you to query a database and verify the results. +It requires an appropriate **Python module to be installed separately** - depending on your database, like e.g. `oracledb` or `pymysql`. + +The library consists of some keywords designed to perform different checks on your database. +Here you can find the [keyword docs](http://marketsquare.github.io/Robotframework-Database-Library/). + +Wath the [talk at Robocon 2024 about the Database Library update](https://youtu.be/A96NTUps8sU). + +[![Talk at Robocon 2024 about the Database Library update](http://img.youtube.com/vi/A96NTUps8sU/0.jpg)](https://youtu.be/A96NTUps8sU) + +# Requirements +- Python +- Robot Framework +- Python database module you're going to use - e.g. `oracledb` +# Installation +``` +pip install robotframework-databaselibrary +``` +# Basic usage examples +```RobotFramework +*** Settings *** +Library DatabaseLibrary +Test Setup Connect To My Oracle DB + +*** Keywords *** +Connect To My Oracle DB + Connect To Database + ... oracledb + ... db_name=db + ... db_user=my_user + ... db_password=my_pass + ... db_host=127.0.0.1 + ... db_port=1521 + +*** Test Cases *** +Get All Names + ${Rows}= Query select FIRST_NAME, LAST_NAME from person + Should Be Equal ${Rows}[0][0] Franz Allan + Should Be Equal ${Rows}[0][1] See + Should Be Equal ${Rows}[1][0] Jerry + Should Be Equal ${Rows}[1][1] Schneider + +Person Table Contains Expected Records + ${sql}= Catenate select LAST_NAME from person + Check Query Result ${sql} contains See + Check Query Result ${sql} equals Schneider row=1 + +Wait Until Table Gets New Record + ${sql}= Catenate select LAST_NAME from person + Check Row Count ${sql} > 2 retry_timeout=5s + +Person Table Contains No Joe + ${sql}= Catenate SELECT id FROM person + ... WHERE FIRST_NAME= 'Joe' + Check Row Count ${sql} == 0 +``` +See more examples in the folder `tests`. + +# Handling multiple database connections +The library can handle multiple connections to different databases using *aliases*. +An alias is set while creating a connection and can be passed to library keywords in a corresponding argument. +## Example +```RobotFramework +*** Settings *** +Library DatabaseLibrary +Test Setup Connect To All Databases +Test Teardown Disconnect From All Databases + +*** Keywords *** +Connect To All Databases + Connect To Database + ... psycopg2 + ... db_name=db + ... db_user=db_user + ... db_password=pass + ... db_host=127.0.0.1 + ... db_port=5432 + ... alias=postgres + Connect To Database + ... pymysql + ... db_name=db + ... db_user=db_user + ... db_password=pass + ... db_host=127.0.0.1 + ... db_port=3306 + ... alias=mysql + +*** Test Cases *** +Using Aliases + ${names}= Query select LAST_NAME from person alias=postgres + Execute Sql String drop table XYZ alias=mysql + +Switching Default Alias + Switch Database postgres + ${names}= Query select LAST_NAME from person + Switch Database mysql + Execute Sql String drop table XYZ +``` + +# Connection examples for different DB modules +
+Oracle (oracle_db) + +```RobotFramework +# Thin mode is used by default +Connect To Database +... oracledb +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=1521 + +# Thick mode with default location of the Oracle Instant Client +Connect To Database +... oracledb +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=1521 +... oracle_driver_mode=thick + +# Thick mode with custom location of the Oracle Instant Client +Connect To Database +... oracledb +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=1521 +... oracle_driver_mode=thick,lib_dir=C:/instant_client_23_5 +``` +
+ +
+ PostgreSQL (psycopg2) + +```RobotFramework +Connect To Database +... psycopg2 +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=5432 +``` +
+ +
+Microsoft SQL Server (pymssql) + +```RobotFramework +# UTF-8 charset is used by default +Connect To Database +... pymssql +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=1433 + +# Specifying a custom charset +Connect To Database +... pymssql +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=1433 +... db_charset=cp1252 +``` +
+ +
+MySQL (pymysql) + +```RobotFramework +# UTF-8 charset is used by default +Connect To Database +... pymysql +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=3306 + +# Specifying a custom charset +Connect To Database +... pymysql +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=3306 +... db_charset=cp1252 +``` +
+ +
+IBM DB2 (ibm_db_dbi) + +```RobotFramework +Connect To Database +... ibm_db_dbi +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=50000 +``` +
+ +
+MySQL via ODBC (pyodbc) + +```RobotFramework +# ODBC driver name is required +# ODBC driver itself has to be installed +Connect To Database +... pyodbc +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=3306 +... odbc_driver={MySQL ODBC 9.2 ANSI Driver} + +# Specifying a custom charset if needed +Connect To Database +... pyodbc +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=3306 +... odbc_driver={MySQL ODBC 9.2 ANSI Driver} +... db_charset=latin1 +``` +
+ +
+Oracle via JDBC (jaydebeapi) + +```RobotFramework +# Username and password must be set as a dictionary +VAR &{CREDENTIALS} user=db_user password=pass + +# JAR file with Oracle JDBC driver is required +# Jaydebeapi is not "natively" supported by the Database Library, +# so using the custom parameters +Connect To Database +... jaydebeapi +... jclassname=oracle.jdbc.driver.OracleDriver +... url=jdbc:oracle:thin:@127.0.0.1:1521/db +... driver_args=${CREDENTIALS} +... jars=C:/ojdbc17.jar + +# Set if getting error 'Could not commit/rollback with auto-commit enabled' +Set Auto Commit False + +# Set for automatically removing trailing ';' (might be helpful for Oracle) +Set Omit Trailing Semicolon True +``` +
+ +
+SQLite (sqlite3) + +```RobotFramework +# Using custom parameters required +Connect To Database +... sqlite3 +... database=./my_database.db +... isolation_level=${None} +``` +
+ +
+Teradata (teradata) + +```RobotFramework +Connect To Database +... teradata +... db_name=db +... db_user=db_user +... db_password=pass +... db_host=127.0.0.1 +... db_port=1025 +``` +
+ +# Using configuration file +The `Connect To Database` keyword allows providing the connection parameters in two ways: +- As keyword arguments +- In a configuration file - a simple list of _key=value_ pairs, set inside an _alias_ section. + +You can use only one way or you can combine them: +- The keyword arguments are taken by default +- If no keyword argument is provided, a parameter value is searched in the config file + +Along with commonly used connection parameters, named exactly as keyword arguments, a config file +can contain any other DB module specific parameters as key/value pairs. +If same custom parameter is provided both as a keyword argument *and* in config file, +the *keyword argument value takes precedence*. + +The path to the config file is set by default to `./resources/db.cfg`. +You can change it using an according parameter in the `Connect To Database` keyword. + +A config file *must* contain at least one section name - +the connection alias, if used (see [Handling multiple database connections](#handling-multiple-database-connections)), or +`[default]` if no aliases are used. + +## Config file examples +### Config file with default alias (equal to using no aliases at all) +``` +[default] +db_module=psycopg2 +db_name=yourdbname +db_user=yourusername +db_password=yourpassword +db_host=yourhost +db_port=yourport +``` +### Config file with a specific alias +``` +[myoracle] +db_module=oracledb +db_name=yourdbname +db_user=yourusername +db_password=yourpassword +db_host=yourhost +db_port=yourport +``` + +### Config file with some params only +``` +[default] +db_password=mysecret +``` +### Config file with some custom DB module specific params +``` +[default] +my_custom_param=value +``` + +# Inline assertions +Keywords, that accept arguments ``assertion_operator`` and ``expected_value``, +perform a check according to the specified condition - using the [Assertion Engine](https://github.com/MarketSquare/AssertionEngine). + +## Examples +```RobotFramework +Check Row Count SELECT id FROM person == 2 +Check Query Result SELECT first_name FROM person contains Allan +``` + +# Retry mechanism +Assertion keywords, that accept arguments ``retry_timeout`` and ``retry_pause``, support waiting for assertion to pass. + +Setting the ``retry_timeout`` argument enables the mechanism - +in this case the SQL request and the assertion are executed in a loop, +until the assertion is passed or the ``retry_timeout`` is reached. +The pause between the loop iterations is set using the ``retry_pause`` argument. + +The argument values are set in [Robot Framework time format](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format) - e.g. ``5 seconds``. + +The retry mechanism is disabled by default - ``retry_timeout`` is set to ``0``. + +## Examples +```RobotFramework +${sql}= Catenate SELECT first_name FROM person +Check Row Count ${sql} == 2 retry_timeout=10 seconds +Check Query Result ${sql} contains Allan retry_timeout=5s retry_pause=1s +``` + +# Logging query results +Keywords, that fetch results of a SQL query, print the result rows as a table in RF log. +- A log head limit of *50 rows* is applied, other table rows are truncated in the log message. +- The limit and the logging in general can be adjusted any time in your tests using the Keyword `Set Logging Query Results`. + +You can also setup the limit or disable the logging during the library import. +## Examples +```RobotFramework +*** Settings *** +# Default behavior - logging of query results is enabled, log head is 50 rows. +Library DatabaseLibrary + +# Logging of query results is disabled, log head is 50 rows (default). +Library DatabaseLibrary log_query_results=False + +# Logging of query results is enabled (default), log head is 10 rows. +Library DatabaseLibrary log_query_results_head=10 + +# Logging of query results is enabled (default), log head limit is disabled (log all rows). +Library DatabaseLibrary log_query_results_head=0 +``` + +# Commit behavior +While creating a database connection, the library doesn't explicitly set the _autocommit_ behavior - +so the default value of the Python DB module is used. +According to Python DB API specification it should be disabled by default - +which means each SQL transaction (even a simple _SELECT_) must contain a dedicated commit statement, if necessary. + +The library manages it for you - keywords like `Query` or `Execute SQL String` +perform automatically a commit after running the query (or a rollback in case of error). + +You can turn off this automatic commit/rollback behavior using the ``no_transaction`` parameter. +See docs of a particular keyword. + +It's also possible to explicitly set the _autocommit_ behavior on the Python DB module level - +using the `Set Auto Commit` keyword. +This has no impact on the automatic commit/rollback behavior in library keywords (described above). + +# Omitting trailing semicolon behavior +Some databases (e.g. Oracle) throw an exception, if you leave a semicolon (;) at the SQL string end. +However, there are exceptional cases, when you need it even for Oracle - e.g. at the end of a PL/SQL block. + +The library can handle it for you and remove the semicolon at the end of the SQL string. +By default, it's decided based on the current database module in use: +- For `oracle_db` and `cx_Oracle`, the trailing semicolon is removed +- For other modules, the trailing semicolon is left as it is + +You can also set this behavior explicitly: +- Using the `Set Omit Trailing Semicolon` keyword +- Using the `omit_trailing_semicolon` parameter in the `Execute SQL String` keyword. + +# Database modules compatibility +> Looking for [Connection examples for different DB modules](#connection-examples-for-different-db-modules)? + +The library is basically compatible with any [Python Database API Specification 2.0](https://peps.python.org/pep-0249/) module. + +However, the actual implementation in existing Python modules is sometimes quite different, which requires custom handling in the library. +Therefore there are some modules, which are "natively" supported in the library - and others, which may work and may not. + +## Python modules currently "natively" supported +### Oracle +- [oracledb](https://oracle.github.io/python-oracledb/) + - Both thick and thin client modes are supported - you can select one using the `oracle_driver_mode` parameter. + - However, due to current limitations of the oracledb module, **it's not possible to switch between thick and thin modes during a test execution session** - even in different suites. +- [cx_Oracle](https://oracle.github.io/python-cx_Oracle/) +### MySQL +- [pymysql](https://github.com/PyMySQL/PyMySQL) +- [MySQLdb](https://mysqlclient.readthedocs.io/index.html) +### PostgreSQL +- [psycopg2](https://www.psycopg.org/docs/) +### MS SQL Server +- [pymssql](https://github.com/pymssql/pymssql) +### SQLite +- [sqlite3](https://docs.python.org/3/library/sqlite3.html) +### Teradata +- [teradata](https://github.com/teradata/PyTd) +### IBM DB2 +- The Python package to be installed is [ibm_db](https://github.com/ibmdb/python-ibmdb). It includes two modules - `ibm_db` and `ibm_db_dbi`. +- *Using `ibm_db_dbi` is highly recommended* as only this module is Python DB API 2.0 compatible. See [official docs](https://www.ibm.com/docs/en/db2/12.1?topic=applications-python-sqlalchemy-django-framework). +### ODBC +- [pyodbc](https://github.com/mkleehammer/pyodbc) +- [pypyodbc](https://github.com/pypyodbc/pypyodbc) +### Kingbase +- ksycopg2 + +# Further references (partly outdated) +- [List of Python DB interfaces](https://wiki.python.org/moin/DatabaseInterfaces) +- [Python DB programming](https://wiki.python.org/moin/DatabaseProgramming) diff --git a/dist/robotframework-databaselibrary-1.2.4.tar.gz b/dist/robotframework-databaselibrary-1.2.4.tar.gz deleted file mode 100644 index 555dced2..00000000 Binary files a/dist/robotframework-databaselibrary-1.2.4.tar.gz and /dev/null differ diff --git a/dist/robotframework_databaselibrary-1.2.4-py3-none-any.whl b/dist/robotframework_databaselibrary-1.2.4-py3-none-any.whl deleted file mode 100644 index 89cf0d61..00000000 Binary files a/dist/robotframework_databaselibrary-1.2.4-py3-none-any.whl and /dev/null differ diff --git a/doc/DatabaseLibrary.html b/doc/DatabaseLibrary.html deleted file mode 100644 index 058765e5..00000000 --- a/doc/DatabaseLibrary.html +++ /dev/null @@ -1,912 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - -
-

Opening library documentation failed

- -
- - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 00000000..266ef9c0 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + +
+

Opening library documentation failed

+ +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f678224e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = [ + "setuptools>=61.0", + "robotframework>=5.0.1", + "robotframework-assertion-engine" + ] +build-backend = "setuptools.build_meta" + +[project] +name = "robotframework-databaselibrary" +authors = [{name="Franz Allan Valencia See", email="franz.see@gmail.com"}, +] +description = "Database Library for Robot Framework" +readme = "README.md" +requires-python = ">=3.8.1" +dependencies = [ + "robotframework>=5.0.1", + "robotframework-assertion-engine" +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] +license = {text = "Apache License 2.0"} +dynamic = ["version"] + +[project.urls] +"Homepage" = "https://github.com/MarketSquare/Robotframework-Database-Library" +"Keyword docs" = "http://marketsquare.github.io/Robotframework-Database-Library/" + +[tool.setuptools.dynamic] +version = {attr = "DatabaseLibrary.__version__"} + +[tool.black] +line-length = 120 + +[tool.isort] +profile = "black" +line_length = 120 diff --git a/requirements.txt b/requirements.txt index cb708a2b..0a7cdb7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ -#PyMySQL==0.7.4 -#psycopg2==2.6.1 -robotframework>=3.0 +robotframework +robotframework-excellib +robotframework-assertion-engine +psycopg2-binary +pre-commit +build +twine \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100755 index fa5c0286..00000000 --- a/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2010 Franz Allan Valencia See -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -"""Setup script for Robot's DatabaseLibrary distributions""" - -from os.path import abspath, dirname, join - -try: - from setuptools import setup -except ImportError as error: - from distutils.core import setup - - -version_file = join(dirname(abspath(__file__)), 'src', 'DatabaseLibrary', 'version.py') - -with open(version_file) as file: - code = compile(file.read(), version_file, 'exec') - exec(code) - -setup(name = 'robotframework-databaselibrary', - version = VERSION, - description = 'Database utility library for Robot Framework', - author = 'Franz Allan Valencia See', - author_email = 'franz.see@gmail.com', - url = 'https://github.com/franz-see/Robotframework-Database-Library', - package_dir = { '' : 'src'}, - packages = ['DatabaseLibrary'], - package_data = {'DatabaseLibrary': []}, - requires = ['robotframework'] - ) diff --git a/src/DatabaseLibrary/__init__.py b/src/DatabaseLibrary/__init__.py index b9daabef..8b1ef859 100644 --- a/src/DatabaseLibrary/__init__.py +++ b/src/DatabaseLibrary/__init__.py @@ -14,55 +14,422 @@ import os +from DatabaseLibrary.assertion import Assertion from DatabaseLibrary.connection_manager import ConnectionManager from DatabaseLibrary.query import Query -from DatabaseLibrary.assertion import Assertion from DatabaseLibrary.version import VERSION -_version_ = VERSION +__version__ = VERSION + class DatabaseLibrary(ConnectionManager, Query, Assertion): """ - Database Library contains utilities meant for Robot Framework's usage. + The Database Library for [https://robotframework.org|Robot Framework] allows you to query a database and verify the results. + It requires an appropriate *Python module to be installed separately* - depending on your database, like e.g. `oracledb` or `pymysql`. + + == Table of contents == + %TOC% + + = Requirements = + - Python + - Robot Framework + - Python database module you're going to use - e.g. `oracledb` + + = Installation = + | pip install robotframework-databaselibrary + Don't forget to install the required Python database module! + + = Basic usage examples = + | *** Settings *** + | Library DatabaseLibrary + | Test Setup Connect To My Oracle DB + | + | *** Keywords *** + | Connect To My Oracle DB + | Connect To Database + | ... oracledb + | ... db_name=db + | ... db_user=my_user + | ... db_password=my_pass + | ... db_host=127.0.0.1 + | ... db_port=1521 + | + | *** Test Cases *** + | Get All Names + | ${Rows}= Query select FIRST_NAME, LAST_NAME from person + | Should Be Equal ${Rows}[0][0] Franz Allan + | Should Be Equal ${Rows}[0][1] See + | Should Be Equal ${Rows}[1][0] Jerry + | Should Be Equal ${Rows}[1][1] Schneider + | + | Person Table Contains Expected Records + | ${sql}= Catenate select LAST_NAME from person + | Check Query Result ${sql} contains See + | Check Query Result ${sql} equals Schneider row=1 + | + | Wait Until Table Gets New Record + | ${sql}= Catenate select LAST_NAME from person + | Check Row Count ${sql} > 2 retry_timeout=5s + | + | Person Table Contains No Joe + | ${sql}= Catenate SELECT id FROM person + | ... WHERE FIRST_NAME= 'Joe' + | Check Row Count ${sql} == 0 + | + + = Handling multiple database connections = + The library can handle multiple connections to different databases using *aliases*. + An alias is set while creating a connection and can be passed to library keywords in a corresponding argument. + == Example == + | *** Settings *** + | Library DatabaseLibrary + | Test Setup Connect To All Databases + | Test Teardown Disconnect From All Databases + | + | *** Keywords *** + | Connect To All Databases + | Connect To Database + | ... psycopg2 + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=5432 + | ... alias=postgres + | Connect To Database + | ... pymysql + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=3306 + | ... alias=mysql + | + | *** Test Cases *** + | Using Aliases + | ${names}= Query select LAST_NAME from person alias=postgres + | Execute Sql String drop table XYZ alias=mysql + | + | Switching Default Alias + | Switch Database postgres + | ${names}= Query select LAST_NAME from person + | Switch Database mysql + | Execute Sql String drop table XYZ + | + + = Connection examples for different DB modules = + == Oracle (oracle_db) == + | # Thin mode is used by default + | Connect To Database + | ... oracledb + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=1521 + | + | # Thick mode with default location of the Oracle Instant Client + | Connect To Database + | ... oracledb + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=1521 + | ... oracle_driver_mode=thick + | + | # Thick mode with custom location of the Oracle Instant Client + | Connect To Database + | ... oracledb + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=1521 + | ... oracle_driver_mode=thick,lib_dir=C:/instant_client_23_5 + == PostgreSQL (psycopg2) == + | Connect To Database + | ... psycopg2 + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=5432 + == Microsoft SQL Server (pymssql) == + | # UTF-8 charset is used by default + | Connect To Database + | ... pymssql + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=1433 + | + | # Specifying a custom charset + | Connect To Database + | ... pymssql + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=1433 + | ... db_charset=cp1252 + == MySQL (pymysql) == + | # UTF-8 charset is used by default + | Connect To Database + | ... pymysql + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=3306 + | + | # Specifying a custom charset + | Connect To Database + | ... pymysql + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=3306 + | ... db_charset=cp1252 + == IBM DB2 (ibm_db) == + | Connect To Database + | ... ibm_db_dbi + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=50000 + == MySQL via ODBC (pyodbc) == + | # ODBC driver name is required + | # ODBC driver itself has to be installed + | Connect To Database + | ... pyodbc + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=3306 + | ... odbc_driver={MySQL ODBC 9.2 ANSI Driver} + | + | # Specifying a custom charset if needed + | Connect To Database + | ... pyodbc + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=3306 + | ... odbc_driver={MySQL ODBC 9.2 ANSI Driver} + | ... db_charset=latin1 + == Oracle via JDBC (jaydebeapi) == + | # Username and password must be set as a dictionary + | VAR &{CREDENTIALS} user=db_user password=pass + | + | # JAR file with Oracle JDBC driver is required + | # Jaydebeapi is not "natively" supported by the Database Library, + | # so using the custom parameters + | Connect To Database + | ... jaydebeapi + | ... jclassname=oracle.jdbc.driver.OracleDriver + | ... url=jdbc:oracle:thin:@127.0.0.1:1521/db + | ... driver_args=${CREDENTIALS} + | ... jars=C:/ojdbc17.jar + | + | # Set if getting error 'Could not commit/rollback with auto-commit enabled' + | Set Auto Commit False + | + | # Set for automatically removing trailing ';' (might be helpful for Oracle) + | Set Omit Trailing Semicolon True + == SQLite (sqlite3) == + | # Using custom parameters required + | Connect To Database + | ... sqlite3 + | ... database=./my_database.db + | ... isolation_level=${None} + == Teradata (teradata) == + | Connect To Database + | ... teradata + | ... db_name=db + | ... db_user=db_user + | ... db_password=pass + | ... db_host=127.0.0.1 + | ... db_port=1025 + + = Using configuration file = + The `Connect To Database` keyword allows providing the connection parameters in two ways: + - As keyword arguments + - In a configuration file - a simple list of _key=value_ pairs, set inside an _alias_ section. + + You can use only one way or you can combine them: + - The keyword arguments are taken by default + - If no keyword argument is provided, a parameter value is searched in the config file + + Along with commonly used connection parameters, named exactly as keyword arguments, a config file + can contain any other DB module specific parameters as key/value pairs. + If same custom parameter is provided both as a keyword argument *and* in config file, + the *keyword argument value takes precedence*. + + The path to the config file is set by default to `./resources/db.cfg`. + You can change it using an according parameter in the `Connect To Database` keyword. + + A config file *must* contain at least one section name - + the connection alias, if used (see `Handling multiple database connections`), or + `[default]` if no aliases are used. + + == Config file examples == + === Config file with default alias (equal to using no aliases at all) === + | [default] + | db_module=psycopg2 + | db_name=yourdbname + | db_user=yourusername + | db_password=yourpassword + | db_host=yourhost + | db_port=yourport - This can allow you to query your database after an action has been made to verify the results. + === Config file with a specific alias === + | [myoracle] + | db_module=oracledb + | db_name=yourdbname + | db_user=yourusername + | db_password=yourpassword + | db_host=yourhost + | db_port=yourport - This is `compatible*` with any Database API Specification 2.0 module. + === Config file with some params only === + | [default] + | db_password=mysecret + === Config file with some custom DB module specific params === + | [default] + | my_custom_param=value - References: + = Inline assertions = + Keywords, that accept arguments ``assertion_operator`` <`AssertionOperator`> and ``expected_value``, + perform a check according to the specified condition - using the [https://github.com/MarketSquare/AssertionEngine|Assertion Engine]. - + Database API Specification 2.0 - http://www.python.org/dev/peps/pep-0249/ + Examples: + | Check Row Count | SELECT id FROM person | *==* | 2 | + | Check Query Result | SELECT first_name FROM person | *contains* | Allan | - + Lists of DB API 2.0 - http://wiki.python.org/moin/DatabaseInterfaces + = Retry mechanism = + Assertion keywords, that accept arguments ``retry_timeout`` and ``retry_pause``, support waiting for assertion to pass. - + Python Database Programming - http://wiki.python.org/moin/DatabaseProgramming/ + Setting the ``retry_timeout`` argument enables the mechanism - + in this case the SQL request and the assertion are executed in a loop, + until the assertion is passed or the ``retry_timeout`` is reached. + The pause between the loop iterations is set using the ``retry_pause`` argument. - Notes: + The argument values are set in [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format|Robot Framework time format] - + e.g. ``5 seconds``. + The retry mechanism is disabled by default - ``retry_timeout`` is set to ``0``. + Examples: + | Check Row Count | SELECT id FROM person | *==* | 2 | retry_timeout=10 seconds | + | Check Query Result | SELECT first_name FROM person | *contains* | Allan | retry_timeout=5s | retry_pause=1s | - `compatible* - or at least theoretically it should be compatible. Currently tested only with postgresql - (using psycopg2).` + = Logging query results = + Keywords, that fetch results of a SQL query, print the result rows as a table in RF log. + - A log head limit of *50 rows* is applied, other table rows are truncated in the log message. + - The limit and the logging in general can be adjusted any time in your tests using the Keyword `Set Logging Query Results`. - Example Usage: - | # Setup | - | Connect to Database | - | # Guard assertion (verify that test started in expected state). | - | Check if not exists in database | select id from person where first_name = 'Franz Allan' and last_name = 'See' | - | # Drive UI to do some action | - | Go To | http://localhost/person/form.html | | # From selenium library | - | Input Text | name=first_name | Franz Allan | # From selenium library | - | Input Text | name=last_name | See | # From selenium library | - | Click Button | Save | | # From selenium library | - | # Log results | - | @{queryResults} | Query | select * from person | - | Log Many | @{queryResults} | - | # Verify if persisted in the database | - | Check if exists in database | select id from person where first_name = 'Franz Allan' and last_name = 'See' | - | # Teardown | - | Disconnect from Database | + You can also setup the limit or disable the logging during the library import. + Examples: + + | *** Settings *** + | # Default behavior - logging of query results is enabled, log head is 50 rows. + | Library DatabaseLibrary + | + | # Logging of query results is disabled, log head is 50 rows (default). + | Library DatabaseLibrary log_query_results=False + | + | # Logging of query results is enabled (default), log head is 10 rows. + | Library DatabaseLibrary log_query_results_head=10 + | + | # Logging of query results is enabled (default), log head limit is disabled (log all rows). + | Library DatabaseLibrary log_query_results_head=0 + + = Commit behavior = + While creating a database connection, the library doesn't explicitly set the _autocommit_ behavior - + so the default value of the Python DB module is used. + According to Python DB API specification it should be disabled by default - + which means each SQL transaction (even a simple _SELECT_) must contain a dedicated commit statement, if necessary. + + The library manages it for you - keywords like `Query` or `Execute SQL String` + perform automatically a commit after running the query (or a rollback in case of error). + + You can turn off this automatic commit/rollback behavior using the ``no_transaction`` parameter. + See docs of a particular keyword. + + It's also possible to explicitly set the _autocommit_ behavior on the Python DB module level - + using the `Set Auto Commit` keyword. + This has no impact on the automatic commit/rollback behavior in library keywords (described above). + + = Omitting trailing semicolon behavior = + Some databases (e.g. Oracle) throw an exception, if you leave a semicolon (;) at the SQL string end. + However, there are exceptional cases, when you need it even for Oracle - e.g. at the end of a PL/SQL block. + + The library can handle it for you and remove the semicolon at the end of the SQL string. + By default, it's decided based on the current database module in use: + - For `oracle_db` and `cx_Oracle`, the trailing semicolon is removed + - For other modules, the trailing semicolon is left as it is + + You can also set this behavior explicitly: + - Using the `Set Omit Trailing Semicolon` keyword + - Using the `omit_trailing_semicolon` parameter in the `Execute SQL String` keyword. + + = Database modules compatibility = + The library is basically compatible with any [https://peps.python.org/pep-0249|Python Database API Specification 2.0] module. + + However, the actual implementation in existing Python modules is sometimes quite different, which requires custom handling in the library. + Therefore, there are some modules, which are "natively" supported in the library - and others, which may work and may not. + + == Python modules currently "natively" supported == + === Oracle === + [https://oracle.github.io/python-oracledb/|oracledb] + - Both thick and thin client modes are supported - you can select one using the `oracle_driver_mode` parameter. + - However, due to current limitations of the oracledb module, *it's not possible to switch between thick and thin modes during a test execution session* - even in different suites. + + [https://oracle.github.io/python-cx_Oracle/|cx_Oracle] + + === MySQL === + - [https://github.com/PyMySQL/PyMySQL|pymysql] + - [https://mysqlclient.readthedocs.io/index.html|MySQLdb] + === PostgreSQL === + - [https://www.psycopg.org/docs/|psycopg2] + === MS SQL Server === + - [https://github.com/pymssql/pymssql|pymssql] + === SQLite === + - [https://docs.python.org/3/library/sqlite3.html|sqlite3] + === Teradata === + - [https://github.com/teradata/PyTd|teradata] + === IBM DB2 === + - The Python package to be installed is [https://github.com/ibmdb/python-ibmdb|ibm_db]. It includes two modules - `ibm_db` and `ibm_db_dbi`. + - Using *`ibm_db_dbi` is highly recommended* as only this module is Python DB API 2.0 compatible. See [https://www.ibm.com/docs/en/db2/12.1?topic=applications-python-sqlalchemy-django-framework|official docs]. + === ODBC === + - [https://github.com/mkleehammer/pyodbc|pyodbc] + - [https://github.com/pypyodbc/pypyodbc|pypyodbc] + === Kingbase === + - ksycopg2 """ - ROBOT_LIBRARY_SCOPE = 'GLOBAL' + ROBOT_LIBRARY_SCOPE = "GLOBAL" + ROBOT_LIBRARY_VERSION = __version__ + + def __init__(self, log_query_results=True, log_query_results_head=50, warn_on_connection_overwrite=True): + """ + The library can be imported without any arguments: + | *** Settings *** + | Library DatabaseLibrary + + Use optional library import parameters: + - ``log_query_results`` and ``log_query_results_head`` to disable `Logging query results` or setup the log head + - ``warn_on_connection_overwrite`` to disable the warning about overwriting an existing connection + """ + ConnectionManager.__init__(self, warn_on_connection_overwrite=warn_on_connection_overwrite) + if log_query_results_head < 0: + raise ValueError(f"Wrong log head value provided: {log_query_results_head}. The value can't be negative!") + Query.__init__(self, log_query_results=log_query_results, log_query_results_head=log_query_results_head) diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index 70ed6dd8..3d2fd565 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -11,209 +11,466 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Optional, Tuple +from assertionengine import AssertionOperator, verify_assertion from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn +from robot.utils import timestr_to_secs +from .params_decorator import renamed_args -class Assertion(object): + +class Assertion: """ Assertion handles all the assertions of Database Library. """ - def check_if_exists_in_database(self, selectStatement, sansTran=False): + def check_if_exists_in_database( + self, + select_statement: str, + *, + no_transaction: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + ): """ - Check if any row would be returned by given the input `selectStatement`. If there are no results, then this will - throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction + *DEPRECATED* Use new `Check Row Count` keyword with assertion engine instead. + The deprecated keyword will be removed in future versions. + + Check if any row would be returned by given the input ``select_statement``. If there are no results, then this will + throw an AssertionError. + + Set optional input ``no_transaction`` to _True_ to run command without an explicit transaction commit or rollback. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + The default error message can be overridden with the ``msg`` argument. - When you have the following assertions in your robot - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - Then you will get the following: - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | # PASS | - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | # FAIL | + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | True | + Examples: + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | msg=my error message | + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | no_transaction=True | + | @{parameters} | Create List | John | + | Check If Exists In Database | SELECT id FROM person WHERE first_name = %s | parameters=${parameters} | """ - logger.info ('Executing : Check If Exists In Database | %s ' % selectStatement) - if not self.query(selectStatement, sansTran): - raise AssertionError("Expected to have have at least one row from '%s' " - "but got 0 rows." % selectStatement) - - def check_if_not_exists_in_database(self, selectStatement, sansTran=False): + if not self.query(select_statement, no_transaction, alias=alias, parameters=parameters): + raise AssertionError( + msg or f"Expected to have have at least one row, but got 0 rows from: '{select_statement}'" + ) + + def check_if_not_exists_in_database( + self, + selectStatement: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + ): """ + *DEPRECATED* Use new `Check Row Count` keyword with assertion engine instead. + The deprecated keyword will be removed in future versions. + This is the negation of `check_if_exists_in_database`. - Check if no rows would be returned by given the input `selectStatement`. If there are any results, then this - will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. + Check if no rows would be returned by given the input ``selectStatement``. If there are any results, then this + will throw an AssertionError. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - When you have the following assertions in your robot - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | + The default error message can be overridden with the ``msg`` argument. - Then you will get the following: - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | # PASS | - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | # FAIL | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | True | - """ - logger.info('Executing : Check If Not Exists In Database | %s ' % selectStatement) - queryResults = self.query(selectStatement, sansTran) - if queryResults: - raise AssertionError("Expected to have have no rows from '%s' " - "but got some rows : %s." % (selectStatement, queryResults)) + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - def row_count_is_0(self, selectStatement, sansTran=False): + Examples: + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | msg=my error message | + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | sansTran=True | + | @{parameters} | Create List | John | + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = %s | parameters=${parameters} | + """ + query_results = self.query(selectStatement, sansTran, alias=alias, parameters=parameters) + if query_results: + raise AssertionError( + msg or f"Expected to have have no rows from '{selectStatement}', but got some rows: {query_results}" + ) + + def row_count_is_0( + self, + selectStatement: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + ): """ - Check if any rows are returned from the submitted `selectStatement`. If there are, then this will throw an - AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction commit or + *DEPRECATED* Use new `Check Row Count` keyword with assertion engine instead. + The deprecated keyword will be removed in future versions. + + Check if any rows are returned from the submitted ``selectStatement``. If there are, then this will throw an + AssertionError. + + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + The default error message can be overridden with the ``msg`` argument. - When you have the following assertions in your robot - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - Then you will get the following: - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | # FAIL | - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | # PASS | + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | True | + Examples: + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | msg=my error message | + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | alias=my_alias | + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | sansTran=True | + | @{parameters} | Create List | John | + | Row Count is 0 | SELECT id FROM person WHERE first_name = %s | parameters=${parameters} | """ - logger.info('Executing : Row Count Is 0 | %s ' % selectStatement) - num_rows = self.row_count(selectStatement, sansTran) + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) if num_rows > 0: - raise AssertionError("Expected zero rows to be returned from '%s' " - "but got rows back. Number of rows returned was %s" % (selectStatement, num_rows)) - - def row_count_is_equal_to_x(self, selectStatement, numRows, sansTran=False): + raise AssertionError(msg or f"Expected 0 rows, but {num_rows} were returned from: '{selectStatement}'") + + def row_count_is_equal_to_x( + self, + selectStatement: str, + numRows: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + ): """ - Check if the number of rows returned from `selectStatement` is equal to the value submitted. If not, then this - will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. + *DEPRECATED* Use new `Check Row Count` keyword with assertion engine instead. + The deprecated keyword will be removed in future versions. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | - | 2 | Jerry | Schneider | + Check if the number of rows returned from ``selectStatement`` is equal to the value submitted. If not, then this + will throw an AssertionError. - When you have the following assertions in your robot - | Row Count Is Equal To X | SELECT id FROM person | 1 | - | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - Then you will get the following: - | Row Count Is Equal To X | SELECT id FROM person | 1 | # FAIL | - | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | # PASS | + The default error message can be overridden with the ``msg`` argument. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | True | - """ - logger.info('Executing : Row Count Is Equal To X | %s | %s ' % (selectStatement, numRows)) - num_rows = self.row_count(selectStatement, sansTran) - if num_rows != int(numRows.encode('ascii')): - raise AssertionError("Expected same number of rows to be returned from '%s' " - "than the returned rows of %s" % (selectStatement, num_rows)) + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - def row_count_is_greater_than_x(self, selectStatement, numRows, sansTran=False): + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). + + Examples: + | Row Count Is Equal To X | SELECT id FROM person | 1 | + | Row Count Is Equal To X | SELECT id FROM person | 3 | msg=my error message | + | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | + | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | sansTran=True | + | @{parameters} | Create List | John | + | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = %s | 0 | parameters=${parameters} | + """ + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) + if num_rows != int(numRows.encode("ascii")): + raise AssertionError( + msg or f"Expected {numRows} rows, but {num_rows} were returned from: '{selectStatement}'" + ) + + def row_count_is_greater_than_x( + self, + selectStatement: str, + numRows: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + ): """ - Check if the number of rows returned from `selectStatement` is greater than the value submitted. If not, then - this will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. + *DEPRECATED* Use new `Check Row Count` keyword with assertion engine instead. + The deprecated keyword will be removed in future versions. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | - | 2 | Jerry | Schneider | + Check if the number of rows returned from ``selectStatement`` is greater than the value submitted. If not, then + this will throw an AssertionError. - When you have the following assertions in your robot - | Row Count Is Greater Than X | SELECT id FROM person | 1 | - | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - Then you will get the following: - | Row Count Is Greater Than X | SELECT id FROM person | 1 | # PASS | - | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | # FAIL | + The default error message can be overridden with the ``msg`` argument. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Row Count Is Greater Than X | SELECT id FROM person | 1 | True | - """ - logger.info('Executing : Row Count Is Greater Than X | %s | %s ' % (selectStatement, numRows)) - num_rows = self.row_count(selectStatement, sansTran) - if num_rows <= int(numRows.encode('ascii')): - raise AssertionError("Expected more rows to be returned from '%s' " - "than the returned rows of %s" % (selectStatement, num_rows)) + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - def row_count_is_less_than_x(self, selectStatement, numRows, sansTran=False): + Examples: + | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | + | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | msg=my error message | + | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | + | Row Count Is Greater Than X | SELECT id FROM person | 1 | sansTran=True | + | @{parameters} | Create List | John | + | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = %s | 0 | parameters=${parameters} | + """ + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) + if num_rows <= int(numRows.encode("ascii")): + raise AssertionError( + msg or f"Expected more than {numRows} rows, but {num_rows} were returned from '{selectStatement}'" + ) + + def row_count_is_less_than_x( + self, + selectStatement: str, + numRows: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + ): """ - Check if the number of rows returned from `selectStatement` is less than the value submitted. If not, then this - will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. + *DEPRECATED* Use new `Check Row Count` keyword with assertion engine instead. + The deprecated keyword will be removed in future versions. + + Check if the number of rows returned from ``selectStatement`` is less than the value submitted. If not, then this + will throw an AssertionError. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | - | 2 | Jerry | Schneider | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - When you have the following assertions in your robot - | Row Count Is Less Than X | SELECT id FROM person | 3 | + Using optional ``msg`` to override the default error message: + + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). + + Examples: | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 2 | msg=my error message | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 3 | alias=my_alias | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 4 | sansTran=True | + | @{parameters} | Create List | John | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = %s | 5 | parameters=${parameters} | + """ + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) + if num_rows >= int(numRows.encode("ascii")): + raise AssertionError( + msg or f"Expected less than {numRows} rows, but {num_rows} were returned from '{selectStatement}'" + ) + + @renamed_args(mapping={"selectStatement": "select_statement", "sansTran": "no_transaction"}) + def check_row_count( + self, + select_statement: str, + assertion_operator: AssertionOperator, + expected_value: int, + assertion_message: Optional[str] = None, + no_transaction: bool = False, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + retry_timeout="0 seconds", + retry_pause="0.5 seconds", + *, + selectStatement: Optional[str] = None, + sansTran: Optional[bool] = None, + ): + """ + Check the number of rows returned from ``select_statement`` using ``assertion_operator`` + and ``expected_value``. See `Inline assertions` for more details. + + Use ``assertion_message`` to override the default error message. + + Set ``no_transaction`` to _True_ to run command without explicit transaction rollback in case of error. + See `Commit behavior` for details. - Then you will get the following: - | Row Count Is Less Than X | SELECT id FROM person | 3 | # PASS | - | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | # FAIL | + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Row Count Is Less Than X | SELECT id FROM person | 3 | True | + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). + + Use ``retry_timeout`` and ``retry_pause`` parameters to enable waiting for assertion to pass. + See `Retry mechanism` for more details. + + === Some parameters were renamed in version 2.0 === + The old parameters ``selectStatement`` and ``sansTran`` are *deprecated*, + please use new parameters ``select_statement`` and ``no_transaction`` instead. + + *The old parameters will be removed in future versions.* + + === Examples === + | Check Row Count | SELECT id FROM person WHERE first_name = 'John' | *==* | 1 | + | Check Row Count | SELECT id FROM person WHERE first_name = 'John' | *>=* | 2 | assertion_message=my error message | + | Check Row Count | SELECT id FROM person WHERE first_name = 'John' | *inequal* | 3 | alias=my_alias | + | Check Row Count | SELECT id FROM person WHERE first_name = 'John' | *less than* | 4 | no_transaction=True | + | @{parameters} | Create List | John | + | Check Row Count | SELECT id FROM person WHERE first_name = %s | *equals* | 5 | parameters=${parameters} | """ - logger.info('Executing : Row Count Is Less Than X | %s | %s ' % (selectStatement, numRows)) - num_rows = self.row_count(selectStatement, sansTran) - if num_rows >= int(numRows.encode('ascii')): - raise AssertionError("Expected less rows to be returned from '%s' " - "than the returned rows of %s" % (selectStatement, num_rows)) + check_ok = False + time_counter = 0 + while not check_ok: + try: + num_rows = self.row_count( + select_statement, no_transaction=no_transaction, alias=alias, parameters=parameters + ) + verify_assertion(num_rows, assertion_operator, expected_value, "Wrong row count:", assertion_message) + check_ok = True + except AssertionError as e: + if time_counter >= timestr_to_secs(retry_timeout): + logger.info(f"Timeout '{retry_timeout}' reached") + raise e + BuiltIn().sleep(retry_pause) + time_counter += timestr_to_secs(retry_pause) + + @renamed_args(mapping={"selectStatement": "select_statement", "sansTran": "no_transaction"}) + def check_query_result( + self, + select_statement: str, + assertion_operator: AssertionOperator, + expected_value: Any, + row=0, + col=0, + assertion_message: Optional[str] = None, + no_transaction: bool = False, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + retry_timeout="0 seconds", + retry_pause="0.5 seconds", + *, + selectStatement: Optional[str] = None, + sansTran: Optional[bool] = None, + ): + """ + Check value in query result returned from ``select_statement`` using ``assertion_operator`` and ``expected_value``. + The value position in results can be adjusted using ``row`` and ``col`` parameters (0-based). + See `Inline assertions` for more details. + + *The assertion in this keyword is type sensitive!* + The ``expected_value`` is taken as a string, no argument conversion is performed. + Use RF syntax like ``${1}`` for numeric values. + + Use optional ``assertion_message`` to override the default error message. - def table_must_exist(self, tableName, sansTran=False): + Set ``no_transaction`` to _True_ to run command without explicit transaction rollback in case of error. + See `Commit behavior` for details. + + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. + + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). + + Use ``retry_timeout`` and ``retry_pause`` parameters to enable waiting for assertion to pass. + See `Retry mechanism` for more details. + + === Some parameters were renamed in version 2.0 === + The old parameters ``selectStatement`` and ``sansTran`` are *deprecated*, + please use new parameters ``select_statement`` and ``no_transaction`` instead. + + *The old parameters will be removed in future versions.* + + === Examples === + | Check Query Result | SELECT first_name FROM person | *contains* | Allan | + | Check Query Result | SELECT first_name, last_name FROM person | *==* | Schneider | row=1 | col=1 | + | Check Query Result | SELECT id FROM person WHERE first_name = 'John' | *==* | 2 | # Fails, if query returns an integer value | + | Check Query Result | SELECT id FROM person WHERE first_name = 'John' | *==* | ${2} | # Works, if query returns an integer value | + | Check Query Result | SELECT first_name FROM person | *equal* | Franz Allan | assertion_message=my error message | + | Check Query Result | SELECT first_name FROM person | *inequal* | John | alias=my_alias | + | Check Query Result | SELECT first_name FROM person | *contains* | Allan | no_transaction=True | + | @{parameters} | Create List | John | + | Check Query Result | SELECT first_name FROM person | *contains* | Allan | parameters=${parameters} | + """ + check_ok = False + time_counter = 0 + while not check_ok: + try: + query_results = self.query( + select_statement, no_transaction=no_transaction, alias=alias, parameters=parameters + ) + row_count = len(query_results) + assert ( + row < row_count + ), f"Checking row '{row}' is not possible, as query results contain {row_count} rows only!" + col_count = len(query_results[row]) + assert ( + col < col_count + ), f"Checking column '{col}' is not possible, as query results contain {col_count} columns only!" + actual_value = query_results[row][col] + verify_assertion( + actual_value, assertion_operator, expected_value, "Wrong query result:", assertion_message + ) + check_ok = True + except AssertionError as e: + if time_counter >= timestr_to_secs(retry_timeout): + logger.info(f"Timeout '{retry_timeout}' reached") + raise e + BuiltIn().sleep(retry_pause) + time_counter += timestr_to_secs(retry_pause) + + @renamed_args(mapping={"tableName": "table_name", "sansTran": "no_transaction"}) + def table_must_exist( + self, + table_name: str, + no_transaction: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + *, + tableName: Optional[str] = None, + sansTran: Optional[bool] = None, + ): """ - Check if the table given exists in the database. Set optional input `sansTran` to True to run command without an - explicit transaction commit or rollback. + Check if the table with `table_name` exists in the database. - For example, given we have a table `person` in a database + Use ``msg`` for custom error message. - When you do the following: - | Table Must Exist | person | + Set ``no_transaction`` to _True_ to run command without explicit transaction rollback in case of error. + See `Commit behavior` for details. + + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. + + === Some parameters were renamed in version 2.0 === + The old parameters ``tableName`` and ``sansTran`` are *deprecated*, + please use new parameters ``table_name`` and ``no_transaction`` instead. - Then you will get the following: - | Table Must Exist | person | # PASS | - | Table Must Exist | first_name | # FAIL | - - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Table Must Exist | person | True | - """ - logger.info('Executing : Table Must Exist | %s ' % tableName) - if self.db_api_module_name in ["cx_Oracle"]: - selectStatement = ("SELECT * FROM all_objects WHERE object_type IN ('TABLE','VIEW') AND owner = SYS_CONTEXT('USERENV', 'SESSION_USER') AND object_name = UPPER('%s')" % tableName) - elif self.db_api_module_name in ["sqlite3"]: - selectStatement = ("SELECT name FROM sqlite_master WHERE type='table' AND name='%s' COLLATE NOCASE" % tableName) - elif self.db_api_module_name in ["ibm_db", "ibm_db_dbi"]: - selectStatement = ("SELECT name FROM SYSIBM.SYSTABLES WHERE type='T' AND name=UPPER('%s')" % tableName) - elif self.db_api_module_name in ["teradata"]: - selectStatement = ("SELECT TableName FROM DBC.TablesV WHERE TableKind='T' AND TableName='%s'" % tableName) + *The old parameters will be removed in future versions.* + + === Examples === + | Table Must Exist | person | + | Table Must Exist | person | msg=my error message | + | Table Must Exist | person | alias=my_alias | + | Table Must Exist | person | no_transaction=True | + """ + db_connection = self.connection_store.get_connection(alias) + if db_connection.module_name in ["cx_Oracle", "oracledb"]: + query = ( + "SELECT * FROM all_objects WHERE object_type IN ('TABLE','VIEW') AND " + f"owner = SYS_CONTEXT('USERENV', 'SESSION_USER') AND object_name = UPPER('{table_name}')" + ) + table_exists = self.row_count(query, no_transaction=no_transaction, alias=alias) > 0 + elif db_connection.module_name in ["sqlite3"]: + query = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}' COLLATE NOCASE" + table_exists = self.row_count(query, no_transaction=no_transaction, alias=alias) > 0 + elif db_connection.module_name in ["ibm_db", "ibm_db_dbi"]: + query = f"SELECT name FROM SYSIBM.SYSTABLES WHERE type='T' AND name=UPPER('{table_name}')" + table_exists = self.row_count(query, no_transaction=no_transaction, alias=alias) > 0 + elif db_connection.module_name in ["teradata"]: + query = f"SELECT TableName FROM DBC.TablesV WHERE TableKind='T' AND TableName='{table_name}'" + table_exists = self.row_count(query, no_transaction=no_transaction, alias=alias) > 0 else: - selectStatement = ("SELECT * FROM information_schema.tables WHERE table_name='%s'" % tableName) - num_rows = self.row_count(selectStatement, sansTran) - if num_rows == 0: - raise AssertionError("Table '%s' does not exist in the db" % tableName) + try: + query = f"SELECT * FROM information_schema.tables WHERE table_name='{table_name}'" + table_exists = self.row_count(query, no_transaction=no_transaction, alias=alias) > 0 + except: + logger.info("Database doesn't support information schema, try using a simple SQL request") + try: + query = f"SELECT 1 from {table_name} where 1=0" + self.row_count(query, no_transaction=no_transaction, alias=alias) + table_exists = True + except: + table_exists = False + assert table_exists, msg or f"Table '{table_name}' does not exist in the db" diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 3060a451..87926ab5 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -13,191 +13,664 @@ # limitations under the License. import importlib - -try: - import ConfigParser -except: - import configparser as ConfigParser +import os +from configparser import ConfigParser, NoOptionError, NoSectionError +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, Optional from robot.api import logger +from .params_decorator import renamed_args + + +@dataclass +class Connection: + client: Any + module_name: str + omit_trailing_semicolon: bool + + +class ConnectionStore: + def __init__(self, warn_on_overwrite=True): + self._connections: Dict[str, Connection] = {} + self.default_alias: str = "default" + self.warn_on_overwrite = warn_on_overwrite + + def register_connection(self, client: Any, module_name: str, alias: str, omit_trailing_semicolon=False): + if alias in self._connections and self.warn_on_overwrite: + if alias == self.default_alias: + logger.warn("Overwriting not closed connection.") + else: + logger.warn(f"Overwriting not closed connection for alias = '{alias}'") + self._connections[alias] = Connection(client, module_name, omit_trailing_semicolon) + + def get_connection(self, alias: Optional[str]) -> Connection: + """ + Return connection with given alias. + + If alias is not provided, it will return default connection. + If there is no default connection, it will return last opened connection. + """ + if not self._connections: + raise ValueError(f"No database connection is open.") + if not alias: + if self.default_alias in self._connections: + return self._connections[self.default_alias] + return list(self._connections.values())[-1] + if alias not in self._connections: + raise ValueError(f"Alias '{alias}' not found in existing connections.") + return self._connections[alias] + + def pop_connection(self, alias: Optional[str]) -> Connection: + if not self._connections: + return None + if not alias: + alias = self.default_alias + if alias not in self._connections: + alias = list(self._connections.keys())[-1] + return self._connections.pop(alias, None) + + def clear(self): + self._connections = {} + + def switch(self, alias: str): + if alias not in self._connections: + raise ValueError(f"Alias '{alias}' not found in existing connections.") + self.default_alias = alias -class ConnectionManager(object): + def __iter__(self): + return iter(self._connections.values()) + + +class ConfigReader: + def __init__(self, config_file: Optional[str], alias: str): + if config_file is None: + config_file = "./resources/db.cfg" + self.alias = alias + self.config = self._load_config(config_file) + + @staticmethod + def _load_config(config_file: str) -> Optional[ConfigParser]: + config_path = Path(config_file) + logger.info(f"Looking for configuration file: '{config_path}'") + if not config_path.exists(): + logger.info("Configuration file doesn't exist") + return None + config = ConfigParser() + config.read([config_path]) + logger.info("Successfully loaded configuration file") + return config + + def pop(self, param: str) -> Optional[str]: + """ + Returns the `param` value read from the config file and deletes it from the list of all params read + """ + if self.config is None: + logger.debug("Configuration file not loaded") + return None + try: + logger.debug(f"Looking for parameter '{param}' in configuration file") + param_value = self.config.get(self.alias, param) + logger.info(f"Found parameter '{param}' in configuration file") + self.config.remove_option(self.alias, param) + return param_value + except NoSectionError: + logger.debug(f"Configuration file does not have [{self.alias}] section.") + except NoOptionError: + logger.debug(f"Parameter '{param}' missing in configuration file.") + return None + + def get_all_available_params(self) -> Dict: + """ + Returns a dictionary of all params read from the config file, which are currently available + (some of them might have been removed using the `pop` function) + """ + if self.config is None: + logger.debug("Configuration file not loaded") + return {} + try: + all_options = dict(self.config.items(self.alias)) + return all_options + except NoSectionError: + logger.debug(f"Configuration file does not have [{self.alias}] section.") + return {} + + +class ConnectionManager: """ Connection Manager handles the connection & disconnection to the database. """ - def __init__(self): - """ - Initializes _dbconnection to None. - """ - self._dbconnection = None - self.db_api_module_name = None + def __init__(self, warn_on_connection_overwrite=True): + self.connection_store: ConnectionStore = ConnectionStore(warn_on_overwrite=warn_on_connection_overwrite) + self.ibmdb_driver_already_added_to_path: bool = False + + @staticmethod + def _hide_password_values(string_with_pass, params_separator=","): + string_with_hidden_pass = string_with_pass + for pass_param_name in ["pass", "passwd", "password", "pwd", "PWD"]: + pass_param_name += "=" + splitted = string_with_hidden_pass.split(pass_param_name) + if len(splitted) < 2: + continue + splitted = splitted[1].split(params_separator) + value_to_hide = splitted[0] + string_with_hidden_pass = string_with_hidden_pass.replace( + f"{pass_param_name}{value_to_hide}", f"{pass_param_name}***" + ) + return string_with_hidden_pass - def connect_to_database(self, dbapiModuleName=None, dbName=None, dbUsername=None, dbPassword=None, dbHost=None, dbPort=None, dbCharset=None, dbDriver=None, dbConfigFile="./resources/db.cfg"): + @renamed_args( + mapping={ + "dbapiModuleName": "db_module", + "dbName": "db_name", + "dbUsername": "db_user", + "dbPassword": "db_password", + "dbHost": "db_host", + "dbPort": "db_port", + "dbCharset": "db_charset", + "dbDriver": "odbc_driver", + "dbConfigFile": "config_file", + "driverMode": "oracle_driver_mode", + } + ) + def connect_to_database( + self, + db_module: Optional[str] = None, + db_name: Optional[str] = None, + db_user: Optional[str] = None, + db_password: Optional[str] = None, + db_host: Optional[str] = None, + db_port: Optional[int] = None, + db_charset: Optional[str] = None, + odbc_driver: Optional[str] = None, + config_file: Optional[str] = None, + oracle_driver_mode: Optional[str] = None, + alias: str = "default", + **custom_connection_params, + ): """ - Loads the DB API 2.0 module given `dbapiModuleName` then uses it to - connect to the database using `dbName`, `dbUsername`, and `dbPassword`. + Creates a database connection using the DB API 2.0 ``db_module`` and the parameters provided. + Along with listed commonly used arguments (`db_name`, `db_host` etc.) + you can set any other DB module specific parameters as key/value pairs. - Optionally, you can specify a `dbConfigFile` wherein it will load the - default property values for `dbapiModuleName`, `dbName` `dbUsername` - and `dbPassword` (note: specifying `dbapiModuleName`, `dbName` - `dbUsername` or `dbPassword` directly will override the properties of - the same key in `dbConfigFile`). If no `dbConfigFile` is specified, it - defaults to `./resources/db.cfg`. + Use ``config_file`` to provide a path to configuration file with connection parameters + to be used along with / instead of keyword arguments. + If no specified, it defaults to `./resources/db.cfg`. + See `Using configuration file` for more details. - The `dbConfigFile` is useful if you don't want to check into your SCM - your database credentials. + All params are optional, although ``db_module`` must be set - either as keyword argument or in config file. + If some of the listed keyword arguments (`db_name`, `db_host` etc.) are not provided (i.e. left on default value `None`), + they are normally not passed to the Python DB module at all, except: + - _db_port_ - commonly used port number for known databases is set as fallback + - _db_charset_ - _UTF8_ is used as fallback for _pymysql_, _pymssql_ and _pyodbc_ + - _oracle_driver_mode_ - _thin_ is used as fallback for _oracledb_ - Example db.cfg file - | [default] - | dbapiModuleName=pymysqlforexample - | dbName=yourdbname - | dbUsername=yourusername - | dbPassword=yourpassword - | dbHost=yourhost - | dbPort=yourport + Other custom params from keyword arguments and config file are passed to the Python DB module as provided - + normally as arguments for the _connect()_ function. + However, when using *pyodbc* or *ibm_db_dbi*, the connection is established using a *connection string* - + so all the custom params are added into it instead of function arguments. - Example usage: - | # explicitly specifies all db property values | - | Connect To Database | psycopg2 | my_db | postgres | s3cr3t | tiger.foobar.com | 5432 | + Set ``alias`` for `Handling multiple database connections`. + If the same alias is given twice, then previous connection will be overridden. - | # loads all property values from default.cfg | - | Connect To Database | dbConfigFile=default.cfg | + The ``oracle_driver_mode`` is used to select the *oracledb* client mode. + Allowed values are: + - _thin_ (default if omitted) + - _thick_ + - _thick,lib_dir=_ - | # loads all property values from ./resources/db.cfg | - | Connect To Database | + By default, there is a warning when overwriting an existing connection (i.e. not closing it properly). + This can be disabled by setting the ``warn_on_connection_overwrite`` parameter to *False* in the library import. - | # uses explicit `dbapiModuleName` and `dbName` but uses the `dbUsername` and `dbPassword` in 'default.cfg' | - | Connect To Database | psycopg2 | my_db_test | dbConfigFile=default.cfg | + === Some parameters were renamed in version 2.0 === + The old parameters ``dbapiModuleName``, ``dbName``, ``dbUsername``, + ``dbPassword``, ``dbHost``, ``dbPort``, ``dbCharset``, ``dbDriver``, + ``dbConfigFile`` and ``driverMode`` are *deprecated*, + please use new parameters ``db_module``, ``db_name``, ``db_user``, + ``db_password``, ``db_host``, ``db_port``, ``db_charset``, ``odbc_driver``, + ``config_file`` and ``oracle_driver_mode`` instead. - | # uses explicit `dbapiModuleName` and `dbName` but uses the `dbUsername` and `dbPassword` in './resources/db.cfg' | - | Connect To Database | psycopg2 | my_db_test | + *The old parameters will be removed in future versions.* + + == Basic examples == + | Connect To Database | psycopg2 | my_db | user | pass | 127.0.0.1 | 5432 | + | Connect To Database | psycopg2 | my_db | user | pass | 127.0.0.1 | 5432 | my_custom_param=value | + | Connect To Database | psycopg2 | my_db | user | pass | 127.0.0.1 | 5432 | alias=my_alias | + | Connect To Database | config_file=my_db_params.cfg | + + See `Connection examples for different DB modules`. """ + config = ConfigReader(config_file, alias) + + def _build_connection_params(custom_params=True, **basic_params): + con_params = basic_params.copy() + for param_name, param_val in basic_params.items(): + if param_val is None: + con_params.pop(param_name, None) + if custom_params: + con_params.update(custom_connection_params) + con_params.update(other_config_file_params) + + return con_params + + def _log_all_connection_params(*, connection_object=None, connection_string=None, **connection_params): + connection_object = connection_object or db_module + msg = f"Connect to DB using : {connection_object}.connect(" + params_separator = "," + if connection_string: + msg += f'"{connection_string}"' + params_separator = ";" + for param_name, param_value in connection_params.items(): + msg += f", {param_name}=" + if isinstance(param_value, str): + msg += f"'{param_value}'" + else: + msg += f"{param_value}" + if db_password: + msg = msg.replace(f"'{db_password}'", "***") + msg = self._hide_password_values(msg, params_separator) + msg = msg.replace("connect(, ", "connect(") + msg += ")" + logger.info(msg) + + def _arg_or_config(arg_value, param_name, *, old_param_name=None, mandatory=False): + val_from_config = config.pop(param_name) + + # support deprecated old param names + if val_from_config is None and old_param_name is not None: + val_from_config = config.pop(old_param_name) + if val_from_config is not None: + logger.warn(f"Config file: argument '{old_param_name}' is deprecated, use '{param_name}' instead") + + if arg_value is not None: + final_value = arg_value + if val_from_config is not None: + logger.info( + f"Parameter '{param_name}' set both as keyword argument and in config file, " + "but keyword arguments take precedence" + ) + else: + final_value = val_from_config + if final_value is None and mandatory: + raise ValueError( + f"Required parameter '{param_name}' was not provided - " + "neither in keyword arguments nor in config file" + ) + return final_value + + # mandatory parameter + db_module = _arg_or_config(db_module, "db_module", mandatory=True, old_param_name="dbapiModuleName") + # optional named params - named because of custom module specific handling + db_name = _arg_or_config(db_name, "db_name", old_param_name="dbName") + db_user = _arg_or_config(db_user, "db_user", old_param_name="dbUsername") + db_password = _arg_or_config(db_password, "db_password", old_param_name="dbPassword") + db_host = _arg_or_config(db_host, "db_host", old_param_name="dbHost") + db_port = _arg_or_config(db_port, "db_port", old_param_name="dbPort") + if db_port is not None: + db_port = int(db_port) + db_charset = _arg_or_config(db_charset, "db_charset", old_param_name="dbCharset") + odbc_driver = _arg_or_config(odbc_driver, "odbc_driver", old_param_name="dbDriver") + oracle_driver_mode = _arg_or_config(oracle_driver_mode, "oracle_driver_mode", old_param_name="driverMode") - config = ConfigParser.ConfigParser() - config.read([dbConfigFile]) + for param_name, param_value in custom_connection_params.items(): + _arg_or_config(param_value, param_name) + other_config_file_params = config.get_all_available_params() + if other_config_file_params: + logger.info(f"Other params from configuration file: {list(other_config_file_params.keys())}") - dbapiModuleName = dbapiModuleName or config.get('default', 'dbapiModuleName') - dbName = dbName or config.get('default', 'dbName') - dbUsername = dbUsername or config.get('default', 'dbUsername') - dbPassword = dbPassword if dbPassword is not None else config.get('default', 'dbPassword') - dbHost = dbHost or config.get('default', 'dbHost') or 'localhost' - dbPort = int(dbPort or config.get('default', 'dbPort')) + omit_trailing_semicolon = False - if dbapiModuleName == "excel" or dbapiModuleName == "excelrw": - self.db_api_module_name = "pyodbc" - db_api_2 = importlib.import_module("pyodbc") + if db_module == "excel" or db_module == "excelrw": + db_api_module_name = "pyodbc" else: - self.db_api_module_name = dbapiModuleName - db_api_2 = importlib.import_module(dbapiModuleName) - if dbapiModuleName in ["MySQLdb", "pymysql"]: - dbPort = dbPort or 3306 - logger.info('Connecting using : %s.connect(db=%s, user=%s, passwd=%s, host=%s, port=%s, charset=%s) ' % (dbapiModuleName, dbName, dbUsername, dbPassword, dbHost, dbPort, dbCharset)) - self._dbconnection = db_api_2.connect(db=dbName, user=dbUsername, passwd=dbPassword, host=dbHost, port=dbPort, charset=dbCharset) - elif dbapiModuleName in ["psycopg2"]: - dbPort = dbPort or 5432 - logger.info('Connecting using : %s.connect(database=%s, user=%s, password=%s, host=%s, port=%s) ' % (dbapiModuleName, dbName, dbUsername, dbPassword, dbHost, dbPort)) - self._dbconnection = db_api_2.connect(database=dbName, user=dbUsername, password=dbPassword, host=dbHost, port=dbPort) - elif dbapiModuleName in ["pyodbc", "pypyodbc"]: - dbPort = dbPort or 1433 - dbDriver = dbDriver or "{SQL Server}" - logger.info('Connecting using : %s.connect(DRIVER=%s;SERVER=%s,%s;DATABASE=%s;UID=%s;PWD=%s)' % (dbapiModuleName, dbDriver, dbHost, dbPort, dbName, dbUsername, dbPassword)) - self._dbconnection = db_api_2.connect('DRIVER=%s;SERVER=%s,%s;DATABASE=%s;UID=%s;PWD=%s' % ( dbDriver, dbHost, dbPort, dbName, dbUsername, dbPassword)) - elif dbapiModuleName in ["excel"]: - logger.info( - 'Connecting using : %s.connect(DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};DBQ=%s;ReadOnly=1;Extended Properties="Excel 8.0;HDR=YES";)' % ( - dbapiModuleName, dbName)) - self._dbconnection = db_api_2.connect( - 'DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};DBQ=%s;ReadOnly=1;Extended Properties="Excel 8.0;HDR=YES";)' % ( - dbName), autocommit=True) - elif dbapiModuleName in ["excelrw"]: - logger.info( - 'Connecting using : %s.connect(DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};DBQ=%s;ReadOnly=0;Extended Properties="Excel 8.0;HDR=YES";)' % ( - dbapiModuleName, dbName)) - self._dbconnection = db_api_2.connect( - 'DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};DBQ=%s;ReadOnly=0;Extended Properties="Excel 8.0;HDR=YES";)' % ( - dbName), autocommit=True) - elif dbapiModuleName in ["ibm_db", "ibm_db_dbi"]: - dbPort = dbPort or 50000 - logger.info('Connecting using : %s.connect(DATABASE=%s;HOSTNAME=%s;PORT=%s;PROTOCOL=TCPIP;UID=%s;PWD=%s;) ' % (dbapiModuleName, dbName, dbHost, dbPort, dbUsername, dbPassword)) - self._dbconnection = db_api_2.connect('DATABASE=%s;HOSTNAME=%s;PORT=%s;PROTOCOL=TCPIP;UID=%s;PWD=%s;' % (dbName, dbHost, dbPort, dbUsername, dbPassword), '', '') - elif dbapiModuleName in ["cx_Oracle"]: - dbPort = dbPort or 1521 - oracle_dsn = db_api_2.makedsn(host=dbHost, port=dbPort, service_name=dbName) - logger.info('Connecting using: %s.connect(user=%s, password=%s, dsn=%s) ' % (dbapiModuleName, dbUsername, dbPassword, oracle_dsn)) - self._dbconnection = db_api_2.connect(user=dbUsername, password=dbPassword, dsn=oracle_dsn) - elif dbapiModuleName in ["teradata"]: - dbPort = dbPort or 1025 + db_api_module_name = db_module + + if db_api_module_name in ["ibm_db", "ibm_db_dbi"]: + if os.name == "nt": + if not self.ibmdb_driver_already_added_to_path: + spec = importlib.util.find_spec(db_api_module_name) + if spec is not None: + logger.info( + f"Importing DB module '{db_api_module_name}' on Windows requires configuring the DLL directory for CLI driver" + ) + site_packages_path = os.path.dirname(spec.origin) + clidriver_bin_path = os.path.join(site_packages_path, "clidriver", "bin") + if os.path.exists(clidriver_bin_path): + os.add_dll_directory(clidriver_bin_path) + self.ibmdb_driver_already_added_to_path = True + logger.info(f"Added default CLI driver location to DLL search path: '{clidriver_bin_path}'") + else: + logger.info(f"Default CLI driver location folder not found: '{clidriver_bin_path}'") + + db_api_2 = importlib.import_module(db_api_module_name) + + if db_module in ["MySQLdb", "pymysql"]: + db_port = db_port or 3306 + db_charset = db_charset or "utf8mb4" + con_params = _build_connection_params( + db=db_name, user=db_user, passwd=db_password, host=db_host, port=db_port, charset=db_charset + ) + _log_all_connection_params(**con_params) + db_connection = db_api_2.connect(**con_params) + + elif db_module in ["pymssql"]: + db_port = db_port or 1433 + db_charset = db_charset or "UTF-8" + con_params = _build_connection_params( + database=db_name, user=db_user, password=db_password, host=db_host, port=db_port, charset=db_charset + ) + _log_all_connection_params(**con_params) + db_connection = db_api_2.connect(**con_params) + + elif db_module in ["psycopg2"]: + db_port = db_port or 5432 + con_params = _build_connection_params( + database=db_name, user=db_user, password=db_password, host=db_host, port=db_port + ) + _log_all_connection_params(**con_params) + db_connection = db_api_2.connect(**con_params) + + elif db_module in ["pyodbc", "pypyodbc"]: + db_port = db_port or 1433 + db_charset = db_charset or "utf8mb4" + + if odbc_driver: + con_str = f"DRIVER={odbc_driver};" + else: + con_str = "" + logger.info("No ODBC driver specified") + logger.info(f"List of installed ODBC drivers: {db_api_2.drivers()}") + if db_name: + con_str += f"DATABASE={db_name};" + if db_user: + con_str += f"UID={db_user};" + if db_password: + con_str += f"PWD={db_password};" + if db_charset: + con_str += f"charset={db_charset};" + if db_host and db_port: + con_str_server = f"SERVER={db_host},{db_port};" # default for most databases + if odbc_driver: + driver_lower = odbc_driver.lower() + if "mysql" in driver_lower: + con_str_server = f"SERVER={db_host}:{db_port};" + elif "saphana" in driver_lower or "hdbodbc" in driver_lower or "sap hana" in driver_lower: + con_str_server = f"SERVERNODE={db_host}:{db_port};" + con_str += con_str_server + + for param_name, param_value in custom_connection_params.items(): + con_str += f"{param_name}={param_value};" + + for param_name, param_value in other_config_file_params.items(): + con_str += f"{param_name}={param_value};" + + _log_all_connection_params(connection_string=con_str) + db_connection = db_api_2.connect(con_str) + + elif db_module in ["excel", "excelrw"]: + con_str = f"DRIVER={{Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}};DBQ={db_name};" + con_str += "ReadOnly=" + if db_module == "excel": + con_str += "1;" + elif db_module == "excelrw": + con_str += "0;" + con_str += 'Extended Properties="Excel 8.0;HDR=YES";)' + logger.info(f"Connecting using : {db_api_module_name}.connect({con_str}, autocommit=True)") + db_connection = db_api_2.connect(con_str, autocommit=True) + + elif db_module in ["ibm_db", "ibm_db_dbi"]: + db_port = db_port or 50000 + con_str = "" + if db_name: + con_str += f"DATABASE={db_name};" + if db_user: + con_str += f"UID={db_user};" + if db_password: + con_str += f"PWD={db_password};" + if db_host: + con_str += f"HOSTNAME={db_host};" + if db_port: + con_str += f"PORT={db_port};" + + for param_name, param_value in custom_connection_params.items(): + con_str += f"{param_name}={param_value};" + + for param_name, param_value in other_config_file_params.items(): + con_str += f"{param_name}={param_value};" + + con_params = _build_connection_params(custom_params=False, user="", password="") + _log_all_connection_params(connection_string=con_str, **con_params) + db_connection = db_api_2.connect(con_str, **con_params) + + elif db_module in ["cx_Oracle"]: + db_port = db_port or 1521 + oracle_dsn = db_api_2.makedsn(host=db_host, port=db_port, service_name=db_name) + con_params = _build_connection_params(user=db_user, password=db_password, dsn=oracle_dsn) + _log_all_connection_params(**con_params) + db_connection = db_api_2.connect(**con_params) + omit_trailing_semicolon = True + + elif db_module in ["oracledb"]: + db_port = db_port or 1521 + oracle_driver_mode = oracle_driver_mode or "thin" + oracle_connection_params = db_api_2.ConnectParams(host=db_host, port=db_port, service_name=db_name) + if "thick" in oracle_driver_mode.lower(): + logger.info("Using thick Oracle client mode") + mode_param = oracle_driver_mode.lower().split(",lib_dir=") + if len(mode_param) == 2 and mode_param[0].lower() == "thick": + lib_dir = mode_param[1] + logger.info(f"Oracle client lib dir specified: {lib_dir}") + db_api_2.init_oracle_client(lib_dir=lib_dir) + else: + logger.info("No Oracle client lib dir specified, oracledb will search it in usual places") + db_api_2.init_oracle_client() + oracle_thin_mode = False + elif "thin" in oracle_driver_mode.lower(): + oracle_thin_mode = True + logger.info("Using thin Oracle client mode") + else: + raise ValueError(f"Invalid Oracle client mode provided: {oracle_driver_mode}") + con_params = _build_connection_params(user=db_user, password=db_password, params=oracle_connection_params) + _log_all_connection_params(**con_params) + db_connection = db_api_2.connect(**con_params) + assert db_connection.thin == oracle_thin_mode, ( + f"Expected oracledb to run in thin mode: {oracle_thin_mode}, " + f"but the connection has thin mode: {db_connection.thin}" + ) + omit_trailing_semicolon = True + + elif db_module in ["teradata"]: + db_port = db_port or 1025 teradata_udaExec = db_api_2.UdaExec(appName="RobotFramework", version="1.0", logConsole=False) - logger.info('Connecting using : %s.connect(database=%s, user=%s, password=%s, host=%s, port=%s) ' % (dbapiModuleName, dbName, dbUsername, dbPassword, dbHost, dbPort)) - self._dbconnection = teradata_udaExec.connect( + con_params = _build_connection_params( method="odbc", - system=dbHost, - database=dbName, - username=dbUsername, - password=dbPassword, - host=dbHost, - port=dbPort + system=db_host, + database=db_name, + username=db_user, + password=db_password, + host=db_host, + port=db_port, ) - elif dbapiModuleName in ["ksycopg2"]: - dbPort = dbPort or 54321 - logger.info('Connecting using : %s.connect(database=%s, user=%s, password=%s, host=%s, port=%s) ' % ( - dbapiModuleName, dbName, dbUsername, dbPassword, dbHost, dbPort)) - self._dbconnection = db_api_2.connect(database=dbName, user=dbUsername, password=dbPassword, host=dbHost, - port=dbPort) + _log_all_connection_params(connection_object=f"{db_module}.UdaExec", **con_params) + db_connection = teradata_udaExec.connect(**con_params) + + elif db_module in ["ksycopg2"]: + db_port = db_port or 54321 + con_params = _build_connection_params( + database=db_name, user=db_user, password=db_password, host=db_host, port=db_port + ) + _log_all_connection_params(**con_params) + db_connection = db_api_2.connect(**con_params) + else: - logger.info('Connecting using : %s.connect(database=%s, user=%s, password=%s, host=%s, port=%s) ' % (dbapiModuleName, dbName, dbUsername, dbPassword, dbHost, dbPort)) - self._dbconnection = db_api_2.connect(database=dbName, user=dbUsername, password=dbPassword, host=dbHost, port=dbPort) + con_params = _build_connection_params( + database=db_name, user=db_user, password=db_password, host=db_host, port=db_port + ) + _log_all_connection_params(**con_params) + db_connection = db_api_2.connect(**con_params) + + self.connection_store.register_connection(db_connection, db_api_module_name, alias, omit_trailing_semicolon) - def connect_to_database_using_custom_params(self, dbapiModuleName=None, db_connect_string=''): + @renamed_args(mapping={"dbapiModuleName": "db_module"}) + def connect_to_database_using_custom_params( + self, + db_module: Optional[str] = None, + db_connect_string: str = "", + alias: str = "default", + *, + dbapiModuleName: Optional[str] = None, + ): """ - Loads the DB API 2.0 module given `dbapiModuleName` then uses it to - connect to the database using the map string `db_custom_param_string`. + *DEPRECATED* Use new `Connect To Database` keyword with custom parameters instead. + The deprecated keyword will be removed in future versions. - Example usage: - | # for psycopg2 | - | Connect To Database Using Custom Params | psycopg2 | database='my_db_test', user='postgres', password='s3cr3t', host='tiger.foobar.com', port=5432 | + Loads the DB API 2.0 module given ``db_module`` then uses it to + connect to the database using the map string ``db_connect_string`` + (parsed as a list of named arguments). + + Use `connect_to_database_using_custom_connection_string` for passing + all params in a single connection string or URI. + + === Some parameters were renamed in version 2.0 === + The old parameter ``dbapiModuleName`` is *deprecated*, + please use new parameter ``db_module`` instead. - | # for JayDeBeApi | + *The old parameter will be removed in future versions.* + + === Examples === + | Connect To Database Using Custom Params | psycopg2 | database='my_db_test', user='postgres', password='s3cr3t', host='tiger.foobar.com', port=5432 | | Connect To Database Using Custom Params | jaydebeapi | 'oracle.jdbc.driver.OracleDriver', 'my_db_test', 'system', 's3cr3t' | + | Connect To Database Using Custom Params | oracledb | user="username", password="pass", dsn="localhost/orclpdb" | + | Connect To Database Using Custom Params | sqlite3 | database="./my_database.db", isolation_level=None | + """ + db_api_2 = importlib.import_module(db_module) + db_api_module_name = db_module + db_connect_string = f"db_api_2.connect({db_connect_string})" + logger.info( + f"Executing : Connect To Database Using Custom Params : {db_module}.connect(" + f"{self._hide_password_values(db_connect_string)})" + ) + + db_connection = eval(db_connect_string) + self.connection_store.register_connection(db_connection, db_api_module_name, alias) + + @renamed_args(mapping={"dbapiModuleName": "db_module"}) + def connect_to_database_using_custom_connection_string( + self, + db_module: Optional[str] = None, + db_connect_string: str = "", + alias: str = "default", + *, + dbapiModuleName: Optional[str] = None, + ): """ - db_api_2 = importlib.import_module(dbapiModuleName) + Loads the DB API 2.0 module given ``db_module`` then uses it to + connect to the database using the ``db_connect_string`` + (parsed as single connection string or URI). + + Use `Connect To Database` for passing custom connection params as named arguments. - db_connect_string = 'db_api_2.connect(%s)' % db_connect_string + === Some parameters were renamed in version 2.0 === + The old parameter ``dbapiModuleName`` is *deprecated*, + please use new parameter ``db_module`` instead. - self.db_api_module_name = dbapiModuleName - logger.info('Executing : Connect To Database Using Custom Params : %s.connect(%s) ' % (dbapiModuleName, db_connect_string)) - self._dbconnection = eval(db_connect_string) + *The old parameter will be removed in future versions.* - def disconnect_from_database(self): + Example usage: + | Connect To Database Using Custom Connection String | psycopg2 | postgresql://postgres:s3cr3t@tiger.foobar.com:5432/my_db_test | + | Connect To Database Using Custom Connection String | oracledb | username/pass@localhost:1521/orclpdb | + """ + db_api_2 = importlib.import_module(db_module) + db_api_module_name = db_module + logger.info( + f"Executing : Connect To Database Using Custom Connection String : {db_module}.connect(" + f"'{db_connect_string}')" + ) + db_connection = db_api_2.connect(db_connect_string) + self.connection_store.register_connection(db_connection, db_api_module_name, alias) + + def disconnect_from_database(self, error_if_no_connection: bool = False, alias: Optional[str] = None): """ Disconnects from the database. - For example: - | Disconnect From Database | # disconnects from current connection to the database | + By default, it's not an error if there was no open database connection - + suitable for usage as a teardown. + However, you can enforce it using the ``error_if_no_connection`` parameter. + + Use ``alias`` to specify what connection should be closed if `Handling multiple database connections`. + + === Examples === + | Disconnect From Database | + | Disconnect From Database | alias=postgres | """ - logger.info('Executing : Disconnect From Database') - if self._dbconnection==None: - return 'No open connection to close' + db_connection = self.connection_store.pop_connection(alias) + if db_connection is None: + log_msg = "No open database connection to close" + if error_if_no_connection: + raise ConnectionError(log_msg) from None + logger.info(log_msg) else: - self._dbconnection.close() - - def set_auto_commit(self, autoCommit=True): - """ - Turn the autocommit on the database connection ON or OFF. - - The default behaviour on a newly created database connection is to automatically start a - transaction, which means that database actions that won't work if there is an active - transaction will fail. Common examples of these actions are creating or deleting a database - or database snapshot. By turning on auto commit on the database connection these actions - can be performed. - - Example: - | # Default behaviour, sets auto commit to true + db_connection.client.close() + + def disconnect_from_all_databases(self): + """ + Disconnects from all the databases - + useful when testing with multiple database connections (aliases). + """ + for db_connection in self.connection_store: + db_connection.client.close() + self.connection_store.clear() + + @renamed_args(mapping={"autoCommit": "auto_commit"}) + def set_auto_commit( + self, auto_commit: bool = True, alias: Optional[str] = None, *, autoCommit: Optional[bool] = None + ): + """ + Explicitly sets the autocommit behavior of the database connection to ``auto_commit``. + See `Commit behavior` for details. + + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. + + === Some parameters were renamed in version 2.0 === + The old parameter ``autoCommit`` is *deprecated*, + please use new parameter ``auto_commit`` instead. + + *The old parameter will be removed in future versions.* + + === Examples === | Set Auto Commit - | # Explicitly set the desired state - | Set Auto Commit | False + | Set Auto Commit | False | + | Set Auto Commit | True | alias=postgres | + """ + db_connection = self.connection_store.get_connection(alias) + if db_connection.module_name == "jaydebeapi": + db_connection.client.jconn.setAutoCommit(auto_commit) + elif db_connection.module_name in ["ibm_db", "ibm_db_dbi"]: + raise ValueError(f"Setting autocommit for {db_connection.module_name} is not supported") + else: + db_connection.client.autocommit = auto_commit + + def switch_database(self, alias: str): + """ + Switch the default database connection to ``alias``. + + Examples: + | Switch Database | my_alias | + | Switch Database | alias=my_alias | + """ + self.connection_store.switch(alias) + + def set_omit_trailing_semicolon(self, omit_trailing_semicolon=True, alias: Optional[str] = None): + """ + Set the ``omit_trailing_semicolon`` to control the `Omitting trailing semicolon behavior` for the connection. + + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. + + Examples: + | Set Omit Trailing Semicolon | True | + | Set Omit Trailing Semicolon | False | alias=my_alias | """ - logger.info('Executing : Set Auto Commit') - self._dbconnection.autocommit = autoCommit + db_connection = self.connection_store.get_connection(alias) + db_connection.omit_trailing_semicolon = omit_trailing_semicolon diff --git a/src/DatabaseLibrary/params_decorator.py b/src/DatabaseLibrary/params_decorator.py new file mode 100644 index 00000000..b122b3f9 --- /dev/null +++ b/src/DatabaseLibrary/params_decorator.py @@ -0,0 +1,33 @@ +""" +These decorators are introduced for the transition from old argument naming / positioning to the new one. +""" +from functools import wraps + +from robot.api import logger + + +def renamed_args(mapping): + """ + Decorator to rename arguments and warn users about deprecated argument names. + + :param mapping: Dictionary mapping old argument names to new argument names. + :return: The decorated function with remapped arguments. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + # Check if any old argument names are used + for old_name, new_name in mapping.items(): + if old_name in kwargs: + # Issue a warning to the user + logger.warn(f"Argument '{old_name}' is deprecated, use '{new_name}' instead") + # Move the argument value to the new name + logger.info(f"Replacing '{old_name}' with '{new_name}'") + kwargs[new_name] = kwargs.pop(old_name) + # Call the original function with updated kwargs + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index 289d0482..36492030 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -12,360 +12,827 @@ # See the License for the specific language governing permissions and # limitations under the License. +import importlib +import inspect +import re import sys +from typing import List, Optional, Tuple + from robot.api import logger +from robot.utils.dotdict import DotDict + +from .connection_manager import Connection +from .params_decorator import renamed_args -class Query(object): +class Query: """ Query handles all the querying done by the Database Library. """ - def query(self, selectStatement, sansTran=False, returnAsDict=False): + def __init__(self, log_query_results, log_query_results_head): + self.LOG_QUERY_RESULTS = log_query_results + self.LOG_QUERY_RESULTS_HEAD = log_query_results_head + + @renamed_args( + mapping={"selectStatement": "select_statement", "sansTran": "no_transaction", "returnAsDict": "return_dict"} + ) + def query( + self, + select_statement: str, + no_transaction: bool = False, + return_dict: bool = False, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + *, + selectStatement: Optional[str] = None, + sansTran: Optional[bool] = None, + returnAsDict: Optional[bool] = None, + ): """ - Uses the input `selectStatement` to query for the values that will be returned as a list of tuples. Set optional - input `sansTran` to True to run command without an explicit transaction commit or rollback. - Set optional input `returnAsDict` to True to return values as a list of dictionaries. + Runs a query with the ``select_statement`` and returns the result as list of rows. + The type of row values depends on the database module - + usually they are tuples or tuple-like objects. - Tip: Unless you want to log all column values of the specified rows, - try specifying the column names in your select statements - as much as possible to prevent any unnecessary surprises with schema - changes and to easily see what your [] indexing is trying to retrieve - (i.e. instead of `"select * from my_table"`, try - `"select id, col_1, col_2 from my_table"`). + Set ``no_transaction`` to _True_ to run command without explicit transaction commit or rollback in case of error. + See `Commit behavior` for details. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + Set ``return_dict`` to _True_ to explicitly convert the return values into list of dictionaries. - When you do the following: - | @{queryResults} | Query | SELECT * FROM person | - | Log Many | @{queryResults} | + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. - You will get the following: - [1, 'Franz Allan', 'See'] + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - Also, you can do something like this: - | ${queryResults} | Query | SELECT first_name, last_name FROM person | - | Log | ${queryResults[0][1]}, ${queryResults[0][0]} | + === Some parameters were renamed in version 2.0 === + The old parameters ``selectStatement``, ``sansTran`` and ``returnAsDict`` are *deprecated*, + please use new parameters ``select_statement``, ``no_transaction`` and ``return_dict`` instead. - And get the following - See, Franz Allan + *The old parameters will be removed in future versions.* - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | @{queryResults} | Query | SELECT * FROM person | True | + === Examples === + | ${Results}= | Query | select LAST_NAME from person | + | ${Results}= | Query | select LAST_NAME from person | no_transaction=True | + | ${Results}= | Query | select LAST_NAME from person | return_dict=True | + | ${Results}= | Query | select LAST_NAME from person | alias=postgres | + | @{parameters} | Create List | person | + | ${Results}= | Query | SELECT * FROM %s | parameters=${parameters} | """ + db_connection = self.connection_store.get_connection(alias) cur = None try: - cur = self._dbconnection.cursor() - logger.info('Executing : Query | %s ' % selectStatement) - self.__execute_sql(cur, selectStatement) - allRows = cur.fetchall() - - if returnAsDict: - mappedRows = [] - col_names = [c[0] for c in cur.description] - - for rowIdx in range(len(allRows)): - d = {} - for colIdx in range(len(allRows[rowIdx])): - d[col_names[colIdx]] = allRows[rowIdx][colIdx] - mappedRows.append(d) - return mappedRows - - return allRows - finally: - if cur: - if not sansTran: - self._dbconnection.rollback() - - def row_count(self, selectStatement, sansTran=False): + cur = db_connection.client.cursor() + self._execute_sql( + cur, + select_statement, + parameters=parameters, + omit_trailing_semicolon=db_connection.omit_trailing_semicolon, + ) + all_rows = cur.fetchall() + self._commit_if_needed(db_connection, no_transaction) + col_names = [c[0] for c in cur.description] + self._log_query_results(col_names, all_rows) + if return_dict: + return [DotDict(zip(col_names, row)) for row in all_rows] + return all_rows + except Exception as e: + self._rollback_and_raise(db_connection, no_transaction, e) + + @renamed_args(mapping={"selectStatement": "select_statement", "sansTran": "no_transaction"}) + def row_count( + self, + select_statement: str, + no_transaction: bool = False, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + *, + selectStatement: Optional[str] = None, + sansTran: Optional[bool] = None, + ): """ - Uses the input `selectStatement` to query the database and returns the number of rows from the query. Set - optional input `sansTran` to True to run command without an explicit transaction commit or rollback. + Runs a query with the ``select_statement`` and returns the number of rows in the result. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | - | 2 | Jerry | Schneider | + Set ``no_transaction`` to _True_ to run command without explicit transaction commit or rollback in case of error. + See `Commit behavior` for details. - When you do the following: - | ${rowCount} | Row Count | SELECT * FROM person | - | Log | ${rowCount} | + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. - You will get the following: - 2 + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - Also, you can do something like this: - | ${rowCount} | Row Count | SELECT * FROM person WHERE id = 2 | - | Log | ${rowCount} | + === Some parameters were renamed in version 2.0 === + The old parameters ``selectStatement`` and ``sansTran`` are *deprecated*, + please use new parameters ``select_statement`` and ``no_transaction`` instead. - And get the following - 1 + *The old parameters will be removed in future versions.* - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | ${rowCount} | Row Count | SELECT * FROM person | True | + === Examples === + | ${Rows}= | Row Count | select LAST_NAME from person | + | ${Rows}= | Row Count | select LAST_NAME from person | no_transaction=True | + | ${Rows}= | Row Count | select LAST_NAME from person | alias=postgres | + | @{parameters} | Create List | person | + | ${Rows}= | Row Count | SELECT * FROM %s | parameters=${parameters} | """ + db_connection = self.connection_store.get_connection(alias) cur = None try: - cur = self._dbconnection.cursor() - logger.info('Executing : Row Count | %s ' % selectStatement) - self.__execute_sql(cur, selectStatement) + cur = db_connection.client.cursor() + self._execute_sql( + cur, + select_statement, + parameters=parameters, + omit_trailing_semicolon=db_connection.omit_trailing_semicolon, + ) data = cur.fetchall() - if self.db_api_module_name in ["sqlite3", "ibm_db", "ibm_db_dbi", "pyodbc"]: - rowCount = len(data) + self._commit_if_needed(db_connection, no_transaction) + col_names = [c[0] for c in cur.description] + if db_connection.module_name in ["sqlite3", "ibm_db", "ibm_db_dbi", "pyodbc", "jaydebeapi"]: + current_row_count = len(data) else: - rowCount = cur.rowcount - return rowCount - finally: - if cur: - if not sansTran: - self._dbconnection.rollback() - - def description(self, selectStatement, sansTran=False): + current_row_count = cur.rowcount + logger.info(f"Retrieved {current_row_count} rows") + self._log_query_results(col_names, data) + return current_row_count + except Exception as e: + self._rollback_and_raise(db_connection, no_transaction, e) + + @renamed_args(mapping={"selectStatement": "select_statement", "sansTran": "no_transaction"}) + def description( + self, + select_statement: str, + no_transaction: bool = False, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + *, + selectStatement: Optional[str] = None, + sansTran: Optional[bool] = None, + ): """ - Uses the input `selectStatement` to query a table in the db which will be used to determine the description. Set - optional input `sansTran` to True to run command without an explicit transaction commit or rollback. + Runs a query with the ``select_statement`` to determine the table description. + + Set ``no_transaction`` to _True_ to run command without explicit transaction commit or rollback in case of error. + See `Commit behavior` for details. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. - When you do the following: - | @{queryResults} | Description | SELECT * FROM person | - | Log Many | @{queryResults} | + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - You will get the following: - [Column(name='id', type_code=1043, display_size=None, internal_size=255, precision=None, scale=None, null_ok=None)] - [Column(name='first_name', type_code=1043, display_size=None, internal_size=255, precision=None, scale=None, null_ok=None)] - [Column(name='last_name', type_code=1043, display_size=None, internal_size=255, precision=None, scale=None, null_ok=None)] + === Some parameters were renamed in version 2.0 === + The old parameters ``selectStatement`` and ``sansTran`` are *deprecated*, + please use new parameters ``select_statement`` and ``no_transaction`` instead. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | @{queryResults} | Description | SELECT * FROM person | True | + *The old parameters will be removed in future versions.* + + === Examples === + | ${Person table description}= | Description | select LAST_NAME from person | + | ${Person table description}= | Description | select LAST_NAME from person | no_transaction=True | + | ${Person table description}= | Description | select LAST_NAME from person | alias=postgres | + | @{parameters} | Create List | person | + | ${Person table description}= | Description | SELECT * FROM %s | parameters=${parameters} | """ + db_connection = self.connection_store.get_connection(alias) cur = None try: - cur = self._dbconnection.cursor() - logger.info('Executing : Description | %s ' % selectStatement) - self.__execute_sql(cur, selectStatement) + cur = db_connection.client.cursor() + self._execute_sql( + cur, + select_statement, + parameters=parameters, + omit_trailing_semicolon=db_connection.omit_trailing_semicolon, + ) + self._commit_if_needed(db_connection, no_transaction) description = list(cur.description) if sys.version_info[0] < 3: for row in range(0, len(description)): - description[row] = (description[row][0].encode('utf-8'),) + description[row][1:] + description[row] = (description[row][0].encode("utf-8"),) + description[row][1:] return description - finally: - if cur: - if not sansTran: - self._dbconnection.rollback() - - def delete_all_rows_from_table(self, tableName, sansTran=False): + except Exception as e: + self._rollback_and_raise(db_connection, no_transaction, e) + + @renamed_args(mapping={"tableName": "table_name", "sansTran": "no_transaction"}) + def delete_all_rows_from_table( + self, + table_name: str, + no_transaction: bool = False, + alias: Optional[str] = None, + *, + tableName: Optional[str] = None, + sansTran: Optional[bool] = None, + ): """ - Delete all the rows within a given table. Set optional input `sansTran` to True to run command without an - explicit transaction commit or rollback. + Deletes all rows from table with ``table_name``. - For example, given we have a table `person` in a database + Set ``no_transaction`` to _True_ to run command without explicit transaction commit + or rollback in case of error. + See `Commit behavior` for details. - When you do the following: - | Delete All Rows From Table | person | + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. + + === Some parameters were renamed in version 2.0 === + The old parameters ``tableName`` and ``sansTran`` are *deprecated*, + please use new parameters ``table_name`` and ``no_transaction`` instead. - If all the rows can be successfully deleted, then you will get: - | Delete All Rows From Table | person | # PASS | - If the table doesn't exist or all the data can't be deleted, then you - will get: - | Delete All Rows From Table | first_name | # FAIL | + *The old parameters will be removed in future versions.* - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Delete All Rows From Table | person | True | + === Examples === + | Delete All Rows From Table | person | + | Delete All Rows From Table | person | no_transaction=True | + | Delete All Rows From Table | person | alias=my_alias | """ + db_connection = self.connection_store.get_connection(alias) cur = None - selectStatement = ("DELETE FROM %s;" % tableName) + query = f"DELETE FROM {table_name}" try: - cur = self._dbconnection.cursor() - logger.info('Executing : Delete All Rows From Table | %s ' % selectStatement) - result = self.__execute_sql(cur, selectStatement) + cur = db_connection.client.cursor() + result = self._execute_sql(cur, query) + self._commit_if_needed(db_connection, no_transaction) if result is not None: - if not sansTran: - self._dbconnection.commit() return result - if not sansTran: - self._dbconnection.commit() - finally: - if cur: - if not sansTran: - self._dbconnection.rollback() - - def execute_sql_script(self, sqlScriptFileName, sansTran=False): + except Exception as e: + self._rollback_and_raise(db_connection, no_transaction, e) + + @renamed_args(mapping={"sqlScriptFileName": "script_path", "sansTran": "no_transaction"}) + def execute_sql_script( + self, + script_path: str, + no_transaction: bool = False, + alias: Optional[str] = None, + split: bool = True, + *, + sqlScriptFileName: Optional[str] = None, + sansTran: Optional[bool] = None, + ): """ - Executes the content of the `sqlScriptFileName` as SQL commands. Useful for setting the database to a known - state before running your tests, or clearing out your test data after running each a test. Set optional input - `sansTran` to True to run command without an explicit transaction commit or rollback. - - Sample usage : - | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | - | Execute Sql Script | ${EXECDIR}${/}resources${/}DML-setup.sql | - | #interesting stuff here | - | Execute Sql Script | ${EXECDIR}${/}resources${/}DML-teardown.sql | - | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-teardown.sql | - - SQL commands are expected to be delimited by a semi-colon (';'). - - For example: - DELETE FROM person_employee_table; - DELETE FROM person_table; - DELETE FROM employee_table; - - Also, the last SQL command can optionally omit its trailing semi-colon. - - For example: - DELETE FROM person_employee_table; - DELETE FROM person_table; - DELETE FROM employee_table - - Given this, that means you can create spread your SQL commands in several - lines. - - For example: - DELETE - FROM person_employee_table; - DELETE - FROM person_table; - DELETE - FROM employee_table - - However, lines that starts with a number sign (`#`) are treated as a - commented line. Thus, none of the contents of that line will be executed. - - For example: - # Delete the bridging table first... - DELETE - FROM person_employee_table; - # ...and then the bridged tables. - DELETE - FROM person_table; - DELETE - FROM employee_table - - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | True | - """ - sqlScriptFile = open(sqlScriptFileName ,encoding='UTF-8') + Executes the content of the SQL script file loaded from `script_path` as SQL commands. - cur = None - try: - cur = self._dbconnection.cursor() - logger.info('Executing : Execute SQL Script | %s ' % sqlScriptFileName) - sqlStatement = '' - for line in sqlScriptFile: - PY3K = sys.version_info >= (3, 0) - if not PY3K: - #spName = spName.encode('ascii', 'ignore') - line = line.strip().decode("utf-8") - if line.startswith('#'): - continue - elif line.startswith('--'): - continue - - sqlFragments = line.split(';') - if len(sqlFragments) == 1: - sqlStatement += line + ' ' - else: - for sqlFragment in sqlFragments: - sqlFragment = sqlFragment.strip() - if len(sqlFragment) == 0: - continue + SQL commands are expected to be delimited by a semicolon (';') - they will be split and executed separately. + Set ``split`` to _False_ to disable this behavior - in this case the entire script content + will be passed to the database module for execution as a single command. - sqlStatement += sqlFragment + ' ' + Set ``no_transaction`` to _True_ to run command without explicit transaction commit + or rollback in case of error. + See `Commit behavior` for details. - self.__execute_sql(cur, sqlStatement) - sqlStatement = '' + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. - sqlStatement = sqlStatement.strip() - if len(sqlStatement) != 0: - self.__execute_sql(cur, sqlStatement) + === Some parameters were renamed in version 2.0 === + The old parameters ``sqlScriptFileName`` and ``sansTran`` are *deprecated*, + please use new parameters ``script_path`` and ``no_transaction`` instead. - if not sansTran: - self._dbconnection.commit() - finally: - if cur: - if not sansTran: - self._dbconnection.rollback() + *The old parameters will be removed in future versions.* - def execute_sql_string(self, sqlString, sansTran=False): + === Examples === + | Execute SQL Script | insert_data_in_person_table.sql | + | Execute SQL Script | insert_data_in_person_table.sql | no_transaction=True | + | Execute SQL Script | insert_data_in_person_table.sql | alias=postgres | + | Execute SQL Script | insert_data_in_person_table.sql | split=False | """ - Executes the sqlString as SQL commands. Useful to pass arguments to your sql. Set optional input `sansTran` to - True to run command without an explicit transaction commit or rollback. + db_connection = self.connection_store.get_connection(alias) + with open(script_path, encoding="UTF-8") as sql_file: + cur = None + try: + cur = db_connection.client.cursor() + if not split: + logger.info("Statements splitting disabled - pass entire script content to the database module") + self._execute_sql( + cur, + sql_file.read(), + omit_trailing_semicolon=db_connection.omit_trailing_semicolon, + ) + else: + logger.info("Splitting script file into statements...") + statements_to_execute = [] + current_statement = "" + inside_statements_group = False + proc_start_pattern = re.compile("create( or replace)? (procedure|function){1}( )?") + proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?") + for line in sql_file: + line = line.strip() + if line.startswith("#") or line.startswith("--") or line == "/": + continue - SQL commands are expected to be delimited by a semi-colon (';'). + # check if the line matches the creating procedure regexp pattern + if proc_start_pattern.match(line.lower()): + inside_statements_group = True + elif line.lower().startswith("begin"): + inside_statements_group = True + + # semicolons inside the line? use them to separate statements + # ... but not if they are inside a begin/end block (aka. statements group) + sqlFragments = line.split(";") + # no semicolons + if len(sqlFragments) == 1: + current_statement += line + " " + continue + quotes = 0 + # "select * from person;" -> ["select..", ""] + for sqlFragment in sqlFragments: + if len(sqlFragment.strip()) == 0: + continue + + if inside_statements_group: + # if statements inside a begin/end block have semicolns, + # they must persist - even with oracle + sqlFragment += "; " + + if proc_end_pattern.match(sqlFragment.lower()): + inside_statements_group = False + elif proc_start_pattern.match(sqlFragment.lower()): + inside_statements_group = True + elif sqlFragment.lower().startswith("begin"): + inside_statements_group = True + + # check if the semicolon is a part of the value (quoted string) + quotes += sqlFragment.count("'") + quotes -= sqlFragment.count("\\'") + quotes -= sqlFragment.count("''") + inside_quoted_string = quotes % 2 != 0 + if inside_quoted_string: + sqlFragment += ";" # restore the semicolon + + current_statement += sqlFragment + if not inside_statements_group and not inside_quoted_string: + statements_to_execute.append(current_statement.strip()) + current_statement = "" + quotes = 0 + + current_statement = current_statement.strip() + if len(current_statement) != 0: + statements_to_execute.append(current_statement) + + for statement in statements_to_execute: + line_ends_with_proc_end = re.compile(r"(\s|;)" + proc_end_pattern.pattern + "$") + omit_semicolon = not line_ends_with_proc_end.search(statement.lower()) + self._execute_sql(cur, statement, omit_semicolon) + self._commit_if_needed(db_connection, no_transaction) + except Exception as e: + self._rollback_and_raise(db_connection, no_transaction, e) + + @renamed_args( + mapping={ + "sqlString": "sql_string", + "sansTran": "no_transaction", + "omitTrailingSemicolon": "omit_trailing_semicolon", + } + ) + def execute_sql_string( + self, + sql_string: str, + no_transaction: bool = False, + alias: Optional[str] = None, + parameters: Optional[Tuple] = None, + omit_trailing_semicolon: Optional[bool] = None, + *, + sqlString: Optional[str] = None, + sansTran: Optional[bool] = None, + omitTrailingSemicolon: Optional[bool] = None, + ): + """ + Executes the ``sql_string`` as a single SQL command. - For example: - | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | + Set ``no_transaction`` to _True_ to run command without explicit transaction commit + or rollback in case of error. + See `Commit behavior` for details. - For example with an argument: - | Execute Sql String | SELECT * FROM person WHERE first_name = ${FIRSTNAME} | + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | True | - """ - cur = None - try: - cur = self._dbconnection.cursor() - logger.info('Executing : Execute SQL String | %s ' % sqlString) - self.__execute_sql(cur, sqlString) - if not sansTran: - self._dbconnection.commit() - finally: - if cur: - if not sansTran: - self._dbconnection.rollback() - - def call_stored_procedure(self, spName, spParams=None, sansTran=False): - """ - Uses the inputs of `spName` and 'spParams' to call a stored procedure. Set optional input `sansTran` to - True to run command without an explicit transaction commit or rollback. - - spName should be the stored procedure name itself - spParams [Optional] should be a List of the parameters being sent in. The list can be one or multiple items. + Use ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client). - The return from this keyword will always be a list. + Set the ``omit_trailing_semicolon`` to explicitly control the `Omitting trailing semicolon behavior` for the command. - Example: - | @{ParamList} = | Create List | FirstParam | SecondParam | ThirdParam | - | @{QueryResults} = | Call Stored Procedure | DBName.SchemaName.StoredProcName | List of Parameters | + === Some parameters were renamed in version 2.0 === + The old parameters ``sqlString``, ``sansTran`` and ``omitTrailingSemicolon`` are *deprecated*, + please use new parameters ``sql_string``, ``no_transaction`` and ``omit_trailing_semicolon`` instead. - Example: - | @{ParamList} = | Create List | Testing | LastName | - | Set Test Variable | ${SPName} = | DBTest.DBSchema.MyStoredProc | - | @{QueryResults} = | Call Stored Procedure | ${SPName} | ${ParamList} | - | Log List | @{QueryResults} | + *The old parameters will be removed in future versions.* - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | @{QueryResults} = | Call Stored Procedure | DBName.SchemaName.StoredProcName | List of Parameters | True | + === Examples === + | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | + | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | no_transaction=True | + | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | alias=my_alias | + | Execute Sql String | CREATE PROCEDURE proc AS BEGIN DBMS_OUTPUT.PUT_LINE('Hello!'); END; | omit_trailing_semicolon=False | + | @{parameters} | Create List | person_employee_table | + | Execute Sql String | DELETE FROM %s | parameters=${parameters} | + """ + db_connection = self.connection_store.get_connection(alias) + cur = None + try: + cur = db_connection.client.cursor() + if omit_trailing_semicolon is None: + omit_trailing_semicolon = db_connection.omit_trailing_semicolon + self._execute_sql(cur, sql_string, omit_trailing_semicolon=omit_trailing_semicolon, parameters=parameters) + self._commit_if_needed(db_connection, no_transaction) + except Exception as e: + self._rollback_and_raise(db_connection, no_transaction, e) + + @renamed_args(mapping={"spName": "procedure_name", "spParams": "procedure_params", "sansTran": "no_transaction"}) + def call_stored_procedure( + self, + procedure_name: str, + procedure_params: Optional[List] = None, + no_transaction: bool = False, + alias: Optional[str] = None, + additional_output_params: Optional[List] = None, + *, + spName: Optional[str] = None, + spParams: Optional[List] = None, + sansTran: Optional[bool] = None, + ): + """ + Calls a stored procedure `procedure_name` with the `procedure_params` - a *list* of parameters the procedure requires. + *Returns two lists* - the _parameter values_ and the _result sets_. + + Use the special *CURSOR* value for OUT params, which should receive result sets - relevant only for some databases (e.g. Oracle or PostgreSQL). + + Set ``no_transaction`` to _True_ to run command without explicit transaction commit + or rollback in case of error. + See `Commit behavior` for details. + + Use ``alias`` to specify what connection should be used if `Handling multiple database connections`. + + Use the ``additional_output_params`` list for OUT params of a procedure in MSSQL. + + === Some parameters were renamed in version 2.0 === + The old parameters ``spName``, ``spParams`` and ``sansTran`` are *deprecated*, please use + new parameters ``procedure_name``, ``procedure_params`` and ``no_transaction`` instead. + + *The old parameters will be removed in future versions.* + + = Handling parameters and result sets = + Handling the input and output parameters and the result sets is very different + depending on the database itself and on the Python database driver - i.e. how it implements the `cursor.callproc()` function. + + == Common case (e.g. MySQL) == + Generally a procedure call requires all parameter values (IN and OUT) put together in a list - `procedure_params`. + + Calling the procedure returns *two lists*: + - *Param values* - the copy of procedure parameters (modified, if the procedure changes the OUT params). The list is empty, if procedures receives no params. + - *Result sets* - the list of lists, each of them containing results of some query, if the procedure returns them. + + == Oracle (oracledb, cx_Oracle) == + Oracle procedures work fine with simple IN and OUT params, but require some special handling of result sets. + + === Simple case with IN and OUT params (no result sets) === + Consider the following procedure: + | CREATE OR REPLACE PROCEDURE + | get_second_name (person_first_name IN VARCHAR, person_second_name OUT VARCHAR) AS + | BEGIN + | SELECT last_name + | INTO person_second_name + | FROM person + | WHERE first_name = person_first_name; + | END; + + Calling the procedure in Robot Framework: + | @{params}= Create List Jerry OUTPUT + | # Second parameter value can be anything, it will be replaced anyway + | + | ${param values} ${result sets}= Call Stored Procedure get_second_name ${params} + | # ${param values} = ['Jerry', 'Schneider'] + | # ${result sets} = [] + + === Oracle procedure returning a result set === + If a procedure in Oracle should return a result set, it must take OUT parameters of a special type - + _SYS_REFCURSOR_. + + Consider the following procedure: + | get_all_second_names (second_names_cursor OUT SYS_REFCURSOR) AS + | BEGIN + | OPEN second_names_cursor for + | SELECT LAST_NAME FROM person; + | END; + + Calling the procedure in Robot Framework requires the special value *CURSOR* for the OUT parameters, + they will be converted to appropriate DB variables before calling the procedure. + | @{params}= Create List CURSOR + | # The parameter must have this special value CURSOR + | + | ${param values} ${result sets}= Call Stored Procedure get_all_second_names ${params} + | # ${param values} = [>] + | # ${result sets} = [[('Franz Allan',), ('Jerry',)], [('See',), ('Schneider',)]] + + === Oracle procedure returning multiple result sets === + If a procedure takes multiple OUT parameters of the _SYS_REFCURSOR_ type, they all must have + the special *CURSOR* value when calling the procedure: + | @{params} = Create List CURSOR CURSOR + | ${param values} ${result sets} = Call Stored Procedure Get_all_first_and_second_names ${params} + | # ${param values} = [>, >] + | # ${result sets} = [[('Franz Allan',), ('Jerry',)], [('See',), ('Schneider',)]] + + == PostgreSQL (psycopg2, psycopg3) == + PostgreSQL doesn't return single values as params, only as result sets. + It also supports special handling of result sets over OUT params of a special type (like Oracle). + + === Simple case with IN and OUT params (no CURSOR parameters) === + Consider the following procedure: + | CREATE FUNCTION + | get_second_name (IN person_first_name VARCHAR(20), + | OUT person_second_name VARCHAR(20)) + | LANGUAGE plpgsql + | AS + | ' + | BEGIN + | SELECT LAST_NAME INTO person_second_name + | FROM person + | WHERE FIRST_NAME = person_first_name; + | END + | '; + + Calling the procedure in Robot Framework: + | @{params}= Create List Jerry + | ${param values} ${result sets}= Call Stored Procedure get_second_name ${params} + | # ${param values} = ['Jerry'] + | # ${result sets} = [[('Schneider',)]] + + === PostgreSQL procedure with CURSOR parameters === + If a procedure in PostgreSQL should return a proper result set, it must take OUT parameters of a special type - + _refcursor_. + + Consider the following procedure: + | CREATE FUNCTION + | get_all_first_and_second_names(result1 refcursor, result2 refcursor) + | RETURNS SETOF refcursor + | LANGUAGE plpgsql + | AS + | ' + | BEGIN + | OPEN result1 FOR SELECT FIRST_NAME FROM person; + | RETURN NEXT result1; + | OPEN result2 FOR SELECT LAST_NAME FROM person; + | RETURN NEXT result2; + | END + | '; + + Calling the procedure in Robot Framework requires the special value *CURSOR* for the OUT parameters, + they will be converted to appropriate DB variables before calling the procedure. + | @{params}= Create List CURSOR CURSOR + | # The parameters must have this special value CURSOR + | + | ${param values} ${result sets}= Call Stored Procedure get_all_first_and_second_names ${params} + | # ${param values} = ['CURSOR_0', 'CURSOR_1'] + | # ${result sets} = [[('Franz Allan',), ('Jerry',)], [('See',), ('Schneider',)] + + == MS SQL Server (pymssql) == + The _pymssql_ driver doesn't natively support getting the OUT parameter values after calling a procedure. + - This requires special handling of OUT parameters using the `additional_output_params` argument. + - Furthermore, it's not possible to fetch the OUT parameter values for a procedure, which returns a result set AND has OUT parameters. + + === Simple case with IN and OUT params (no result sets) === + Consider the following procedure: + | CREATE PROCEDURE + | return_out_param_without_result_sets + | @my_input VARCHAR(20), + | @my_output INT OUTPUT + | AS + | BEGIN + | IF @my_input = 'give me 1' + | BEGIN + | SELECT @my_output = 1; + | END + | ELSE + | BEGIN + | SELECT @my_output = 0; + | END + | END; + + Calling the procedure in Robot Framework requires putting the IN parameters as usual in the `procedure_params` argument, + but the sample values of OUT parameters must be put in the argument `additional_output_params`. + + | @{params}= Create List give me 1 + | @{out_params}= Create List ${9} + | ${param values} ${result sets}= Call Stored Procedure return_out_param_without_result_sets + | ... ${params} additional_output_params=${out_params} + | # ${result sets} = [] + | # ${param values} = ('give me 1', 1) + + The library uses the sample values in the `additional_output_params` list to determine the number and the type + of OUT parameters - so they are type-sensitive, the type must be the same as in the procedure itself. + + === MS SQL procedure returning a result set (no OUT params) === + If a procedure doesn't have any OUT params and returns only result sets, they are handled in a normal way. + Consider the following procedure: + | CREATE PROCEDURE get_all_first_and_second_names + | AS + | BEGIN + | SELECT FIRST_NAME FROM person; + | SELECT LAST_NAME FROM person; + | RETURN; + | END; + + Calling the procedure in Robot Framework: + | ${param values} ${result sets}= Call Stored Procedure get_all_first_and_second_names + | ${param values} = () + | ${result sets} = [[('Franz Allan',), ('Jerry',)], [('See',), ('Schneider',)]] + + === MS SQL procedure returning result sets AND OUT params === + This case is *not fully supported* by the library - the OUT params won't be fetched. """ - if spParams is None: - spParams = [] + db_connection = self.connection_store.get_connection(alias) + if procedure_params is None: + procedure_params = [] + if additional_output_params is None: + additional_output_params = [] cur = None try: - if self.db_api_module_name in ["cx_Oracle"]: - cur = self._dbconnection.cursor() + if db_connection.module_name == "pymssql": + cur = db_connection.client.cursor(as_dict=False) else: - cur = self._dbconnection.cursor(as_dict=False) - PY3K = sys.version_info >= (3, 0) - if not PY3K: - spName = spName.encode('ascii', 'ignore') - logger.info('Executing : Call Stored Procedure | %s | %s ' % (spName, spParams)) - cur.callproc(spName, spParams) - cur.nextset() - retVal=list() - for row in cur: - #logger.info ( ' %s ' % (row)) - retVal.append(row) - if not sansTran: - self._dbconnection.commit() - return retVal - finally: - if cur: - if not sansTran: - self._dbconnection.rollback() - - def __execute_sql(self, cur, sqlStatement): - return cur.execute(sqlStatement) + cur = db_connection.client.cursor() + + param_values = [] + result_sets = [] + + if db_connection.module_name == "pymysql": + cur.callproc(procedure_name, procedure_params) + + # first proceed the result sets if available + result_sets_available = True + while result_sets_available: + result_sets.append(list(cur.fetchall())) + result_sets_available = cur.nextset() + # last result set is always empty + # https://pymysql.readthedocs.io/en/latest/modules/cursors.html#pymysql.cursors.Cursor.callproc + result_sets.pop() + + # now go on with single values - modified input params + for i in range(0, len(procedure_params)): + cur.execute(f"select @_{procedure_name}_{i}") + param_values.append(cur.fetchall()[0][0]) + + elif db_connection.module_name in ["oracledb", "cx_Oracle"]: + # check if "CURSOR" params were passed - they will be replaced + # with cursor variables for storing the result sets + params_substituted = procedure_params.copy() + cursor_params = [] + for i in range(0, len(procedure_params)): + if procedure_params[i] == "CURSOR": + cursor_param = db_connection.client.cursor() + params_substituted[i] = cursor_param + cursor_params.append(cursor_param) + param_values = cur.callproc(procedure_name, params_substituted) + for result_set in cursor_params: + result_sets.append(list(result_set)) + + elif db_connection.module_name in ["psycopg2", "psycopg3"]: + # check if "CURSOR" params were passed - they will be replaced + # with cursor variables for storing the result sets + params_substituted = procedure_params.copy() + cursor_params = [] + for i in range(0, len(procedure_params)): + if procedure_params[i] == "CURSOR": + cursor_param = f"CURSOR_{i}" + params_substituted[i] = cursor_param + cursor_params.append(cursor_param) + param_values = cur.callproc(procedure_name, params_substituted) + if cursor_params: + for cursor_param in cursor_params: + cur.execute(f'FETCH ALL IN "{cursor_param}"') + result_set = cur.fetchall() + result_sets.append(list(result_set)) + else: + if db_connection.module_name in ["psycopg3"]: + result_sets_available = True + while result_sets_available: + result_sets.append(list(cur.fetchall())) + result_sets_available = cur.nextset() + else: + result_set = cur.fetchall() + result_sets.append(list(result_set)) + + else: + if db_connection.module_name == "pymssql": + mssql = importlib.import_module("pymssql") + procedure_params = procedure_params.copy() + for param in additional_output_params: + procedure_params.append(mssql.output(type(param), param)) + + else: + logger.info( + f"Calling a stored procedure for '{db_connection.module_name}'. " + "No special handling is known, so trying the common way with return params and result sets." + ) + + param_values = cur.callproc(procedure_name, procedure_params) + logger.info("Reading the procedure result sets..") + result_sets_available = True + while result_sets_available: + result_set = [] + for row in cur: + result_set.append(row) + if result_set: + result_sets.append(list(result_set)) + if hasattr(cur, "nextset") and inspect.isroutine(cur.nextset): + result_sets_available = cur.nextset() + else: + result_sets_available = False + + self._commit_if_needed(db_connection, no_transaction) + return param_values, result_sets + except Exception as e: + self._rollback_and_raise(db_connection, no_transaction, e) + + def set_logging_query_results(self, enabled: Optional[bool] = None, log_head: Optional[int] = None): + """ + Allows to enable/disable logging of query results and to adjust the log head value. + - Overrides the values, which were set during the library import. + - See `Logging query results` for details. + + Examples: + | Set Logging Query Results | enabled=False | + | Set Logging Query Results | enabled=True | log_head=0 | + | Set Logging Query Results | log_head=10 | + """ + if enabled is not None: + self.LOG_QUERY_RESULTS = enabled + if log_head is not None: + if log_head < 0: + raise ValueError(f"Wrong log head value provided: {log_head}. The value can't be negative!") + self.LOG_QUERY_RESULTS_HEAD = log_head + + def _execute_sql( + self, + cur, + sql_statement: str, + omit_trailing_semicolon: Optional[bool] = False, + parameters: Optional[Tuple] = None, + ): + """ + Runs the `sql_statement` using `cur` as Cursor object. + Use `omit_trailing_semicolon` parameter (bool) for explicit instruction, + if the trailing semicolon (;) should be removed - otherwise the statement + won't be executed by some databases (e.g. Oracle). + Otherwise, it's decided based on the current database module in use. + """ + if omit_trailing_semicolon: + sql_statement = sql_statement.rstrip(";") + if parameters is None: + logger.info(f'Executing sql:
{sql_statement}', html=True) + return cur.execute(sql_statement) + else: + logger.info( + f'Executing sql:
{sql_statement}
Parameters: {parameters}', + html=True, + ) + return cur.execute(sql_statement, parameters) + + def _commit_if_needed(self, db_connection: Connection, no_transaction): + if no_transaction: + logger.info(f"Perform no commit, because 'no_transaction' set to {no_transaction}") + else: + logger.info("Commit the transaction") + db_connection.client.commit() + + def _rollback_and_raise(self, db_connection: Connection, no_transaction, e): + logger.info(f"Error occurred: {e}") + if no_transaction: + logger.info(f"Perform no rollback, because 'no_transaction' set to {no_transaction}") + else: + logger.info("Rollback the transaction") + db_connection.client.rollback() + raise e + + def _log_query_results(self, col_names, result_rows, log_head: Optional[int] = None): + """ + Logs the `result_rows` of a query in RF log as a HTML table. + The `col_names` are needed for the table header. + Max. `log_head` rows are logged (`0` disables the limit). + """ + if not self.LOG_QUERY_RESULTS: + return + + if log_head is None: + log_head = self.LOG_QUERY_RESULTS_HEAD + cell_border_and_align = "border: 1px solid rgb(160 160 160);padding: 8px 10px;text-align: center;" + table_border = "2px solid rgb(140 140 140)" + row_index_background_color = "#d6ecd4" + row_index_text_color = "black" + msg = '
' + msg += f'' + msg += f'' + msg += "" + msg += f'' + for col in col_names: + msg += f'' + msg += "" + table_truncated = False + for i, row in enumerate(result_rows): + if log_head and i >= log_head: + table_truncated = True + break + row_style = "" + if i % 2 == 0: + row_style = ' style="background-color: var(--secondary-color, #eee)"' + msg += f"" + msg += f'' + for cell in row: + msg += f'' + msg += "" + msg += "
Query returned {len(result_rows)} rows
Row{col}
{i}{cell}
" + if table_truncated: + msg += ( + f'

Log limit of {log_head} rows was reached, the table was truncated

' + ) + msg += "
" + logger.info(msg, html=True) diff --git a/src/DatabaseLibrary/version.py b/src/DatabaseLibrary/version.py index 69378709..4260069c 100644 --- a/src/DatabaseLibrary/version.py +++ b/src/DatabaseLibrary/version.py @@ -1 +1 @@ -VERSION = '1.2.4' +VERSION = "2.1.3" diff --git a/test/DB2SQL_DB_Conf.txt b/test/DB2SQL_DB_Conf.txt deleted file mode 100644 index 3a22bea0..00000000 --- a/test/DB2SQL_DB_Conf.txt +++ /dev/null @@ -1,6 +0,0 @@ -*** Variables *** -${DBName} dbname -${DBUser} user -${DBPass} password -${DBHost} host -${DBPort} port diff --git a/test/DB2SQL_DB_Tests.robot b/test/DB2SQL_DB_Tests.robot deleted file mode 100644 index 46e50f5e..00000000 --- a/test/DB2SQL_DB_Tests.robot +++ /dev/null @@ -1,99 +0,0 @@ -*** Settings *** -Suite Setup Connect To Database ibm_db_dbi ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} -Suite Teardown Disconnect From Database -Resource DB2SQL_DB_Conf.txt -Library DatabaseLibrary - -*** Test Cases *** -Create person table - ${output} = Execute SQL String CREATE TABLE person (id decimal(10,0),first_name varchar(30),last_name varchar(30)); - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL Script - Insert Data person table - Comment ${output} = Execute SQL Script ./my_db_test_insertData.sql - ${output} = Execute SQL Script ../test/my_db_test_insertData.sql - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL String - Create Table - ${output} = Execute SQL String create table foobar (id integer , firstname varchar(20) ) - Log ${output} - Should Be Equal As Strings ${output} None - -Check If Exists In DB - Franz Allan - Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; - -Check If Not Exists In DB - Joe - Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; - -Table Must Exist - person - Table Must Exist person - -Verify Row Count is 0 - Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; - -Verify Row Count is Equal to X - Row Count is Equal to X SELECT id FROM person; 2 - -Verify Row Count is Less Than X - Row Count is Less Than X SELECT id FROM person; 3 - -Verify Row Count is Greater Than X - Row Count is Greater Than X SELECT * FROM person; 1 - -Retrieve Row Count - ${output} = Row Count SELECT id FROM person; - Log ${output} - Should Be Equal As Strings ${output} 2 - -Verify person Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM person fetch first 1 rows only; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ['ID', DBAPITypeObject(['NUM', 'DECIMAL', 'DEC', 'NUMERIC']), 12, 12, 10, 0, True] - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ['FIRST_NAME', DBAPITypeObject(['CHARACTER VARYING', 'CHAR VARYING', 'VARCHAR', 'STRING', 'CHARACTER', 'CHAR']), 30, 30, 30, 0, True] - ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} ['LAST_NAME', DBAPITypeObject(['CHARACTER VARYING', 'CHAR VARYING', 'VARCHAR', 'STRING', 'CHARACTER', 'CHAR']), 30, 30, 30, 0, True] - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 3 - -Verify Query - Row Count person table - ${output} = Query SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} [(2,)] - -Verify Query - Row Count foobar table - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(0,)] - -Verify Query - Get results as a list of dictionaries - [Tags] db smoke - ${output} = Query SELECT * FROM person; \ True - Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry - -Insert Data Into Table foobar - ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Query - Row Count foobar table 1 row - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(1,)] - -Verify Delete All Rows From Table - foobar - Delete All Rows From Table foobar - -Verify Query - Row Count foobar table 0 row - Row Count Is 0 SELECT * FROM foobar; - -Drop person and foobar table - Execute SQL String DROP TABLE person; - Execute SQL String DROP TABLE foobar; diff --git a/test/MSSQL_DB_Tests.robot b/test/MSSQL_DB_Tests.robot deleted file mode 100644 index f5870d20..00000000 --- a/test/MSSQL_DB_Tests.robot +++ /dev/null @@ -1,210 +0,0 @@ -*** Settings *** -Suite Setup Connect To Database pymssql ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} -Suite Teardown Disconnect From Database -Library DatabaseLibrary -Library OperatingSystem - -*** Variables *** -${DBHost} ${EMPTY} -${DBName} ${EMPTY} -${DBPass} ${EMPTY} -${DBPort} ${EMPTY} -${DBUser} ${EMPTY} - -*** Test Cases *** -Create person table - [Tags] db smoke - ${output} = Execute SQL String CREATE TABLE person (id integer unique, first_name varchar(20), last_name varchar(20)); - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL Script - Insert Data person table - [Tags] db smoke - ${output} = Execute SQL Script ./my_db_test_insertData.sql - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL String - Create Table - [Tags] db smoke - ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar(20) unique) - Log ${output} - Should Be Equal As Strings ${output} None - -Check If Exists In DB - Franz Allan - [Tags] db smoke - Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; - -Check If Not Exists In DB - Joe - [Tags] db smoke - Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; - -Table Must Exist - person - [Tags] db smoke - Table Must Exist person - -Verify Row Count is 0 - [Tags] db smoke - Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; - -Verify Row Count is Equal to X - [Tags] db smoke - Row Count is Equal to X SELECT id FROM person; 2 - -Verify Row Count is Less Than X - [Tags] db smoke - Row Count is Less Than X SELECT id FROM person; 3 - -Verify Row Count is Greater Than X - [Tags] db smoke - Row Count is Greater Than X SELECT * FROM person; 1 - -Retrieve Row Count - [Tags] db smoke - ${output} = Row Count SELECT id FROM person; - Log ${output} - Should Be Equal As Strings ${output} 2 - -Retrieve records from person table - [Tags] db smoke - ${output} = Execute SQL String SELECT * FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT TOP 1 * FROM person; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} (u'id', 3, None, None, None, None, None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} (u'first_name', 1, None, None, None, None, None) - ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} (u'last_name', 1, None, None, None, None, None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 3 - -Verify foobar Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT TOP 1 * FROM foobar; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} (u'id', 3, None, None, None, None, None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} (u'firstname', 1, None, None, None, None, None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 2 - -Verify Query - Row Count person table - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} [(2,)] - -Verify Query - Row Count foobar table - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(0,)] - -Verify Query - Get results as a list of dictionaries - [Tags] db smoke - ${output} = Query SELECT * FROM person; \ True - Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry - -Verify Execute SQL String - Row Count person table - [Tags] db smoke - ${output} = Execute SQL String SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Execute SQL String - Row Count foobar table - [Tags] db smoke - ${output} = Execute SQL String SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} None - -Insert Data Into Table foobar - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Query - Row Count foobar table 1 row - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(1,)] - -Verify Delete All Rows From Table - foobar - [Tags] db smoke - Delete All Rows From Table foobar - Comment Sleep 2s - -Verify Query - Row Count foobar table 0 row - [Tags] db smoke - Row Count Is 0 SELECT * FROM foobar; - -Begin first transaction - [Tags] db smoke - ${output} = Execute SQL String SAVE TRANSACTION first True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in first transaction - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO person VALUES(101,'Bilbo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person in first transaction - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Begin second transaction - [Tags] db smoke - ${output} = Execute SQL String SAVE TRANSACTION second True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in second transaction - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO person VALUES(102,'Frodo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify persons in first and second transactions - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 2 True - -Rollback second transaction - [Tags] db smoke - ${output} = Execute SQL String ROLLBACK TRANSACTION second True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify second transaction rollback - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Rollback first transaction - [Tags] db smoke - ${output} = Execute SQL String ROLLBACK TRANSACTION first True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify first transaction rollback - [Tags] db smoke - Row Count is 0 SELECT * FROM person WHERE last_name = 'Baggins'; True - -Drop person and foobar tables - [Tags] db smoke - ${output} = Execute SQL String DROP TABLE IF EXISTS person; - Log ${output} - Should Be Equal As Strings ${output} None - ${output} = Execute SQL String DROP TABLE IF EXISTS foobar; - Log ${output} - Should Be Equal As Strings ${output} None diff --git a/test/MySQL_DB_Tests.robot b/test/MySQL_DB_Tests.robot deleted file mode 100644 index d242df96..00000000 --- a/test/MySQL_DB_Tests.robot +++ /dev/null @@ -1,211 +0,0 @@ -*** Settings *** -Suite Setup Connect To Database pymysql ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} -Suite Teardown Disconnect From Database -Library DatabaseLibrary -Library OperatingSystem - -*** Variables *** -${DBHost} 127.0.0.1 -${DBName} my_db_test -${DBPass} "" -${DBPort} 3306 -${DBUser} root - -*** Test Cases *** -Create person table - [Tags] db smoke - ${output} = Execute SQL String CREATE TABLE person (id integer unique,first_name varchar(20),last_name varchar(20)); - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL Script - Insert Data person table - [Tags] db smoke - Comment ${output} = Execute SQL Script ./${DBName}_insertData.sql - ${output} = Execute SQL Script ./my_db_test_insertData.sql - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL String - Create Table - [Tags] db smoke - ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar(20) unique) - Log ${output} - Should Be Equal As Strings ${output} None - -Check If Exists In DB - Franz Allan - [Tags] db smoke - Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; - -Check If Not Exists In DB - Joe - [Tags] db smoke - Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; - -Table Must Exist - person - [Tags] db smoke - Table Must Exist person - -Verify Row Count is 0 - [Tags] db smoke - Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; - -Verify Row Count is Equal to X - [Tags] db smoke - Row Count is Equal to X SELECT id FROM person; 2 - -Verify Row Count is Less Than X - [Tags] db smoke - Row Count is Less Than X SELECT id FROM person; 3 - -Verify Row Count is Greater Than X - [Tags] db smoke - Row Count is Greater Than X SELECT * FROM person; 1 - -Retrieve Row Count - [Tags] db smoke - ${output} = Row Count SELECT id FROM person; - Log ${output} - Should Be Equal As Strings ${output} 2 - -Retrieve records from person table - [Tags] db smoke - ${output} = Execute SQL String SELECT * FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM person LIMIT 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} (u'id', 3, None, 11, 11, 0, True) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} (u'first_name', 253, None, 20, 20, 0, True) - ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} (u'last_name', 253, None, 20, 20, 0, True) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 3 - -Verify foobar Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM foobar LIMIT 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} (u'id', 3, None, 11, 11, 0, False) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} (u'firstname', 253, None, 20, 20, 0, True) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 2 - -Verify Query - Row Count person table - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} ((2,),) - -Verify Query - Row Count foobar table - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} ((0,),) - -Verify Query - Get results as a list of dictionaries - [Tags] db smoke - ${output} = Query SELECT * FROM person; \ True - Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry - -Verify Execute SQL String - Row Count person table - [Tags] db smoke - ${output} = Execute SQL String SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Execute SQL String - Row Count foobar table - [Tags] db smoke - ${output} = Execute SQL String SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} None - -Insert Data Into Table foobar - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Query - Row Count foobar table 1 row - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} ((1,),) - -Verify Delete All Rows From Table - foobar - [Tags] db smoke - Delete All Rows From Table foobar - Comment Sleep 2s - -Verify Query - Row Count foobar table 0 row - [Tags] db smoke - Row Count Is 0 SELECT * FROM foobar; - Comment ${output} = Query SELECT COUNT(*) FROM foobar; - Comment Log ${output} - Comment Should Be Equal As Strings ${output} [(0,)] - -Begin first transaction - [Tags] db smoke - ${output} = Execute SQL String SAVEPOINT first True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in first transaction - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO person VALUES(101,'Bilbo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person in first transaction - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Begin second transaction - [Tags] db smoke - ${output} = Execute SQL String SAVEPOINT second True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in second transaction - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO person VALUES(102,'Frodo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify persons in first and second transactions - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 2 True - -Rollback second transaction - [Tags] db smoke - ${output} = Execute SQL String ROLLBACK TO SAVEPOINT second True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify second transaction rollback - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Rollback first transaction - [Tags] db smoke - ${output} = Execute SQL String ROLLBACK TO SAVEPOINT first True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify first transaction rollback - [Tags] db smoke - Row Count is 0 SELECT * FROM person WHERE last_name = 'Baggins'; True - -Drop person and foobar tables - [Tags] db smoke - ${output} = Execute SQL String DROP TABLE IF EXISTS person,foobar; - Log ${output} - Should Be Equal As Strings ${output} None diff --git a/test/PostgreSQL_DB_Tests.robot b/test/PostgreSQL_DB_Tests.robot deleted file mode 100644 index fb458db9..00000000 --- a/test/PostgreSQL_DB_Tests.robot +++ /dev/null @@ -1,148 +0,0 @@ -*** Settings *** -Suite Setup Connect To Database psycopg2 ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} -Suite Teardown Disconnect From Database -Library DatabaseLibrary -Library OperatingSystem -Library Collections - -*** Variables *** -${DBHost} localhost -${DBName} travis_ci_test -${DBPass} "" -${DBPort} 5432 -${DBUser} postgres - -*** Test Cases *** -Create person table - ${output} = Execute SQL String CREATE TABLE person (id integer unique,first_name varchar,last_name varchar); - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL Script - Insert Data person table - Comment ${output} = Execute SQL Script ./${DBName}_insertData.sql - ${output} = Execute SQL Script ./my_db_test_insertData.sql - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL String - Create Table - ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar unique) - Log ${output} - Should Be Equal As Strings ${output} None - -Check If Exists In DB - Franz Allan - Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; - -Check If Not Exists In DB - Joe - Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; - -Table Must Exist - person - Table Must Exist person - -Verify Row Count is 0 - Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; - -Verify Row Count is Equal to X - Row Count is Equal to X SELECT id FROM person; 2 - -Verify Row Count is Less Than X - Row Count is Less Than X SELECT id FROM person; 3 - -Verify Row Count is Greater Than X - Row Count is Greater Than X SELECT * FROM person; 1 - -Retrieve Row Count - ${output} = Row Count SELECT id FROM person; - Log ${output} - Should Be Equal As Strings ${output} 2 - -Retrieve records from person table - ${output} = Execute SQL String SELECT * FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM person LIMIT 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} Column(name='id', type_code=23, display_size=None, internal_size=4, precision=None, scale=None, null_ok=None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} Column(name='first_name', type_code=1043, display_size=None, internal_size=-1, precision=None, scale=None, null_ok=None) - ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} Column(name='last_name', type_code=1043, display_size=None, internal_size=-1, precision=None, scale=None, null_ok=None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 3 - -Verify foobar Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM foobar LIMIT 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} Column(name='id', type_code=23, display_size=None, internal_size=4, precision=None, scale=None, null_ok=None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} Column(name='firstname', type_code=1043, display_size=None, internal_size=-1, precision=None, scale=None, null_ok=None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 2 - -Verify Query - Row Count person table - ${output} = Query SELECT COUNT(*) FROM person; - Log ${output} - ${val}= Get from list ${output} 0 - ${val}= Convert to list ${val} - ${val}= Get from list ${val} 0 - Should be equal as Integers ${val} 2 - -Verify Query - Row Count foobar table - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - ${val}= Get from list ${output} 0 - ${val}= Convert to list ${val} - ${val}= Get from list ${val} 0 - Should be equal as Integers ${val} 0 - -Verify Query - Get results as a list of dictionaries - [Tags] db smoke - ${output} = Query SELECT * FROM person; \ True - Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry - -Verify Execute SQL String - Row Count person table - ${output} = Execute SQL String SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Execute SQL String - Row Count foobar table - ${output} = Execute SQL String SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} None - -Insert Data Into Table foobar - ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Query - Row Count foobar table 1 row - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - ${val}= Get from list ${output} 0 - ${val}= Convert to list ${val} - ${val}= Get from list ${val} 0 - Should be equal as Integers ${val} 1 - -Verify Delete All Rows From Table - foobar - Delete All Rows From Table foobar - Comment Sleep 2s - -Verify Query - Row Count foobar table 0 row - Row Count Is 0 SELECT * FROM foobar; - Comment ${output} = Query SELECT COUNT(*) FROM foobar; - Comment Log ${output} - Comment Should Be Equal As Strings ${output} [(0,)] - -Drop person and foobar tables - ${output} = Execute SQL String DROP TABLE IF EXISTS person,foobar; - Log ${output} - Should Be Equal As Strings ${output} None diff --git a/test/PyODBC_DB_Tests.robot b/test/PyODBC_DB_Tests.robot deleted file mode 100644 index 554b2945..00000000 --- a/test/PyODBC_DB_Tests.robot +++ /dev/null @@ -1,177 +0,0 @@ -*** Settings *** -Suite Setup Connect To Database pyodbc ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} dbDriver=${dbDriver} -Suite Teardown Disconnect From Database -Library DatabaseLibrary -Library OperatingSystem - -*** Variables *** -${DBHost} ${EMPTY} -${DBName} ${EMPTY} -${DBPass} ${EMPTY} -${DBPort} ${EMPTY} -${DBUser} ${EMPTY} -${dbDriver} ${EMPTY} - -*** Test Cases *** -Create person table - ${output} = Execute SQL String CREATE TABLE person (id integer unique, first_name varchar(20), last_name varchar(20)); - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL Script - Insert Data person table - ${output} = Execute SQL Script ./my_db_test_insertData.sql - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL String - Create Table - ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar(20) unique) - Log ${output} - Should Be Equal As Strings ${output} None - -Check If Exists In DB - Franz Allan - Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; - -Check If Not Exists In DB - Joe - Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; - -Table Must Exist - person - Table Must Exist person - -Verify Row Count is 0 - Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; - -Verify Row Count is Equal to X - Row Count is Equal to X SELECT id FROM person; 2 - -Verify Row Count is Less Than X - Row Count is Less Than X SELECT id FROM person; 3 - -Verify Row Count is Greater Than X - Row Count is Greater Than X SELECT * FROM person; 1 - -Retrieve Row Count - ${output} = Row Count SELECT id FROM person; - Log ${output} - Should Be Equal As Strings ${output} 2 - -Retrieve records from person table - ${output} = Execute SQL String SELECT * FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person Description - Comment Query db for table column descriptions - @{queryResults} = Description SELECT TOP 1 * FROM person; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} (u'id', 3, None, None, None, None, None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} (u'first_name', 1, None, None, None, None, None) - ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} (u'last_name', 1, None, None, None, None, None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 3 - -Verify foobar Description - Comment Query db for table column descriptions - @{queryResults} = Description SELECT TOP 1 * FROM foobar; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} (u'id', 3, None, None, None, None, None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} (u'firstname', 1, None, None, None, None, None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 2 - -Verify Query - Row Count person table - ${output} = Query SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} [(2,)] - -Verify Query - Row Count foobar table - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(0,)] - -Verify Query - Get results as a list of dictionaries - ${output} = Query SELECT * FROM person; \ True - Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry - -Verify Execute SQL String - Row Count person table - ${output} = Execute SQL String SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Execute SQL String - Row Count foobar table - ${output} = Execute SQL String SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} None - -Insert Data Into Table foobar - ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Query - Row Count foobar table 1 row - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(1,)] - -Verify Delete All Rows From Table - foobar - Delete All Rows From Table foobar - Comment Sleep 2s - -Verify Query - Row Count foobar table 0 row - Row Count Is 0 SELECT * FROM foobar; - -Begin first transaction - ${output} = Execute SQL String SAVE TRANSACTION first True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in first transaction - ${output} = Execute SQL String INSERT INTO person VALUES(101,'Bilbo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person in first transaction - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Begin second transaction - ${output} = Execute SQL String SAVE TRANSACTION second True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in second transaction - ${output} = Execute SQL String INSERT INTO person VALUES(102,'Frodo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify persons in first and second transactions - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 2 True - -Rollback second transaction - ${output} = Execute SQL String ROLLBACK TRANSACTION second True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify second transaction rollback - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Rollback first transaction - ${output} = Execute SQL String ROLLBACK TRANSACTION first True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify first transaction rollback - Row Count is 0 SELECT * FROM person WHERE last_name = 'Baggins'; True - -Drop person and foobar tables - ${output} = Execute SQL String DROP TABLE IF EXISTS person; - Log ${output} - Should Be Equal As Strings ${output} None - ${output} = Execute SQL String DROP TABLE IF EXISTS foobar; - Log ${output} - Should Be Equal As Strings ${output} None diff --git a/test/SQLite3_DB_Tests.robot b/test/SQLite3_DB_Tests.robot deleted file mode 100644 index 3b63684e..00000000 --- a/test/SQLite3_DB_Tests.robot +++ /dev/null @@ -1,216 +0,0 @@ -*** Settings *** -Library DatabaseLibrary -Library OperatingSystem - -*** Variables *** -${DBName} my_db_test - -*** Test Cases *** -Remove old DB if exists - [Tags] db smoke - ${Status} ${value} = Run Keyword And Ignore Error File Should Not Exist ./${DBName}.db - Run Keyword If "${Status}" == "FAIL" Run Keyword And Ignore Error Remove File ./${DBName}.db - File Should Not Exist ./${DBName}.db - Comment Sleep 1s - -Connect to SQLiteDB - [Tags] db smoke - Comment Connect To Database Using Custom Params sqlite3 database='path_to_dbfile\dbname.db' - Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None - -Create person table - [Tags] db smoke - ${output} = Execute SQL String CREATE TABLE person (id integer unique,first_name varchar,last_name varchar); - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL Script - Insert Data person table - [Tags] db smoke - ${output} = Execute SQL Script ./${DBName}_insertData.sql - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL String - Create Table - [Tags] db smoke - ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar unique) - Log ${output} - Should Be Equal As Strings ${output} None - -Check If Exists In DB - Franz Allan - [Tags] db smoke - Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; - -Check If Not Exists In DB - Joe - [Tags] db smoke - Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; - -Table Must Exist - person - [Tags] db smoke - Table Must Exist person - -Verify Row Count is 0 - [Tags] db smoke - Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; - -Verify Row Count is Equal to X - [Tags] db smoke - Row Count is Equal to X SELECT id FROM person; 2 - -Verify Row Count is Less Than X - [Tags] db smoke - Row Count is Less Than X SELECT id FROM person; 3 - -Verify Row Count is Greater Than X - [Tags] db smoke - Row Count is Greater Than X SELECT * FROM person; 1 - -Retrieve Row Count - [Tags] db smoke - ${output} = Row Count SELECT id FROM person; - Log ${output} - Should Be Equal As Strings ${output} 2 - -Retrieve records from person table - [Tags] db smoke - ${output} = Execute SQL String SELECT * FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM person LIMIT 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', None, None, None, None, None, None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('first_name', None, None, None, None, None, None) - ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} ('last_name', None, None, None, None, None, None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 3 - -Verify foobar Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM foobar LIMIT 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', None, None, None, None, None, None) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('firstname', None, None, None, None, None, None) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 2 - -Verify Query - Row Count person table - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} [(2,)] - -Verify Query - Row Count foobar table - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(0,)] - -Verify Query - Get results as a list of dictionaries - [Tags] db smoke - ${output} = Query SELECT * FROM person; \ True - Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry - -Verify Execute SQL String - Row Count person table - [Tags] db smoke - ${output} = Execute SQL String SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Execute SQL String - Row Count foobar table - [Tags] db smoke - ${output} = Execute SQL String SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} None - -Insert Data Into Table foobar - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Query - Row Count foobar table 1 row - [Tags] db smoke - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} [(1,)] - -Verify Delete All Rows From Table - foobar - [Tags] db smoke - Delete All Rows From Table foobar - Comment Sleep 2s - -Verify Query - Row Count foobar table 0 row - [Tags] db smoke - Row Count Is 0 SELECT * FROM foobar; - -Begin first transaction - [Tags] db smoke - ${output} = Execute SQL String SAVEPOINT first True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in first transaction - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO person VALUES(101,'Bilbo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person in first transaction - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Begin second transaction - [Tags] db smoke - ${output} = Execute SQL String SAVEPOINT second True - Log ${output} - Should Be Equal As Strings ${output} None - -Add person in second transaction - [Tags] db smoke - ${output} = Execute SQL String INSERT INTO person VALUES(102,'Frodo','Baggins'); True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify persons in first and second transactions - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 2 True - -Rollback second transaction - [Tags] db smoke - ${output} = Execute SQL String ROLLBACK TO SAVEPOINT second True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify second transaction rollback - [Tags] db smoke - Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True - -Rollback first transaction - [Tags] db smoke - ${output} = Execute SQL String ROLLBACK TO SAVEPOINT first True - Log ${output} - Should Be Equal As Strings ${output} None - -Verify first transaction rollback - [Tags] db smoke - Row Count is 0 SELECT * FROM person WHERE last_name = 'Baggins'; True - -Drop person and foobar tables - [Tags] db smoke - ${output} = Execute SQL String DROP TABLE IF EXISTS person; - Log ${output} - Should Be Equal As Strings ${output} None - ${output} = Execute SQL String DROP TABLE IF EXISTS foobar; - Log ${output} - Should Be Equal As Strings ${output} None diff --git a/test/Teradata_DB_Tests.robot b/test/Teradata_DB_Tests.robot deleted file mode 100644 index 35c7f82b..00000000 --- a/test/Teradata_DB_Tests.robot +++ /dev/null @@ -1,154 +0,0 @@ -*** Settings *** -Suite Setup Connect To Database teradata ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} -Suite Teardown Disconnect From Database -Library DatabaseLibrary -Library OperatingSystem -Library Collections - -*** Variables *** -${DBHost} 192.168.10.45 -${DBName} ADRIAN -${DBPass} dbc -${DBPort} 1025 -${DBUser} dbc - -*** Test Cases *** -Create person table - [Tags] db smoke - ${output} = Execute SQL String CREATE TABLE person (id integer not null unique,first_name varchar(20),last_name varchar(20)); - Log ${output} - Should Be Equal As Strings ${output} None - -Execute SQL Script - Insert Data person table - Comment ${output} = Execute SQL Script ./${DBName}_insertData.sql - ${output} = Execute SQL Script ./my_db_test_insertData.sql - Log ${output} - Should Be Equal As Strings ${output} None - -Create foobar table - ${output} = Execute SQL String create table foobar (id integer not null primary key, firstname varchar(100) not null unique) - Log ${output} - Should Be Equal As Strings ${output} None - -Check If Exists In DB - Franz Allan - Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; - -Check If Not Exists In DB - Joe - Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; - -Table Must Exist - person - Table Must Exist person - -Verify Row Count is 0 - Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; - -Verify Row Count is Equal to X - Row Count is Equal to X SELECT id FROM person; 2 - -Verify Row Count is Less Than X - Row Count is Less Than X SELECT id FROM person; 3 - -Verify Row Count is Greater Than X - Row Count is Greater Than X SELECT * FROM person; 1 - -Retrieve Row Count - ${output} = Row Count SELECT id FROM person; - Log ${output} - Should Be Equal As Strings ${output} 2 - -Retrieve records from person table - ${output} = Execute SQL String SELECT * FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify person Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM person SAMPLE 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', , None, 10, 0, None, 0) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('first_name', , None, 20, 0, None, 1) - ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} ('last_name', , None, 20, 0, None, 1) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 3 - -Verify foobar Description - [Tags] db smoke - Comment Query db for table column descriptions - @{queryResults} = Description SELECT * FROM foobar SAMPLE 1; - Log Many @{queryResults} - ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', , None, 10, 0, None, 0) - ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('firstname', , None, 100, 0, None, 0) - ${NumColumns} = Get Length ${queryResults} - Should Be Equal As Integers ${NumColumns} 2 - -Verify Query - Row Count person table - ${output} = Query SELECT COUNT(*) FROM person; - Log ${output} - ${val}= Get from list ${output} 0 - ${val}= Convert to list ${val} - ${val}= Get from list ${val} 0 - Should be equal as Integers ${val} 2 - -Verify Query - Row Count foobar table - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - ${val}= Get from list ${output} 0 - ${val}= Convert to list ${val} - ${val}= Get from list ${val} 0 - Should be equal as Integers ${val} 0 - -Verify Query - Get results as a list of dictionaries - [Tags] db smoke - ${output} = Query SELECT * FROM person; \ True - Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry - -Verify Execute SQL String - Row Count person table - ${output} = Execute SQL String SELECT COUNT(*) FROM person; - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Execute SQL String - Row Count foobar table - ${output} = Execute SQL String SELECT COUNT(*) FROM foobar; - Log ${output} - Should Be Equal As Strings ${output} None - -Insert Data Into Table foobar - ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); - Log ${output} - Should Be Equal As Strings ${output} None - -Verify Query - Row Count foobar table 1 row - ${output} = Query SELECT COUNT(*) FROM foobar; - Log ${output} - ${val}= Get from list ${output} 0 - ${val}= Convert to list ${val} - ${val}= Get from list ${val} 0 - Should be equal as Integers ${val} 1 - -Verify Delete All Rows From Table - foobar - Delete All Rows From Table foobar - Comment Sleep 2s - -Verify Query - Row Count foobar table 0 row - Row Count Is 0 SELECT * FROM foobar; - Comment ${output} = Query SELECT COUNT(*) FROM foobar; - Comment Log ${output} - Comment Should Be Equal As Strings ${output} [(0,)] - -Drop person table - ${output} = Execute SQL String DROP TABLE person; - Log ${output} - Should Be Equal As Strings ${output} None - -Drop foobar table - ${output} = Execute SQL String DROP TABLE foobar; - Log ${output} - Should Be Equal As Strings ${output} None diff --git a/test/readme.md b/test/readme.md new file mode 100644 index 00000000..96870c43 --- /dev/null +++ b/test/readme.md @@ -0,0 +1,61 @@ +# Which tests run automatically in the pipeline? +- Tests from the folder `common_tests` run automatically in the pipeline after pushing in the repository +- The tests in the folder `custom_db_tests` are designed to run locally - they have to be triggered manually. I don't run them at all changes. +- There are some unit tests with pytest, but mostly there are acceptance tests with RF +- See the folder `.github/workflows` + +# Which databases / modules are covered? +- The acceptance tests in the pipeline don't cover all possible DB's - here is a lot of room for improvement +- Running tests locally require DB containers running - see below + +# Running tests locally from VS Code / terminal +- Selecting a DB module works via a global variable `GLOBAL_DB_SELECTOR` - set it from VSC or CLI +- Current debug/launch configs are implemented for old LSP plugin - still need to update to Robotcode from Daniel + +# Here are some advices for local testing of the library with different Python DB modules +## Oracle: +- https://github.com/gvenzl/oci-oracle-free +- https://hub.docker.com/r/gvenzl/oracle-free +- docker pull gvenzl/oracle-free +- docker run --rm --name oracle -d -p 1521:1521 -e ORACLE_PASSWORD=pass -e ORACLE_DATABASE=db -e APP_USER=db_user -e APP_USER_PASSWORD=pass gvenzl/oracle-free + +## PostgreSQL +- https://hub.docker.com/_/postgres +- docker pull postgres +- docker run --rm --name postgres -e POSTGRES_USER=db_user -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=db -p 5432:5432 -d postgres + +## Teradata +- use VM image, e.g. in VirtualBox +- https://downloads.teradata.com/download/database/teradata-express/vmware +- use network bridge mode +- create new DB + CREATE DATABASE db + AS PERMANENT = 60e6, -- 60MB + SPOOL = 120e6; -- 120MB +- Install Teradata driver for your OS + https://downloads.teradata.com/download/connectivity/odbc-driver/windows + +- DEPRECATED: https://github.com/teradata/PyTd + -> new: https://github.com/Teradata/python-driver +- docs: https://quickstarts.teradata.com/getting.started.vbox.html + +## IBM Db2 +- https://hub.docker.com/r/ibmcom/db2 +- docker pull ibmcom/db2 +- docker run --rm -itd --name mydb2 --privileged=true -p 50000:50000 -e LICENSE=accept -e DB2INSTANCE=db_user -e DB2INST1_PASSWORD=pass -e DBNAME=db ibmcom/db2 +--> needs some minutes to start the DB !!! + +## MySQL +- https://hub.docker.com/_/mysql +- docker run --rm --name mysql -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_USER=db_user -e MYSQL_PASSWORD=pass -p 3306:3306 -d mysql +- For tests with pyodbc install the ODBC driver https://learn.microsoft.com/en-us/sql/connect/odbc/windows/system-requirements-installation-and-driver-files?view=sql-server-ver16#installing-microsoft-odbc-driver-for-sql-server + +## Microsoft SQL Server +- https://hub.docker.com/_/microsoft-mssql-server +- docker run --rm --name mssql -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD='MyPass1234!' -p 1433:1433 -d mcr.microsoft.com/mssql/server +--> login and create DB: + - docker exec -it mssql bash + - /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'MyPass1234!' + - CREATE DATABASE db + - go +- docs: https://learn.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-ver16&pivots=cs1-bash \ No newline at end of file diff --git a/test/resources/common.resource b/test/resources/common.resource new file mode 100644 index 00000000..260002ca --- /dev/null +++ b/test/resources/common.resource @@ -0,0 +1,125 @@ +*** Settings *** +Documentation Global variables, which are used in all test common tests +... and which should be set outside at the test execution start (e.g. in CI pipeline) + +Library Collections +Library OperatingSystem +Library DatabaseLibrary +Library DateTime +Resource config_files/connect_config_file.resource + + +*** Variables *** +${DB_MODULE_MODE} standard +${DB_MODULE} psycopg2 +${DB_HOST} 127.0.0.1 +${DB_NAME} db +${DB_PASS} pass +${DB_PORT} 5432 +${DB_USER} db_user + +# used for MySQL via PyODBC only +${DB_DRIVER} ODBC Driver 18 for SQL Server + +# Oracle via Jaydebeapi +${DRIVER_PATH} ${CURDIR}/ojdbc17.jar +${DRIVER_CLASSNAME} oracle.jdbc.driver.OracleDriver +&{DRIVER_ARGS} user=${DB_USER} password=${DB_PASS} +${JDBC_URL} jdbc:oracle:thin:@${DB_HOST}:${DB_PORT}/${DB_NAME} + + +*** Keywords *** +Connect To DB + [Documentation] Connects to the database based on the current DB module under test + ... and connection params set in global variables with alias + [Arguments] ${alias}=${None} + ${DB_KWARGS}= Create Dictionary + IF $alias is not None + Set To Dictionary ${DB_KWARGS} alias=${alias} + END + IF "${DB_MODULE_MODE}" == "custom" + IF "${DB_MODULE}" == "sqlite3" + Remove File ${DBName}.db + Connect To Database sqlite3 database=./${DBName}.db isolation_level=${None} + ... &{DB_KWARGS} + ELSE IF "${DB_MODULE}" == "jaydebeapi" + Connect To Database ${DB_MODULE} jclassname=${DRIVER_CLASSNAME} url=${JDBC_URL} + ... driver_args=${DRIVER_ARGS} jars=${DRIVER_PATH} &{DB_KWARGS} + Set Auto Commit False alias=${alias} + Set Omit Trailing Semicolon True alias=${alias} + ELSE + ${Connection String}= Build Connection String + Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} &{DB_KWARGS} + END + ELSE IF "${DB_MODULE_MODE}" == "standard" + ${DB_ARGS}= Create List + ... ${DB_MODULE} + ... ${DB_NAME} + ... ${DB_USER} + ... ${DB_PASS} + ... ${DB_HOST} + ... ${DB_PORT} + IF "${DB_MODULE}" == "pyodbc" + Set To Dictionary ${DB_KWARGS} odbc_driver=${DB_DRIVER} + END + Connect To Database @{DB_ARGS} &{DB_KWARGS} + ELSE + Fail Unexpected mode - ${DB_MODULE_MODE} + END + +Build Connection String + [Documentation] Returns the connection string variable depending on the DB module + ... currently under test. + IF "${DB_MODULE}" == "oracledb" + ${Result}= Set Variable + ... ${DB_USER}/${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME} + ELSE IF "${DB_MODULE}" == "psycopg2" + ${Result}= Set Variable + ... postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME} + ELSE + Skip Don't know how to build a connection string for '${DB_MODULE}' + END + RETURN ${Result} + +Create Person Table + ${sql}= Catenate + ... CREATE TABLE person + ... (id integer not null unique, FIRST_NAME varchar(20), LAST_NAME varchar(20)) + ${output}= Execute Sql String ${sql} + RETURN ${output} + +Create Person Table And Insert Data + Create Person Table + Insert Data In Person Table Using SQL Script + +Insert Data In Person Table Using SQL Script + [Arguments] ${alias}=${None} + IF $alias is None + ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql + ELSE + ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql alias=${alias} + END + RETURN ${output} + +Create Foobar Table + ${sql}= Catenate + ... CREATE TABLE foobar + ... (id integer not null primary key, FIRST_NAME varchar(30) not null unique) + ${output}= Execute Sql String ${sql} + RETURN ${output} + +Create Foobar Table And Insert Data + Create Foobar Table + Execute SQL String INSERT INTO foobar VALUES(1,'Jerry') + +Create Tables Person And Foobar + Create Person Table + Create Foobar Table + +Drop Tables Person And Foobar + Sleep 1s + FOR ${table} IN person foobar + ${exists}= Run Keyword And Return Status + ... Table Must Exist ${table} + IF ${exists} Execute Sql String DROP TABLE ${table} + END diff --git a/test/resources/config_files/connect_config_file.resource b/test/resources/config_files/connect_config_file.resource new file mode 100644 index 00000000..621ecd36 --- /dev/null +++ b/test/resources/config_files/connect_config_file.resource @@ -0,0 +1,11 @@ +*** Settings *** +Resource ../common.resource + + +*** Keywords *** +Connect Using Config File + [Documentation] `File name` is only name without extension, + ... the path is build relative to the resource directory + [Arguments] ${File name}=${None} &{Params} + ${Path}= Set Variable ${CURDIR}/${File name}.cfg + Connect To Database config_file=${Path} &{Params} diff --git a/test/resources/config_files/oracledb/custom_param_password.cfg b/test/resources/config_files/oracledb/custom_param_password.cfg new file mode 100644 index 00000000..d6d93b96 --- /dev/null +++ b/test/resources/config_files/oracledb/custom_param_password.cfg @@ -0,0 +1,6 @@ +[default] +db_module=oracledb +db_name=db +password=pass +db_host=127.0.0.1 +db_port=1521 \ No newline at end of file diff --git a/test/resources/config_files/oracledb/invalid_custom_params.cfg b/test/resources/config_files/oracledb/invalid_custom_params.cfg new file mode 100644 index 00000000..1214b8be --- /dev/null +++ b/test/resources/config_files/oracledb/invalid_custom_params.cfg @@ -0,0 +1,8 @@ +[default] +db_module=oracledb +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=1521 +blah=blah \ No newline at end of file diff --git a/test/resources/config_files/oracledb/old_param_names.cfg b/test/resources/config_files/oracledb/old_param_names.cfg new file mode 100644 index 00000000..2d81cd40 --- /dev/null +++ b/test/resources/config_files/oracledb/old_param_names.cfg @@ -0,0 +1,8 @@ +[default] +dbapiModuleName=oracledb +dbName=db +dbUsername=db_user +dbPassword=pass +dbHost=127.0.0.1 +dbPort=1521 +driverMode=thin \ No newline at end of file diff --git a/test/resources/config_files/oracledb/simple_default_alias.cfg b/test/resources/config_files/oracledb/simple_default_alias.cfg new file mode 100644 index 00000000..fe487e8e --- /dev/null +++ b/test/resources/config_files/oracledb/simple_default_alias.cfg @@ -0,0 +1,8 @@ +[default] +db_module=oracledb +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=1521 +oracle_driver_mode=thin \ No newline at end of file diff --git a/test/resources/config_files/oracledb/some_basic_params_missing.cfg b/test/resources/config_files/oracledb/some_basic_params_missing.cfg new file mode 100644 index 00000000..7e845af3 --- /dev/null +++ b/test/resources/config_files/oracledb/some_basic_params_missing.cfg @@ -0,0 +1,3 @@ +[default] +db_module=oracledb +db_name=db \ No newline at end of file diff --git a/test/resources/config_files/oracledb/thick_mode.cfg b/test/resources/config_files/oracledb/thick_mode.cfg new file mode 100644 index 00000000..bd1a4875 --- /dev/null +++ b/test/resources/config_files/oracledb/thick_mode.cfg @@ -0,0 +1,8 @@ +[default] +db_module=oracledb +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=1521 +oracle_driver_mode=thick \ No newline at end of file diff --git a/test/resources/config_files/oracledb/valid_custom_params.cfg b/test/resources/config_files/oracledb/valid_custom_params.cfg new file mode 100644 index 00000000..df3bffd1 --- /dev/null +++ b/test/resources/config_files/oracledb/valid_custom_params.cfg @@ -0,0 +1,7 @@ +[default] +db_module=oracledb +db_name=db +user=db_user +password=pass +db_host=127.0.0.1 +db_port=1521 \ No newline at end of file diff --git a/test/resources/config_files/oracledb/wrong_password.cfg b/test/resources/config_files/oracledb/wrong_password.cfg new file mode 100644 index 00000000..87691ac7 --- /dev/null +++ b/test/resources/config_files/oracledb/wrong_password.cfg @@ -0,0 +1,7 @@ +[default] +db_module=oracledb +db_name=db +db_user=db_user +db_password=wrong +db_host=127.0.0.1 +db_port=1521 \ No newline at end of file diff --git a/test/resources/config_files/psycopg2/custom_param_password.cfg b/test/resources/config_files/psycopg2/custom_param_password.cfg new file mode 100644 index 00000000..1ae5cb93 --- /dev/null +++ b/test/resources/config_files/psycopg2/custom_param_password.cfg @@ -0,0 +1,6 @@ +[default] +db_module=psycopg2 +db_name=db +password=pass +db_host=127.0.0.1 +db_port=5432 \ No newline at end of file diff --git a/test/resources/config_files/psycopg2/invalid_custom_params.cfg b/test/resources/config_files/psycopg2/invalid_custom_params.cfg new file mode 100644 index 00000000..14969652 --- /dev/null +++ b/test/resources/config_files/psycopg2/invalid_custom_params.cfg @@ -0,0 +1,8 @@ +[default] +db_module=psycopg2 +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=5432 +blah=blah \ No newline at end of file diff --git a/test/resources/config_files/psycopg2/old_param_names.cfg b/test/resources/config_files/psycopg2/old_param_names.cfg new file mode 100644 index 00000000..d9faef72 --- /dev/null +++ b/test/resources/config_files/psycopg2/old_param_names.cfg @@ -0,0 +1,7 @@ +[default] +dbapiModuleName=psycopg2 +dbName=db +dbUsername=db_user +dbPassword=pass +dbHost=127.0.0.1 +dbPort=5432 \ No newline at end of file diff --git a/test/resources/config_files/psycopg2/simple_default_alias.cfg b/test/resources/config_files/psycopg2/simple_default_alias.cfg new file mode 100644 index 00000000..a80ef74b --- /dev/null +++ b/test/resources/config_files/psycopg2/simple_default_alias.cfg @@ -0,0 +1,7 @@ +[default] +db_module=psycopg2 +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=5432 \ No newline at end of file diff --git a/test/resources/config_files/psycopg2/some_basic_params_missing.cfg b/test/resources/config_files/psycopg2/some_basic_params_missing.cfg new file mode 100644 index 00000000..49d1cdee --- /dev/null +++ b/test/resources/config_files/psycopg2/some_basic_params_missing.cfg @@ -0,0 +1,3 @@ +[default] +db_module=psycopg2 +db_name=db \ No newline at end of file diff --git a/test/resources/config_files/psycopg2/valid_custom_params.cfg b/test/resources/config_files/psycopg2/valid_custom_params.cfg new file mode 100644 index 00000000..fb15ffac --- /dev/null +++ b/test/resources/config_files/psycopg2/valid_custom_params.cfg @@ -0,0 +1,7 @@ +[default] +db_module=psycopg2 +db_name=db +user=db_user +password=pass +db_host=127.0.0.1 +db_port=5432 \ No newline at end of file diff --git a/test/resources/config_files/psycopg2/wrong_password.cfg b/test/resources/config_files/psycopg2/wrong_password.cfg new file mode 100644 index 00000000..9e97614c --- /dev/null +++ b/test/resources/config_files/psycopg2/wrong_password.cfg @@ -0,0 +1,7 @@ +[default] +db_module=psycopg2 +db_name=db +db_user=db_user +db_password=wrong +db_host=127.0.0.1 +db_port=5432 \ No newline at end of file diff --git a/test/resources/config_files/pymssql/charset_invalid.cfg b/test/resources/config_files/pymssql/charset_invalid.cfg new file mode 100644 index 00000000..dab69516 --- /dev/null +++ b/test/resources/config_files/pymssql/charset_invalid.cfg @@ -0,0 +1,8 @@ +[default] +db_module=pymssql +db_name=db +user=SA +password=MyPass1234! +db_host=127.0.0.1 +db_port=1433 +db_charset=wrong \ No newline at end of file diff --git a/test/resources/config_files/pymssql/custom_param_password.cfg b/test/resources/config_files/pymssql/custom_param_password.cfg new file mode 100644 index 00000000..ff03d151 --- /dev/null +++ b/test/resources/config_files/pymssql/custom_param_password.cfg @@ -0,0 +1,6 @@ +[default] +db_module=pymssql +db_name=db +password=MyPass1234! +db_host=127.0.0.1 +db_port=1433 \ No newline at end of file diff --git a/test/resources/config_files/pymssql/invalid_custom_params.cfg b/test/resources/config_files/pymssql/invalid_custom_params.cfg new file mode 100644 index 00000000..1c6c801e --- /dev/null +++ b/test/resources/config_files/pymssql/invalid_custom_params.cfg @@ -0,0 +1,8 @@ +[default] +db_module=pymssql +db_name=db +db_user=SA +db_password=MyPass1234! +db_host=127.0.0.1 +db_port=1433 +blah=blah \ No newline at end of file diff --git a/test/resources/config_files/pymssql/old_param_names.cfg b/test/resources/config_files/pymssql/old_param_names.cfg new file mode 100644 index 00000000..00a68add --- /dev/null +++ b/test/resources/config_files/pymssql/old_param_names.cfg @@ -0,0 +1,7 @@ +[default] +dbapiModuleName=pymssql +dbName=db +dbUsername=SA +dbPassword=MyPass1234! +dbHost=127.0.0.1 +dbPort=1433 \ No newline at end of file diff --git a/test/resources/config_files/pymssql/simple_default_alias.cfg b/test/resources/config_files/pymssql/simple_default_alias.cfg new file mode 100644 index 00000000..cd111ab1 --- /dev/null +++ b/test/resources/config_files/pymssql/simple_default_alias.cfg @@ -0,0 +1,7 @@ +[default] +db_module=pymssql +db_name=db +db_user=SA +db_password=MyPass1234! +db_host=127.0.0.1 +db_port=1433 \ No newline at end of file diff --git a/test/resources/config_files/pymssql/some_basic_params_missing.cfg b/test/resources/config_files/pymssql/some_basic_params_missing.cfg new file mode 100644 index 00000000..20e4533b --- /dev/null +++ b/test/resources/config_files/pymssql/some_basic_params_missing.cfg @@ -0,0 +1,3 @@ +[default] +db_module=pymssql +db_name=db \ No newline at end of file diff --git a/test/resources/config_files/pymssql/valid_custom_params.cfg b/test/resources/config_files/pymssql/valid_custom_params.cfg new file mode 100644 index 00000000..47613a21 --- /dev/null +++ b/test/resources/config_files/pymssql/valid_custom_params.cfg @@ -0,0 +1,7 @@ +[default] +db_module=pymssql +db_name=db +user=SA +password=MyPass1234! +db_host=127.0.0.1 +db_port=1433 \ No newline at end of file diff --git a/test/resources/config_files/pymssql/wrong_password.cfg b/test/resources/config_files/pymssql/wrong_password.cfg new file mode 100644 index 00000000..04b37f2e --- /dev/null +++ b/test/resources/config_files/pymssql/wrong_password.cfg @@ -0,0 +1,7 @@ +[default] +db_module=pymssql +db_name=db +db_user=SA +db_password=wrong +db_host=127.0.0.1 +db_port=1433 \ No newline at end of file diff --git a/test/resources/config_files/pymysql/charset_invalid.cfg b/test/resources/config_files/pymysql/charset_invalid.cfg new file mode 100644 index 00000000..9eb9a14f --- /dev/null +++ b/test/resources/config_files/pymysql/charset_invalid.cfg @@ -0,0 +1,8 @@ +[default] +db_module=pymysql +db_name=db +user=db_user +password=pass +db_host=127.0.0.1 +db_port=3306 +db_charset=wrong \ No newline at end of file diff --git a/test/resources/config_files/pymysql/custom_param_password.cfg b/test/resources/config_files/pymysql/custom_param_password.cfg new file mode 100644 index 00000000..52b68e76 --- /dev/null +++ b/test/resources/config_files/pymysql/custom_param_password.cfg @@ -0,0 +1,6 @@ +[default] +db_module=pymysql +db_name=db +password=pass +db_host=127.0.0.1 +db_port=3306 \ No newline at end of file diff --git a/test/resources/config_files/pymysql/invalid_custom_params.cfg b/test/resources/config_files/pymysql/invalid_custom_params.cfg new file mode 100644 index 00000000..46975eff --- /dev/null +++ b/test/resources/config_files/pymysql/invalid_custom_params.cfg @@ -0,0 +1,8 @@ +[default] +db_module=pymysql +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=3306 +blah=blah \ No newline at end of file diff --git a/test/resources/config_files/pymysql/old_param_names.cfg b/test/resources/config_files/pymysql/old_param_names.cfg new file mode 100644 index 00000000..0d73312d --- /dev/null +++ b/test/resources/config_files/pymysql/old_param_names.cfg @@ -0,0 +1,7 @@ +[default] +dbapiModuleName=pymysql +dbName=db +dbUsername=db_user +dbPassword=pass +dbHost=127.0.0.1 +dbPort=3306 \ No newline at end of file diff --git a/test/resources/config_files/pymysql/simple_default_alias.cfg b/test/resources/config_files/pymysql/simple_default_alias.cfg new file mode 100644 index 00000000..d4242af6 --- /dev/null +++ b/test/resources/config_files/pymysql/simple_default_alias.cfg @@ -0,0 +1,7 @@ +[default] +db_module=pymysql +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=3306 \ No newline at end of file diff --git a/test/resources/config_files/pymysql/some_basic_params_missing.cfg b/test/resources/config_files/pymysql/some_basic_params_missing.cfg new file mode 100644 index 00000000..f6c24e4e --- /dev/null +++ b/test/resources/config_files/pymysql/some_basic_params_missing.cfg @@ -0,0 +1,3 @@ +[default] +db_module=pymysql +db_name=db \ No newline at end of file diff --git a/test/resources/config_files/pymysql/valid_custom_params.cfg b/test/resources/config_files/pymysql/valid_custom_params.cfg new file mode 100644 index 00000000..dcd264a0 --- /dev/null +++ b/test/resources/config_files/pymysql/valid_custom_params.cfg @@ -0,0 +1,7 @@ +[default] +db_module=pymysql +db_name=db +user=db_user +password=pass +db_host=127.0.0.1 +db_port=3306 \ No newline at end of file diff --git a/test/resources/config_files/pymysql/wrong_password.cfg b/test/resources/config_files/pymysql/wrong_password.cfg new file mode 100644 index 00000000..5d8921ef --- /dev/null +++ b/test/resources/config_files/pymysql/wrong_password.cfg @@ -0,0 +1,7 @@ +[default] +db_module=pymysql +db_name=db +db_user=db_user +db_password=wrong +db_host=127.0.0.1 +db_port=3306 \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/charset_invalid.cfg b/test/resources/config_files/pyodbc/charset_invalid.cfg new file mode 100644 index 00000000..7020a6fd --- /dev/null +++ b/test/resources/config_files/pyodbc/charset_invalid.cfg @@ -0,0 +1,9 @@ +[default] +db_module=pyodbc +db_name=db +user=db_user +password=pass +db_host=127.0.0.1 +db_port=3306 +odbc_driver={MySQL ODBC 9.2 ANSI Driver} +db_charset=wrong \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/custom_param_password.cfg b/test/resources/config_files/pyodbc/custom_param_password.cfg new file mode 100644 index 00000000..7f64dd2b --- /dev/null +++ b/test/resources/config_files/pyodbc/custom_param_password.cfg @@ -0,0 +1,7 @@ +[default] +db_module=pyodbc +db_name=db +PWD=pass +db_host=127.0.0.1 +db_port=3306 +odbc_driver={MySQL ODBC 9.2 ANSI Driver} \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/invalid_custom_params.cfg b/test/resources/config_files/pyodbc/invalid_custom_params.cfg new file mode 100644 index 00000000..e40c70a7 --- /dev/null +++ b/test/resources/config_files/pyodbc/invalid_custom_params.cfg @@ -0,0 +1,9 @@ +[default] +db_module=pyodbc +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=3306 +odbc_driver={MySQL ODBC 9.2 ANSI Driver} +blah=blah \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/old_param_names.cfg b/test/resources/config_files/pyodbc/old_param_names.cfg new file mode 100644 index 00000000..3698fd26 --- /dev/null +++ b/test/resources/config_files/pyodbc/old_param_names.cfg @@ -0,0 +1,8 @@ +[default] +dbapiModuleName=pyodbc +dbName=db +dbUsername=db_user +dbPassword=pass +dbHost=127.0.0.1 +dbPort=3306 +dbDriver={MySQL ODBC 9.2 ANSI Driver} \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/simple_default_alias.cfg b/test/resources/config_files/pyodbc/simple_default_alias.cfg new file mode 100644 index 00000000..78cbcbfb --- /dev/null +++ b/test/resources/config_files/pyodbc/simple_default_alias.cfg @@ -0,0 +1,8 @@ +[default] +db_module=pyodbc +db_name=db +db_user=db_user +db_password=pass +db_host=127.0.0.1 +db_port=3306 +odbc_driver={MySQL ODBC 9.2 ANSI Driver} \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/some_basic_params_missing.cfg b/test/resources/config_files/pyodbc/some_basic_params_missing.cfg new file mode 100644 index 00000000..16a3448f --- /dev/null +++ b/test/resources/config_files/pyodbc/some_basic_params_missing.cfg @@ -0,0 +1,3 @@ +[default] +db_module=pyodbc +db_name=db \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/valid_custom_params.cfg b/test/resources/config_files/pyodbc/valid_custom_params.cfg new file mode 100644 index 00000000..fd441a7b --- /dev/null +++ b/test/resources/config_files/pyodbc/valid_custom_params.cfg @@ -0,0 +1,8 @@ +[default] +db_module=pyodbc +db_name=db +UID=db_user +PWD=pass +db_host=127.0.0.1 +db_port=3306 +odbc_driver={MySQL ODBC 9.2 ANSI Driver} \ No newline at end of file diff --git a/test/resources/config_files/pyodbc/wrong_password.cfg b/test/resources/config_files/pyodbc/wrong_password.cfg new file mode 100644 index 00000000..9f50b420 --- /dev/null +++ b/test/resources/config_files/pyodbc/wrong_password.cfg @@ -0,0 +1,8 @@ +[default] +db_module=pyodbc +db_name=db +db_user=db_user +db_password=wrong +db_host=127.0.0.1 +db_port=3306 +odbc_driver={MySQL ODBC 9.2 ANSI Driver} \ No newline at end of file diff --git a/test/resources/config_files/sqlite3/old_param_names.cfg b/test/resources/config_files/sqlite3/old_param_names.cfg new file mode 100644 index 00000000..4a8047b0 --- /dev/null +++ b/test/resources/config_files/sqlite3/old_param_names.cfg @@ -0,0 +1,4 @@ +[default] +dbapiModuleName=sqlite3 +database=./${DBName}.db +isolation_level= \ No newline at end of file diff --git a/test/resources/config_files/sqlite3/simple_default_alias.cfg b/test/resources/config_files/sqlite3/simple_default_alias.cfg new file mode 100644 index 00000000..bdc48bd4 --- /dev/null +++ b/test/resources/config_files/sqlite3/simple_default_alias.cfg @@ -0,0 +1,4 @@ +[default] +db_module=sqlite3 +database=./${DBName}.db +isolation_level= \ No newline at end of file diff --git a/test/resources/create_stored_procedures_mssql.sql b/test/resources/create_stored_procedures_mssql.sql new file mode 100644 index 00000000..14c2ae85 --- /dev/null +++ b/test/resources/create_stored_procedures_mssql.sql @@ -0,0 +1,69 @@ +DROP PROCEDURE IF EXISTS no_params; +CREATE PROCEDURE no_params +AS +BEGIN +-- Do nothing +RETURN; +END; + +DROP PROCEDURE IF EXISTS get_second_name; +CREATE PROCEDURE +get_second_name +@person_first_name VARCHAR(20) +AS +BEGIN +SELECT LAST_NAME +FROM person +WHERE FIRST_NAME = @person_first_name; +RETURN; +END; + +DROP PROCEDURE IF EXISTS get_all_second_names; +CREATE PROCEDURE get_all_second_names +AS +BEGIN +SELECT LAST_NAME FROM person; +RETURN; +END; + +DROP PROCEDURE IF EXISTS get_all_first_and_second_names; +CREATE PROCEDURE get_all_first_and_second_names +AS +BEGIN +SELECT FIRST_NAME FROM person; +SELECT LAST_NAME FROM person; +RETURN; +END; + +DROP PROCEDURE IF EXISTS check_condition; +CREATE PROCEDURE check_condition +AS +BEGIN +DECLARE @v_condition BIT; +SET @v_condition = 1; +IF @v_condition = 1 +BEGIN +PRINT 'Condition is true'; +END +ELSE +BEGIN +PRINT 'Condition is false'; +END +END; + +DROP PROCEDURE IF EXISTS return_out_param_without_result_sets; +CREATE PROCEDURE +return_out_param_without_result_sets +@my_input VARCHAR(20), +@my_output INT OUTPUT +AS +BEGIN + IF @my_input = 'give me 1' + BEGIN + SELECT @my_output = 1; + END + ELSE + BEGIN + SELECT @my_output = 0; + END +END; \ No newline at end of file diff --git a/test/resources/create_stored_procedures_mysql.sql b/test/resources/create_stored_procedures_mysql.sql new file mode 100644 index 00000000..5da8b262 --- /dev/null +++ b/test/resources/create_stored_procedures_mysql.sql @@ -0,0 +1,41 @@ +DROP PROCEDURE IF EXISTS no_params; +CREATE PROCEDURE +no_params() +BEGIN +-- Do nothing +END; + +DROP PROCEDURE IF EXISTS get_second_name; +CREATE PROCEDURE +get_second_name (IN person_first_name VARCHAR(20), +OUT person_second_name VARCHAR(20)) +BEGIN +SELECT LAST_NAME +INTO person_second_name +FROM person +WHERE FIRST_NAME = person_first_name; +END; + +DROP PROCEDURE IF EXISTS get_all_second_names; +CREATE PROCEDURE get_all_second_names() +BEGIN +SELECT LAST_NAME FROM person; +END; + +DROP PROCEDURE IF EXISTS get_all_first_and_second_names; +CREATE PROCEDURE get_all_first_and_second_names() +BEGIN +SELECT FIRST_NAME FROM person; +SELECT LAST_NAME FROM person; +END; + +DROP PROCEDURE IF EXISTS check_condition; +CREATE PROCEDURE check_condition() +BEGIN + DECLARE v_condition BOOLEAN DEFAULT TRUE; + IF v_condition THEN + SELECT 'Condition is true' AS Result; + ELSE + SELECT 'Condition is false' AS Result; + END IF; +END \ No newline at end of file diff --git a/test/resources/create_stored_procedures_oracle.sql b/test/resources/create_stored_procedures_oracle.sql new file mode 100644 index 00000000..5e155da3 --- /dev/null +++ b/test/resources/create_stored_procedures_oracle.sql @@ -0,0 +1,43 @@ +CREATE OR REPLACE PROCEDURE +no_params AS +BEGIN +DBMS_OUTPUT.PUT_LINE('Hello, World!'); +END; + +CREATE OR REPLACE PROCEDURE +get_second_name (person_first_name IN VARCHAR, person_second_name OUT VARCHAR) AS +BEGIN +SELECT last_name +INTO person_second_name +FROM person +WHERE first_name = person_first_name; +END; + +CREATE OR REPLACE PROCEDURE +get_all_second_names (second_names_cursor OUT SYS_REFCURSOR) AS +BEGIN +OPEN second_names_cursor for +SELECT LAST_NAME FROM person; +END; + +-- parsing the SQL file fails because of the semicolon before the opening of the second cursor +-- , but it's needed +CREATE OR REPLACE PROCEDURE +get_all_first_and_second_names (first_names_cursor OUT SYS_REFCURSOR, second_names_cursor OUT SYS_REFCURSOR) AS +BEGIN +OPEN first_names_cursor for +SELECT FIRST_NAME FROM person; +OPEN second_names_cursor for +SELECT LAST_NAME FROM person; +END; + +CREATE OR REPLACE PROCEDURE +check_condition AS +v_condition BOOLEAN := TRUE; +BEGIN +IF v_condition THEN +DBMS_OUTPUT.PUT_LINE('Condition is true'); +ELSE +DBMS_OUTPUT.PUT_LINE('Condition is false'); +END IF; +END check_condition; \ No newline at end of file diff --git a/test/resources/create_stored_procedures_postgres.sql b/test/resources/create_stored_procedures_postgres.sql new file mode 100644 index 00000000..158f547e --- /dev/null +++ b/test/resources/create_stored_procedures_postgres.sql @@ -0,0 +1,71 @@ +DROP ROUTINE IF EXISTS no_params; +CREATE FUNCTION no_params() +RETURNS VOID +LANGUAGE plpgsql +AS +' +BEGIN +-- Do nothing +END +'; + +DROP ROUTINE IF EXISTS get_second_name; +CREATE FUNCTION +get_second_name (IN person_first_name VARCHAR(20), +OUT person_second_name VARCHAR(20)) +LANGUAGE plpgsql +AS +' +BEGIN +SELECT LAST_NAME INTO person_second_name +FROM person +WHERE FIRST_NAME = person_first_name; +END +'; + +DROP ROUTINE IF EXISTS get_all_second_names; +CREATE FUNCTION +get_all_second_names() +RETURNS TABLE (second_names VARCHAR(20)) +LANGUAGE plpgsql +AS +' +BEGIN +RETURN QUERY SELECT LAST_NAME FROM person; +END +'; + + +DROP ROUTINE IF EXISTS get_all_first_and_second_names; +CREATE FUNCTION +get_all_first_and_second_names(result1 refcursor, result2 refcursor) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS +' +BEGIN +OPEN result1 FOR SELECT FIRST_NAME FROM person; +RETURN NEXT result1; +OPEN result2 FOR SELECT LAST_NAME FROM person; +RETURN NEXT result2; +END +'; + +DROP ROUTINE IF EXISTS check_condition; +CREATE FUNCTION +check_condition() +RETURNS VOID +LANGUAGE plpgsql +AS +' +DECLARE + v_condition BOOLEAN := TRUE; + v_res BOOLEAN := TRUE; +BEGIN + IF v_condition THEN + v_res := TRUE; + ELSE + v_res := FALSE; + END IF; +END +'; \ No newline at end of file diff --git a/test/excel_db_test_insertData.sql b/test/resources/excel_db_test_insertData.sql similarity index 100% rename from test/excel_db_test_insertData.sql rename to test/resources/excel_db_test_insertData.sql diff --git a/test/my_db_test_insertData.sql b/test/resources/insert_data_in_person_table.sql similarity index 100% rename from test/my_db_test_insertData.sql rename to test/resources/insert_data_in_person_table.sql diff --git a/test/resources/insert_data_in_person_table_utf8.sql b/test/resources/insert_data_in_person_table_utf8.sql new file mode 100644 index 00000000..7a15eeb9 --- /dev/null +++ b/test/resources/insert_data_in_person_table_utf8.sql @@ -0,0 +1 @@ +INSERT INTO person VALUES(1,'Jürgen','Gernegroß') \ No newline at end of file diff --git a/test/resources/script_file_tests/semicolons_and_quotes_in_values.sql b/test/resources/script_file_tests/semicolons_and_quotes_in_values.sql new file mode 100644 index 00000000..d5a846a1 --- /dev/null +++ b/test/resources/script_file_tests/semicolons_and_quotes_in_values.sql @@ -0,0 +1 @@ +INSERT INTO person VALUES(5, 'Miles', 'O''Brian'); \ No newline at end of file diff --git a/test/resources/script_file_tests/semicolons_in_values.sql b/test/resources/script_file_tests/semicolons_in_values.sql new file mode 100644 index 00000000..47f6f3f4 --- /dev/null +++ b/test/resources/script_file_tests/semicolons_in_values.sql @@ -0,0 +1,2 @@ +INSERT INTO person VALUES(3, 'Hello; world', 'Another; value'); +INSERT INTO person VALUES(4, 'May the Force; ', 'be with you;'); \ No newline at end of file diff --git a/test/resources/script_file_tests/statements_in_one_line.sql b/test/resources/script_file_tests/statements_in_one_line.sql new file mode 100644 index 00000000..444900cf --- /dev/null +++ b/test/resources/script_file_tests/statements_in_one_line.sql @@ -0,0 +1 @@ +INSERT INTO person VALUES(6, 'Julian', 'Bashir'); INSERT INTO person VALUES(7, 'Jadzia', 'Dax'); \ No newline at end of file diff --git a/test/testing.sql b/test/testing.sql deleted file mode 100644 index d63dd0ed..00000000 --- a/test/testing.sql +++ /dev/null @@ -1,2 +0,0 @@ -# Simple sql file for testing with RobotFramework-DatabaseLibrary (Python) -SELECT COUNT(*) FROM data_formats; diff --git a/test/tests/__init__.py b/test/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/__init__.robot b/test/tests/__init__.robot new file mode 100644 index 00000000..e62e8551 --- /dev/null +++ b/test/tests/__init__.robot @@ -0,0 +1,100 @@ +*** Settings *** +Documentation Set DB connection variables based on a single global variable +... which can be passed from outside (e.g. VS Code lauch config) + +Suite Setup Set DB Variables + + +*** Variables *** +${GLOBAL_DB_SELECTOR} None + + +*** Keywords *** +Set DB Variables + [Documentation] These are custom connection params for databases, + ... running locally on the developer's machine. + ... You might need other values for your databases! + IF "${GLOBAL_DB_SELECTOR}" == "PostgreSQL" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} psycopg2 + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 5432 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} db_user + Set Global Variable ${DB_PASS} pass + ELSE IF "${GLOBAL_DB_SELECTOR}" == "oracledb" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} oracledb + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 1521 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} db_user + Set Global Variable ${DB_PASS} pass + ELSE IF "${GLOBAL_DB_SELECTOR}" == "cx_Oracle" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} cx_Oracle + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 1521 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} db_user + Set Global Variable ${DB_PASS} pass + ELSE IF "${GLOBAL_DB_SELECTOR}" == "SQLite" + Set Global Variable ${DB_MODULE_MODE} custom + Set Global Variable ${DB_MODULE} sqlite3 + ELSE IF "${GLOBAL_DB_SELECTOR}" == "IBM_DB2" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} ibm_db_dbi + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 50000 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} db_user + Set Global Variable ${DB_PASS} pass + ELSE IF "${GLOBAL_DB_SELECTOR}" == "Teradata" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} teradata + Set Global Variable ${DB_HOST} 192.168.0.231 + Set Global Variable ${DB_PORT} 1025 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} dbc + Set Global Variable ${DB_PASS} dbc + ELSE IF "${GLOBAL_DB_SELECTOR}" == "MySQL_pymysql" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} pymysql + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 3306 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} db_user + Set Global Variable ${DB_PASS} pass + ELSE IF "${GLOBAL_DB_SELECTOR}" == "MySQL_pyodbc" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} pyodbc + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 3306 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} db_user + Set Global Variable ${DB_PASS} pass + ELSE IF "${GLOBAL_DB_SELECTOR}" == "Oracle_JDBC" + Set Global Variable ${DB_MODULE_MODE} custom + Set Global Variable ${DB_MODULE} jaydebeapi + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 1521 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} db_user + Set Global Variable ${DB_PASS} pass + ELSE IF "${GLOBAL_DB_SELECTOR}" == "MSSQL" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} pymssql + Set Global Variable ${DB_HOST} 127.0.0.1 + Set Global Variable ${DB_PORT} 1433 + Set Global Variable ${DB_NAME} db + Set Global Variable ${DB_USER} SA + Set Global Variable ${DB_PASS} MyPass1234! + ELSE IF "${GLOBAL_DB_SELECTOR}" == "Excel" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} excel + Set Global Variable ${DB_NAME} db + ELSE IF "${GLOBAL_DB_SELECTOR}" == "Excel_RW" + Set Global Variable ${DB_MODULE_MODE} standard + Set Global Variable ${DB_MODULE} excelrw + Set Global Variable ${DB_NAME} db + END diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot new file mode 100644 index 00000000..e61b3904 --- /dev/null +++ b/test/tests/common_tests/aliased_connection.robot @@ -0,0 +1,132 @@ +*** Settings *** +Resource ../../resources/common.resource +Suite Setup Skip If "${DB_MODULE}" == "sqlite3" +... Aliases tests don't work for SQLite as each connection is always a new file + +Test Setup Connect, Create Some Data And Disconnect +Test Teardown Connect, Clean Up Data And Disconnect + + +*** Test Cases *** +Connections Can Be Aliased + Connect To DB # default alias + Connect To DB alias=second + +Default Alias Can Be Empty + Connect To DB # default alias + Query SELECT * FROM person + Connect To DB alias=second + Query SELECT * FROM person + Query SELECT * FROM person alias=second + +Switch From Default And Disconnect + Connect To DB # default alias + Connect To DB alias=second + Switch Database second + Query SELECT * FROM person # query with 'second' connection + Disconnect From Database alias=second + Query SELECT * FROM person # query with 'default' connection + +Disconnect Not Existing Alias + Connect To DB # default alias + Disconnect From Database alias=idontexist # silent warning + Run Keyword And Expect Error ConnectionError: No open database connection to close + ... Disconnect From Database alias=idontexist error_if_no_connection=${True} + # default alias exist and can be closed + Disconnect From Database error_if_no_connection=${True} + +Switch Not Existing Alias + Run Keyword And Expect Error ValueError: Alias 'second' not found in existing connections. + ... Switch Database second + +Execute SQL Script - Insert Data In Person table + [Setup] Connect, Create Some Data And Disconnect Run SQL script=${False} + Connect To DB alias=aliased_conn + ${output} Insert Data In Person Table Using SQL Script alias=aliased_conn + Should Be Equal As Strings ${output} None + +Check If Exists In DB - Franz Allan + Connect To DB alias=aliased_conn + Check If Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Franz Allan' alias=aliased_conn + +Check If Not Exists In DB - Joe + Connect To DB alias=aliased_conn + Check If Not Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Joe' alias=aliased_conn + +Table Must Exist - person + Connect To DB alias=aliased_conn + Table Must Exist person alias=aliased_conn + +Verify Row Count is 0 + Connect To DB alias=aliased_conn + Row Count is 0 SELECT * FROM person WHERE FIRST_NAME= 'NotHere' alias=aliased_conn + +Verify Row Count is Equal to X + Connect To DB alias=aliased_conn + Row Count is Equal to X SELECT id FROM person 2 alias=aliased_conn + +Verify Row Count is Less Than X + Connect To DB alias=aliased_conn + Row Count is Less Than X SELECT id FROM person 3 alias=aliased_conn + +Verify Row Count is Greater Than X + Connect To DB alias=aliased_conn + Row Count is Greater Than X SELECT * FROM person 1 alias=aliased_conn + +Retrieve Row Count + Connect To DB alias=aliased_conn + ${output} Row Count SELECT id FROM person alias=aliased_conn + Log ${output} + Should Be Equal As Strings ${output} 2 + +Retrieve records from person table + Connect To DB alias=aliased_conn + ${output} Execute SQL String SELECT * FROM person + Log ${output} + Should Be Equal As Strings ${output} None + +Use Last Connected If Not Alias Provided + Connect To DB alias=aliased_conn + ${output} Query SELECT COUNT(*) FROM person + Log ${output} + Should Be Equal As Integers ${output}[0][0] 2 + +Verify Query - Get results as a list of dictionaries + Connect To DB alias=aliased_conn + ${output} Query SELECT * FROM person returnAsDict=True alias=aliased_conn + Log ${output} + # some databases lower field names and you can't do anything about it + TRY + ${value 1} Get From Dictionary ${output}[0] FIRST_NAME + EXCEPT Dictionary does not contain key 'FIRST_NAME'. + ${value 1} Get From Dictionary ${output}[0] first_name + END + TRY + ${value 2} Get From Dictionary ${output}[1] FIRST_NAME + EXCEPT Dictionary does not contain key 'FIRST_NAME'. + ${value 2} Get From Dictionary ${output}[1] first_name + END + Should Be Equal As Strings ${value 1} Franz Allan + Should Be Equal As Strings ${value 2} Jerry + +Verify Delete All Rows From Table + Connect To DB alias=aliased_conn + Delete All Rows From Table person alias=aliased_conn + Row Count Is 0 SELECT * FROM person alias=aliased_conn + + +*** Keywords *** +Connect, Create Some Data And Disconnect + [Arguments] ${Run SQL script}=${True} + Connect To DB + Create Person Table + IF $Run_SQL_script + Insert Data In Person Table Using SQL Script + END + Disconnect From Database + +Connect, Clean Up Data And Disconnect + Disconnect From All Databases + Connect To DB + Drop Tables Person And Foobar + Disconnect From Database diff --git a/test/tests/common_tests/assertion_error_messages.robot b/test/tests/common_tests/assertion_error_messages.robot new file mode 100644 index 00000000..2ab45a3c --- /dev/null +++ b/test/tests/common_tests/assertion_error_messages.robot @@ -0,0 +1,138 @@ +*** Settings *** +Documentation Simulate keyword fails and check that +... using custom and starndard error messages work as expected + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table And Insert Data +Test Teardown Drop Tables Person And Foobar + + +*** Variables *** +${Error Message} My error message +${Non Existing Select} SELECT id FROM person WHERE first_name = 'Joe' +${Existing Select} SELECT id FROM person WHERE first_name = 'Franz Allan' + + +*** Test Cases *** +Check If Exists In DB Fails + ${expected error}= Catenate + ... Expected to have have at least one row, but got 0 rows from: + ... '${Non Existing Select}' + Run Keyword And Expect Error ${expected error} + ... Check If Exists In Database ${Non Existing Select} + +Check If Exists In DB Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Check If Exists In Database ${Non Existing Select} + ... msg=${Error Message} + +Check If Not Exists In DB Fails + ${expected error}= Catenate + ... Expected to have have no rows from + ... '${Existing Select}', + ... but got some rows: * + Run Keyword And Expect Error ${expected error} + ... Check If Not Exists In Database ${Existing Select} + +Check If Not Exists In DB Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Check If Not Exists In Database ${Existing Select} + ... msg=${Error Message} + +Table Must Exist Fails + ${expected error}= Catenate + ... Table 'nonexistent' does not exist in the db + Run Keyword And Expect Error ${expected error} + ... Table Must Exist nonexistent + +Table Must Exist Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Table Must Exist nonexistent + ... msg=${Error Message} + +Verify Row Count Is 0 Fails + ${expected error}= Catenate + ... Expected 0 rows, but 1 were returned from: + ... '${Existing Select}' + Run Keyword And Expect Error ${expected error} + ... Row Count Is 0 ${Existing Select} + +Verify Row Count Is 0 Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Row Count Is 0 ${Existing Select} + ... msg=${Error Message} + +Verify Row Count Is Equal To X Fails + ${expected error}= Catenate + ... Expected 9 rows, but 1 were returned from: + ... '${Existing Select}' + Run Keyword And Expect Error ${expected error} + ... Row Count Is Equal To X ${Existing Select} 9 + +Verify Row Count Is Equal To X Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Row Count Is Equal To X + ... ${Existing Select} 9 msg=${Error Message} + +Verify Row Count Is Less Than X Fails + ${expected error}= Catenate + ... Expected less than 1 rows, but 1 were returned from + ... '${Existing Select}' + Run Keyword And Expect Error ${expected error} + ... Row Count Is Less Than X + ... ${Existing Select} 1 + +Verify Row Count Is Less Than X Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Row Count Is Less Than X + ... ${Existing Select} 1 msg=${Error Message} + +Verify Row Count Is Greater Than X Fails + ${expected error}= Catenate + ... Expected more than 1 rows, but 1 were returned from + ... '${Existing Select}' + Run Keyword And Expect Error ${expected error} + ... Row Count Is Greater Than X + ... ${Existing Select} 1 + +Verify Row Count Is Greater Than X Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Row Count Is Greater Than X + ... ${Existing Select} 1 + ... msg=${Error Message} + +Check Row Count With Assertion Engine Fails + ${expected value}= Set Variable 5 + ${expected error}= Catenate + ... Wrong row count: '1' (int) should be '${expected value}' (int) + Run Keyword And Expect Error + ... ${expected error} + ... Check Row Count ${Existing Select} equals ${expected value} + +Check Row Count With Assertion Engine Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Check Row Count ${Existing Select} less than 1 + ... assertion_message=${Error Message} + + +Check Query Result With Assertion Engine Fails + ${expected value}= Set Variable ${5} + IF "${DB_MODULE}" == "jaydebeapi" + VAR ${Num Type}= jlong + ELSE + VAR ${Num Type}= int + END + ${expected error}= Catenate + ... Wrong query result: '1' (${Num Type}) should be '${expected value}' (int) + Run Keyword And Expect Error + ... ${expected error} + ... Check Query Result ${Existing Select} equals ${expected value} + + +Check Query Result With Assertion Engine Fails With Message + Run Keyword And Expect Error ${Error Message} + ... Check Query Result ${Existing Select} less than ${1} + ... assertion_message=${Error Message} diff --git a/test/tests/common_tests/assertion_retry.robot b/test/tests/common_tests/assertion_retry.robot new file mode 100644 index 00000000..47921da3 --- /dev/null +++ b/test/tests/common_tests/assertion_retry.robot @@ -0,0 +1,60 @@ +*** Settings *** +Documentation Tests for assertion keywords with retry mechanism + +Resource ../../resources/common.resource + +Suite Setup Connect To DB And Prepare Data +Suite Teardown Delete Data And Disconnect +Test Setup Save Start Time + +*** Variables *** +${Timeout} ${3} +${Tolerance} ${0.5} +${Request} SELECT first_name FROM person + +*** Test Cases *** +Check Query Results With Timeout - Fast If DB Ready + Check Query Result ${Request} contains Allan retry_timeout=${Timeout} seconds + ${End time}= Get Current Date + ${Execution time}= Subtract Date From Date ${End time} ${START_TIME} + Should Be True 0 <= $Execution_time <= $Tolerance + +Check Query Results With Timeout - Slow If Result Wrong + Run Keyword And Expect Error Wrong query result: 'Franz Allan' (str) should contain 'Blah' (str) + ... Check Query Result ${Request} contains Blah retry_timeout=${Timeout} seconds retry_pause=1s + ${End time}= Get Current Date + ${Execution time}= Subtract Date From Date ${End time} ${START_TIME} + Should Be True $Timeout <= $Execution_time <= $Timeout + $Tolerance + +Check Query Results With Timeout - Slow If Row Count Wrong + Run Keyword And Expect Error Checking row '5' is not possible, as query results contain 2 rows only! + ... Check Query Result ${Request} contains Blah row=5 retry_timeout=${Timeout} seconds + ${End time}= Get Current Date + ${Execution time}= Subtract Date From Date ${End time} ${START_TIME} + Should Be True $Timeout <= $Execution_time <= $Timeout + $Tolerance + +Check Row Count With Timeout - Fast If DB Ready + Check Row Count ${Request} == 2 retry_timeout=${Timeout} seconds + ${End time}= Get Current Date + ${Execution time}= Subtract Date From Date ${End time} ${START_TIME} + Should Be True 0 <= $Execution_time <= $Tolerance + +Check Row Count With Timeout - Slow If Result Wrong + Run Keyword And Expect Error Wrong row count: '2' (int) should be greater than '5' (int) + ... Check Row Count ${Request} > 5 retry_timeout=${Timeout} seconds retry_pause=1s + ${End time}= Get Current Date + ${Execution time}= Subtract Date From Date ${End time} ${START_TIME} + Should Be True $Timeout <= $Execution_time <= $Timeout + $Tolerance + +*** Keywords *** +Connect To DB And Prepare Data + Connect To DB + Create Person Table And Insert Data + +Delete Data And Disconnect + Drop Tables Person And Foobar + Disconnect From Database + +Save Start Time + ${START_TIME}= Get Current Date + Set Suite Variable ${START_TIME} diff --git a/test/tests/common_tests/basic_tests.robot b/test/tests/common_tests/basic_tests.robot new file mode 100644 index 00000000..adbd93ed --- /dev/null +++ b/test/tests/common_tests/basic_tests.robot @@ -0,0 +1,159 @@ +*** Settings *** +Documentation Tests which work with the same input params across all databases. + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table And Insert Data +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +SQL Statement Ending Without Semicolon Works + Query SELECT * FROM person + +SQL Statement Ending With Semicolon Works + Query SELECT * FROM person; + +Create Person Table + [Setup] Log No setup for this test + ${output}= Create Person Table + Should Be Equal As Strings ${output} None + +Execute SQL Script - Insert Data In Person table + [Setup] Create Person Table + ${output}= Insert Data In Person Table Using SQL Script + Should Be Equal As Strings ${output} None + +Execute SQL String - Create Foobar Table + [Setup] Log No setup for this test + ${output}= Create Foobar Table + Should Be Equal As Strings ${output} None + +Simple Select With Multiple Rows + ${output}= Query select LAST_NAME from person + Length Should Be ${output} 2 + Should Be Equal ${output}[0][0] See + Should Be Equal ${output}[1][0] Schneider + +Check If Exists In DB - Franz Allan + Check If Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Franz Allan' + +Check If Not Exists In DB - Joe + Check If Not Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Joe' + +Table Must Exist - person + Table Must Exist person + +Verify Row Count is 0 + Row Count is 0 SELECT * FROM person WHERE FIRST_NAME= 'NotHere' + +Verify Row Count is Equal to X + Row Count is Equal to X SELECT id FROM person 2 + +Verify Row Count is Less Than X + Row Count is Less Than X SELECT id FROM person 3 + +Verify Row Count is Greater Than X + Row Count is Greater Than X SELECT * FROM person 1 + +Retrieve Row Count + ${output}= Row Count SELECT id FROM person + Log ${output} + Should Be Equal As Strings ${output} 2 + +Check Row Count With Assertion Engine + Check Row Count SELECT id FROM person == 2 + +Check Query Result With Assertion Engine + Check Query Result SELECT first_name FROM person contains Allan + +Check Query Result With Assertion Engine - Different Row And Col + Check Query Result SELECT first_name, last_name, id FROM person >= ${2} row=1 col=2 + +Check Query Result With Assertion Engine - Row Out Of Range + Run Keyword And Expect Error Checking row '2' is not possible, as query results contain 2 rows only! + ... Check Query Result SELECT first_name FROM person == Blah row=2 + +Check Query Result With Assertion Engine - Col Out Of Range + Run Keyword And Expect Error Checking column '5' is not possible, as query results contain 2 columns only! + ... Check Query Result SELECT id, first_name FROM person == Blah col=5 + +Retrieve records from person table + ${output}= Execute SQL String SELECT * FROM person + Log ${output} + Should Be Equal As Strings ${output} None + +Verify Query - Row Count person table + ${output}= Query SELECT COUNT(*) FROM person + Log ${output} + Should Be Equal As Integers ${output}[0][0] 2 + +Verify Query - Row Count foobar table + [Setup] Create Foobar Table + ${output}= Query SELECT COUNT(*) FROM foobar + Log ${output} + Should Be Equal As Integers ${output}[0][0] 0 + +Verify Query - Get results as a list of dictionaries + ${output}= Query SELECT * FROM person returnAsDict=True + Log ${output} + # some databases lower field names and you can't do anything about it + TRY + ${value 1}= Get From Dictionary ${output}[0] FIRST_NAME + EXCEPT Dictionary does not contain key 'FIRST_NAME'. + ${value 1}= Get From Dictionary ${output}[0] first_name + END + TRY + ${value 2}= Get From Dictionary ${output}[1] FIRST_NAME + EXCEPT Dictionary does not contain key 'FIRST_NAME'. + ${value 2}= Get From Dictionary ${output}[1] first_name + END + Should Be Equal As Strings ${value 1} Franz Allan + Should Be Equal As Strings ${value 2} Jerry + +Return As Dictionary - Dotted Syntax + ${output}= Query SELECT * FROM person return_dict=True + ${field_names}= Get Dictionary Keys ${output}[0] + IF "FIRST_NAME" in $field_names + VAR ${field_name}= FIRST_NAME + ELSE IF "first_name" in $field_names + VAR ${field_name}= first_name + ELSE + FAIL Unexpected field name in dictionary + END + Should Be Equal As Strings ${output[0].${field_name}} Franz Allan + Should Be Equal As Strings ${output[1].${field_name}} Jerry + +Verify Execute SQL String - Row Count person table + ${output}= Execute SQL String SELECT COUNT(*) FROM person + Log ${output} + Should Be Equal As Strings ${output} None + +Verify Execute SQL String - Row Count foobar table + [Setup] Create Foobar Table + ${output}= Execute SQL String SELECT COUNT(*) FROM foobar + Log ${output} + Should Be Equal As Strings ${output} None + +Insert Data Into Table foobar + [Setup] Create Foobar Table + ${output}= Execute SQL String INSERT INTO foobar VALUES(1,'Jerry') + Log ${output} + Should Be Equal As Strings ${output} None + +Verify Query - Row Count foobar table 1 row + [Setup] Create Foobar Table And Insert Data + ${output}= Query SELECT COUNT(*) FROM foobar + Log ${output} + Should Be Equal As Integers ${output}[0][0] 1 + +Verify Delete All Rows From Table - foobar + [Setup] Create Foobar Table And Insert Data + Delete All Rows From Table foobar + +Verify Query - Row Count foobar table 0 row + [Setup] Create Foobar Table And Insert Data + Delete All Rows From Table foobar + Row Count Is 0 SELECT * FROM foobar diff --git a/test/tests/common_tests/connection_params.robot b/test/tests/common_tests/connection_params.robot new file mode 100644 index 00000000..75f59ab5 --- /dev/null +++ b/test/tests/common_tests/connection_params.robot @@ -0,0 +1,174 @@ +*** Settings *** +Documentation Tests for the basic _Connect To Database_ keyword - with and without config files. +... The parameter handling is partly DB module specific. + +Resource ../../resources/common.resource + +Test Setup Skip If $DB_MODULE == "sqlite3" or $DB_MODULE == "jaydebeapi" +Test Teardown Disconnect From Database + +*** Variables *** +&{Errors psycopg2} +... missing basic params=OperationalError: connection to server on socket * +... invalid custom param=ProgrammingError: invalid dsn: invalid connection option "blah"* +&{Errors oracledb} +... missing basic params=DatabaseError: DPY-4001: no credentials specified +... invalid custom param=TypeError: connect() got an unexpected keyword argument 'blah' +&{Errors pymssql} +... missing basic params=OperationalError: (20002, b'DB-Lib error message 20002, severity 9* +... invalid custom param=TypeError: connect() got an unexpected keyword argument 'blah' +&{Errors pymysql} +... missing basic params=OperationalError: (1045, "Access denied* +... invalid custom param=REGEXP: TypeError.*__init__.*got an unexpected keyword argument 'blah' +&{Errors pyodbc} +... missing basic params=REGEXP: InterfaceError.*Data source name not found and no default driver specified.* + +&{Errors} +... psycopg2=${Errors psycopg2} +... oracledb=${Errors oracledb} +... pymssql=${Errors pymssql} +... pymysql=${Errors pymysql} +... pyodbc=${Errors pyodbc} + + +*** Test Cases *** +Mandatory params can't be missing + Run Keyword And Expect Error + ... ValueError: Required parameter 'db_module' was not provided* + ... Connect To Database db_name=${DB_NAME} + +All basic params, no config file + Connect To Database + ... db_module=${DB_MODULE} + ... db_name=${DB_NAME} + ... db_user=${DB_USER} + ... db_password=${DB_PASS} + ... db_host=${DB_HOST} + ... db_port=${DB_PORT} + ... odbc_driver=${DB_DRIVER} + +Missing basic params are accepted, error from Python DB module + Run Keyword And Expect Error + ... ${Errors}[${DB_MODULE}][missing basic params] + ... Connect To Database + ... db_module=${DB_MODULE} + +Custom params as keyword args - valid + Connect To Database + ... db_module=${DB_MODULE} + ... db_name=${DB_NAME} + ... db_host=${DB_HOST} + ... db_port=${DB_PORT} + ... odbc_driver=${DB_DRIVER} + ... user=${DB_USER} + ... password=${DB_PASS} + +Custom params as keyword args - invalid, error from Python DB module + Skip If $DB_MODULE == "pyodbc" + ... pyodbc doesn't always throw an error if some wrong parameter was provided + Run Keyword And Expect Error + ... ${Errors}[${DB_MODULE}][invalid custom param] + ... Connect To Database + ... db_module=${DB_MODULE} + ... db_name=${DB_NAME} + ... db_host=${DB_HOST} + ... db_port=${DB_PORT} + ... db_user=${DB_USER} + ... db_password=${DB_PASS} + ... odbc_driver=${DB_DRIVER} + ... blah=blah + +All basic params in config file + Connect Using Config File ${DB_MODULE}/simple_default_alias + +Deprecated basic params in config file + Connect Using Config File ${DB_MODULE}/old_param_names + +Missing basic params in config file are accepted, error from Python DB module + Run Keyword And Expect Error + ... ${Errors}[${DB_MODULE}][missing basic params] + ... Connect Using Config File + ... ${DB_MODULE}/some_basic_params_missing + +Custom params from config file - valid + Connect Using Config File ${DB_MODULE}/valid_custom_params + +Custom params from config file - invalid, error from Python DB module + Skip If $DB_MODULE == "pyodbc" + ... pyodbc doesn't always throw an error if some wrong parameter was provided + Run Keyword And Expect Error + ... ${Errors}[${DB_MODULE}][invalid custom param] + ... Connect Using Config File ${DB_MODULE}/invalid_custom_params + +Custom params as keyword args combined with custom params from config file + Connect Using Config File ${DB_MODULE}/custom_param_password + ... user=${DB_USER} + + +Keyword args override config file values - basic params + Connect Using Config File ${DB_MODULE}/wrong_password + ... db_password=${DB_PASS} + +Keyword args override config file values - custom params + Connect Using Config File ${DB_MODULE}/valid_custom_params + ... user=${DB_USER} + +Oracle specific - basic params, no config file, oracle_driver_mode + Skip If $DB_MODULE != "oracledb" + Connect To Database + ... db_module=${DB_MODULE} + ... db_name=${DB_NAME} + ... db_user=${DB_USER} + ... db_password=${DB_PASS} + ... db_host=${DB_HOST} + ... db_port=${DB_PORT} + ... oracle_driver_mode=thin + +Oracle specific - thick mode in config file - invalid + [Documentation] Invalid as mode switch during test execution is not supported + ... This test must run the last one in the suite, after others used thin mode already. + Skip If $DB_MODULE != "oracledb" + Run Keyword And Expect Error ProgrammingError: DPY-2019: python-oracledb thick mode cannot be used * + ... Connect Using Config File ${DB_MODULE}/thick_mode + + +MSSQL / MySQL / PyODBC specific - charset as keyword argument + Skip If $DB_MODULE not in ["pymssql", "pymysql", "pyodbc"] + Connect To Database + ... db_module=${DB_MODULE} + ... db_name=${DB_NAME} + ... db_user=${DB_USER} + ... db_password=${DB_PASS} + ... db_host=${DB_HOST} + ... db_port=${DB_PORT} + ... odbc_driver=${DB_DRIVER} + ... db_charset=LATIN1 + +MSSQL specific - charset in config file - invalid + Skip If $DB_MODULE not in ["pymssql"] + Run Keyword And Expect Error OperationalError: (20002, b'Unknown error') + ... Connect Using Config File ${DB_MODULE}/charset_invalid + +MySQL specific - charset in config file - invalid + Skip If $DB_MODULE not in ["pymysql"] + Run Keyword And Expect Error AttributeError: 'NoneType' object has no attribute 'encoding' + ... Connect Using Config File ${DB_MODULE}/charset_invalid + +PyODBC specific - charset in config file - invalid + Skip If $DB_MODULE not in ["pyodbc"] + Run Keyword And Expect Error REGEXP: .*Unknown character set: 'wrong'.* + ... Connect Using Config File ${DB_MODULE}/charset_invalid + + +SQlite specific - connection params as custom keyword args + [Setup] Skip If $DB_MODULE != "sqlite3" + Remove File ${DBName}.db + Connect To Database + ... db_module=${DB_MODULE} + ... database=./${DBName}.db + ... isolation_level=${EMPTY} + +SQlite specific - custom connection params in config file + [Setup] Skip If $DB_MODULE != "sqlite3" + Remove File ${DBName}.db + Connect Using Config File ${DB_MODULE}/simple_default_alias diff --git a/test/tests/common_tests/custom_connection.robot b/test/tests/common_tests/custom_connection.robot new file mode 100644 index 00000000..1e27e8c9 --- /dev/null +++ b/test/tests/common_tests/custom_connection.robot @@ -0,0 +1,51 @@ +*** Settings *** +Documentation Keyword 'Connect To Database Using Custom Params' should work properly +... for different DB modules. + +Resource ../../resources/common.resource + +Test Teardown Disconnect From Database + + +*** Variables *** +${CONNECTION_STRING} ${EMPTY} # the variable is set dynamically depending on the current DB module + + +*** Test Cases *** +Connect Using Custom Connection String + [Documentation] Connection string provided without additional quotes should work properly. + ${Connection String}= Build Connection String + Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} + +Connect Using Custom Params + IF "${DB_MODULE}" == "oracledb" + ${Params}= Catenate + ... user='${DB_USER}', + ... password='${DB_PASS}', + ... dsn='${DB_HOST}:${DB_PORT}/${DB_NAME}' + ELSE IF "${DB_MODULE}" == "pyodbc" + ${Params}= Catenate + ... driver='${DB_DRIVER}', + ... charset='${DB_CHARSET}', + ... database='${DB_NAME}', + ... user='${DB_USER}', + ... password='${DB_PASS}', + ... host='${DB_HOST}', + ... port=${DB_PORT} + ELSE IF "${DB_MODULE}" == "sqlite3" + ${Params}= Catenate + ... database="./${DBName}.db", + ... isolation_level=None + ELSE IF "${DB_MODULE}" == "jaydebeapi" + Skip Connecting with custom params for Jaydebeapi is already done in all other tests + ELSE + ${Params}= Catenate + ... database='${DB_NAME}', + ... user='${DB_USER}', + ... password='${DB_PASS}', + ... host='${DB_HOST}', + ... port=${DB_PORT} + END + Connect To Database Using Custom Params + ... ${DB_MODULE} + ... ${Params} diff --git a/test/tests/common_tests/description.robot b/test/tests/common_tests/description.robot new file mode 100644 index 00000000..70b0dcfe --- /dev/null +++ b/test/tests/common_tests/description.robot @@ -0,0 +1,103 @@ +*** Settings *** +Documentation The result of the "description" request is very different depending on the database + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Tables Person And Foobar +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +Verify Person Description + @{queryResults} = Description SELECT * FROM person + Log Many @{queryResults} + Length Should Be ${queryResults} 3 + IF "${DB_MODULE}" == "oracledb" + Should Be Equal As Strings ${queryResults}[0] ('ID', , 39, None, 38, 0, False) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', , 20, 20, None, None, True) + Should Be Equal As Strings ${queryResults}[2] ('LAST_NAME', , 20, 20, None, None, True) + ELSE IF "${DB_MODULE}" == "jaydebeapi" + Should Be Equal As Strings ${queryResults}[0] ('ID', DBAPITypeObject('DECIMAL', 'NUMERIC'), 39, 39, 38, 0, 0) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', DBAPITypeObject('CHAR', 'NCHAR', 'NVARCHAR', 'VARCHAR', 'OTHER'), 20, 20, 20, 0, 1) + Should Be Equal As Strings ${queryResults}[2] ('LAST_NAME', DBAPITypeObject('CHAR', 'NCHAR', 'NVARCHAR', 'VARCHAR', 'OTHER'), 20, 20, 20, 0, 1) + ELSE IF "${DB_MODULE}" == "sqlite3" + Should Be Equal As Strings ${queryResults}[0] ('id', None, None, None, None, None, None) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', None, None, None, None, None, None) + Should Be Equal As Strings ${queryResults}[2] ('LAST_NAME', None, None, None, None, None, None) + ELSE IF "${DB_MODULE}" == "ibm_db_dbi" + Should Be True "${queryResults}[0]".startswith("['ID', DBAPITypeObject(") + Should Be True "${queryResults}[0]".endswith("), 11, 11, 10, 0, False]") + Should Be True "INT" in "${queryResults}[0]" + Should Be True "${queryResults}[1]".startswith("['FIRST_NAME', DBAPITypeObject(") + Should Be True "${queryResults}[1]".endswith("), 20, 20, 20, 0, True]") + Should Be True "VARCHAR" in "${queryResults}[1]" + Should Be True "${queryResults}[2]".startswith("['LAST_NAME', DBAPITypeObject(") + Should Be True "${queryResults}[2]".endswith("), 20, 20, 20, 0, True]") + Should Be True "VARCHAR" in "${queryResults}[2]" + ELSE IF "${DB_MODULE}" == "teradata" + Should Be Equal As Strings ${queryResults}[0] ('id', , None, 10, 0, None, 0) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', , None, 20, 0, None, 1) + Should Be Equal As Strings ${queryResults}[2] ('LAST_NAME', , None, 20, 0, None, 1) + ELSE IF "${DB_MODULE}" == "psycopg2" + Should Be Equal As Strings ${queryResults}[0] Column(name='id', type_code=23) + Should Be Equal As Strings ${queryResults}[1] Column(name='first_name', type_code=1043) + Should Be Equal As Strings ${queryResults}[2] Column(name='last_name', type_code=1043) + ELSE IF "${DB_MODULE}" == "pymysql" + Should Be Equal As Strings ${queryResults}[0] ('id', 3, None, 11, 11, 0, False) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', 253, None, 80, 80, 0, True) + Should Be Equal As Strings ${queryResults}[2] ('LAST_NAME', 253, None, 80, 80, 0, True) + ELSE IF "${DB_MODULE}" == "pyodbc" + Should Be Equal As Strings ${queryResults}[0] ('id', , None, 10, 10, 0, False) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', , None, 20, 20, 0, True) + Should Be Equal As Strings ${queryResults}[2] ('LAST_NAME', , None, 20, 20, 0, True) + ELSE IF "${DB_MODULE}" == "pymssql" + Should Be Equal As Strings ${queryResults}[0] ('id', 3, None, None, None, None, None) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', 1, None, None, None, None, None) + Should Be Equal As Strings ${queryResults}[2] ('LAST_NAME', 1, None, None, None, None, None) + ELSE + Should Be Equal As Strings ${queryResults}[0] Column(name='id', type_code=23) + Should Be Equal As Strings ${queryResults}[1] Column(name='first_name', type_code=1043) + Should Be Equal As Strings ${queryResults}[2] Column(name='last_name', type_code=1043) + END + +Verify Foobar Description + @{queryResults} = Description SELECT * FROM foobar + Log Many @{queryResults} + Length Should Be ${queryResults} 2 + IF "${DB_MODULE}" == "oracledb" + Should Be Equal As Strings ${queryResults}[0] ('ID', , 39, None, 38, 0, False) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', , 30, 30, None, None, False) + ELSE IF "${DB_MODULE}" == "jaydebeapi" + Should Be Equal As Strings ${queryResults}[0] ('ID', DBAPITypeObject('DECIMAL', 'NUMERIC'), 39, 39, 38, 0, 0) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', DBAPITypeObject('CHAR', 'NCHAR', 'NVARCHAR', 'VARCHAR', 'OTHER'), 30, 30, 30, 0, 0) + ELSE IF "${DB_MODULE}" == "sqlite3" + Should Be Equal As Strings ${queryResults}[0] ('id', None, None, None, None, None, None) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', None, None, None, None, None, None) + ELSE IF "${DB_MODULE}" == "ibm_db_dbi" + Should Be True "${queryResults}[0]".startswith("['ID', DBAPITypeObject(") + Should Be True "${queryResults}[0]".endswith("), 11, 11, 10, 0, False]") + Should Be True "INT" in "${queryResults}[0]" + Should Be True "${queryResults}[1]".startswith("['FIRST_NAME', DBAPITypeObject(") + Should Be True "${queryResults}[1]".endswith("), 30, 30, 30, 0, False]") + Should Be True "VARCHAR" in "${queryResults}[1]" + ELSE IF "${DB_MODULE}" == "teradata" + Should Be Equal As Strings ${queryResults}[0] ('id', , None, 10, 0, None, 0) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', , None, 30, 0, None, 0) + ELSE IF "${DB_MODULE}" == "psycopg2" + Should Be Equal As Strings ${queryResults}[0] Column(name='id', type_code=23) + Should Be Equal As Strings ${queryResults}[1] Column(name='first_name', type_code=1043) + ELSE IF "${DB_MODULE}" == "pymysql" + Should Be Equal As Strings ${queryResults}[0] ('id', 3, None, 11, 11, 0, False) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', 253, None, 120, 120, 0, False) + ELSE IF "${DB_MODULE}" in "pyodbc" + Should Be Equal As Strings ${queryResults}[0] ('id', , None, 10, 10, 0, False) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', , None, 30, 30, 0, False) + ELSE IF "${DB_MODULE}" == "pymssql" + Should Be Equal As Strings ${queryResults}[0] ('id', 3, None, None, None, None, None) + Should Be Equal As Strings ${queryResults}[1] ('FIRST_NAME', 1, None, None, None, None, None) + ELSE + Should Be Equal As Strings ${queryResults}[0] Column(name='id', type_code=23) + Should Be Equal As Strings ${queryResults}[1] Column(name='first_name', type_code=1043) + END diff --git a/test/tests/common_tests/disconnect_from_db.robot b/test/tests/common_tests/disconnect_from_db.robot new file mode 100644 index 00000000..76ae94fd --- /dev/null +++ b/test/tests/common_tests/disconnect_from_db.robot @@ -0,0 +1,33 @@ +*** Settings *** +Documentation Keyword 'Disconnect From Database' should work properly if there was no connection at all +... or if it was closed previously. +... It can be also configured to raise an exception if no connection was open. + +Resource ../../resources/common.resource + +Suite Teardown Disconnect From Database + + +*** Test Cases *** +Disconnect If No Connection - No Error Expected + Disconnect From Database + +Disconnect If No Connection - Error Expected + Disconnect From Database + Run Keyword And Expect Error + ... ConnectionError: No open database connection to close + ... Disconnect From Database + ... error_if_no_connection=True + +Disconnect If Connection Was Closed - No Error Expected + Connect To DB + Disconnect From Database + Disconnect From Database + +Disconnect If Connection Was Closed - Error Expected + Connect To DB + Disconnect From Database + Run Keyword And Expect Error + ... ConnectionError: No open database connection to close + ... Disconnect From Database + ... error_if_no_connection=True diff --git a/test/tests/common_tests/encoding.robot b/test/tests/common_tests/encoding.robot new file mode 100644 index 00000000..b7dd2d9b --- /dev/null +++ b/test/tests/common_tests/encoding.robot @@ -0,0 +1,27 @@ +*** Settings *** +Documentation Different non ASCII characters work fine + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +Non ASCII Characters In Values + Execute Sql String INSERT INTO person VALUES(1,'Jürgen','Gernegroß') + ${results}= Query + ... SELECT LAST_NAME FROM person WHERE FIRST_NAME='Jürgen' + Should Be Equal ${results}[0][0] Gernegroß + +Read SQL Script Files As UTF8 + [Documentation] If the SQL script file contains non ASCII characters and saved in UTF8 encoding, + ... Pytho might have an issue opening this file on Windows, as it doesn't use UTF8 by default. + ... In this case you the library should excplicitely set the UTF8 encoding when opening the script file. + ... https://dev.to/methane/python-use-utf-8-mode-on-windows-212i + Execute Sql Script ${CURDIR}/../../resources/insert_data_in_person_table_utf8.sql + ${results}= Query + ... SELECT LAST_NAME FROM person WHERE FIRST_NAME='Jürgen' + Should Be Equal ${results}[0][0] Gernegroß \ No newline at end of file diff --git a/test/tests/common_tests/import_params.robot b/test/tests/common_tests/import_params.robot new file mode 100644 index 00000000..f080bd7b --- /dev/null +++ b/test/tests/common_tests/import_params.robot @@ -0,0 +1,42 @@ +*** Settings *** +Documentation Tests for parameters used when importing the library + +*** Test Cases *** +Import Without Parameters Is Valid + Import Library DatabaseLibrary + +Log Query Results Params Cause No Crash + Import Library DatabaseLibrary log_query_results=False log_query_results_head=0 + +Log Query Results Head - Negative Value Not Allowed + Run Keyword And Expect Error + ... STARTS: Initializing library 'DatabaseLibrary' with arguments [ log_query_results_head=-1 ] failed: ValueError: Wrong log head value provided: -1. The value can't be negative! + ... Import Library DatabaseLibrary log_query_results_head=-1 + +Warn On Connection Overwrite Enabled + Skip If '${DB_MODULE}' != 'psycopg2' + Import Library DatabaseLibrary warn_on_connection_overwrite=True AS MyDBLib + FOR ${counter} IN RANGE 0 2 + MyDBLib.Connect To Database + ... db_module=${DB_MODULE} + ... db_name=${DB_NAME} + ... db_user=${DB_USER} + ... db_password=${DB_PASS} + ... db_host=${DB_HOST} + ... db_port=${DB_PORT} + END + [Teardown] MyDBLib.Disconnect From Database + +Warn On Connection Overwrite Disabled + Skip If '${DB_MODULE}' != 'psycopg2' + Import Library DatabaseLibrary warn_on_connection_overwrite=False AS MyDBLib2 + FOR ${counter} IN RANGE 0 2 + MyDBLib2.Connect To Database + ... db_module=${DB_MODULE} + ... db_name=${DB_NAME} + ... db_user=${DB_USER} + ... db_password=${DB_PASS} + ... db_host=${DB_HOST} + ... db_port=${DB_PORT} + END + [Teardown] MyDBLib2.Disconnect From Database \ No newline at end of file diff --git a/test/tests/common_tests/log_query_results.robot b/test/tests/common_tests/log_query_results.robot new file mode 100644 index 00000000..4b4270bb --- /dev/null +++ b/test/tests/common_tests/log_query_results.robot @@ -0,0 +1,15 @@ +*** Settings *** +Documentation Tests for keywords controlling the logging query results + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table And Insert Data +Test Teardown Drop Tables Person And Foobar + +*** Test Cases *** +Calling The Keyword Causes No Crash + Set Logging Query Results enabled=False + Set Logging Query Results enabled=True log_head=0 + Set Logging Query Results log_head=30 \ No newline at end of file diff --git a/test/tests/common_tests/query_params.robot b/test/tests/common_tests/query_params.robot new file mode 100644 index 00000000..4761b73d --- /dev/null +++ b/test/tests/common_tests/query_params.robot @@ -0,0 +1,71 @@ +*** Settings *** +Documentation Keywords with query params as separate arguments work across all databases. + +Resource ../../resources/common.resource + +Suite Setup Connect To DB And Build Query +Suite Teardown Disconnect From Database +Test Setup Create Person Table And Insert Data +Test Teardown Drop Tables Person And Foobar + + +*** Variables *** +@{SINGLE_PARAM} Franz Allan +@{MULTI_PARAM} Jerry Schneider + + +*** Keywords *** +Connect To DB And Build Query + Connect To DB + Build Query Strings With Params + +Build Query Strings With Params + ${placeholder}= Set Variable %s + IF "${DB_MODULE}" in ["oracledb", "cx_Oracle", "jaydebeapi"] + ${placeholder}= Set Variable :id + ELSE IF "${DB_MODULE}" in ["sqlite3", "pyodbc"] + ${placeholder}= Set Variable ? + END + Set Suite Variable ${QUERY_SINGLE_PARAM} SELECT id FROM person WHERE FIRST_NAME=${placeholder} + Set Suite Variable ${QUERY_MULTI_PARAM} ${QUERY_SINGLE_PARAM} AND LAST_NAME=${placeholder} + + +*** Test Cases *** +Query Single Param + ${out}= Query ${QUERY_SINGLE_PARAM} parameters=${SINGLE_PARAM} + Length Should Be ${out} 1 + +Query Multiple Params + ${out}= Query ${QUERY_MULTI_PARAM} parameters=${MULTI_PARAM} + Length Should Be ${out} 1 + +Row Count + ${out}= Row Count ${QUERY_SINGLE_PARAM} parameters=${SINGLE_PARAM} + Should Be Equal As Strings ${out} 1 + +Description + ${out}= Description ${QUERY_SINGLE_PARAM} parameters=${SINGLE_PARAM} + Length Should Be ${out} 1 + +Execute SQL String + Execute Sql String ${QUERY_SINGLE_PARAM} parameters=${SINGLE_PARAM} + +Check If Exists In DB + Check If Exists In Database ${QUERY_SINGLE_PARAM} parameters=${SINGLE_PARAM} + +Check If Not Exists In DB + @{Wrong params}= Create List Joe + Check If Not Exists In Database ${QUERY_SINGLE_PARAM} parameters=${Wrong params} + +Row Count is 0 + @{Wrong params}= Create List Joe + Row Count is 0 ${QUERY_SINGLE_PARAM} parameters=${Wrong params} + +Row Count is Equal to X + Row Count is Equal to X ${QUERY_SINGLE_PARAM} 1 parameters=${SINGLE_PARAM} + +Row Count is Less Than X + Row Count is Less Than X ${QUERY_SINGLE_PARAM} 5 parameters=${SINGLE_PARAM} + +Row Count is Greater Than X + Row Count is Greater Than X ${QUERY_SINGLE_PARAM} 0 parameters=${SINGLE_PARAM} diff --git a/test/tests/common_tests/script_files.robot b/test/tests/common_tests/script_files.robot new file mode 100644 index 00000000..2d067ec8 --- /dev/null +++ b/test/tests/common_tests/script_files.robot @@ -0,0 +1,42 @@ +*** Settings *** +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +Semicolons As Statement Separators In One Line + Run SQL Script File statements_in_one_line + ${sql}= Catenate select * from person + ... where id=6 or id=7 + ${results}= Query ${sql} + Length Should Be ${results} 2 + Should Be Equal As Strings ${results}[0] (6, 'Julian', 'Bashir') + Should Be Equal As Strings ${results}[1] (7, 'Jadzia', 'Dax') + +Semicolons In Values + Run SQL Script File semicolons_in_values + ${sql}= Catenate select * from person + ... where id=3 or id=4 + ${results}= Query ${sql} + Length Should Be ${results} 2 + Should Be Equal As Strings ${results}[0] (3, 'Hello; world', 'Another; value') + Should Be Equal As Strings ${results}[1] (4, 'May the Force; ', 'be with you;') + +Semicolons And Quotes In Values + Run SQL Script File semicolons_and_quotes_in_values + ${sql}= Catenate select * from person + ... where id=5 + ${results}= Query ${sql} + Length Should Be ${results} 1 + Should Be Equal As Strings ${results}[0] (5, 'Miles', "O'Brian") + + +*** Keywords *** +Run SQL Script File + [Arguments] ${File Name} + ${Script files dir}= Set Variable ${CURDIR}/../../resources/script_file_tests + Execute Sql Script ${Script files dir}/${File Name}.sql diff --git a/test/tests/common_tests/stored_procedures.robot b/test/tests/common_tests/stored_procedures.robot new file mode 100644 index 00000000..7cc136aa --- /dev/null +++ b/test/tests/common_tests/stored_procedures.robot @@ -0,0 +1,124 @@ +*** Settings *** +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create And Fill Tables And Stored Procedures +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +Procedure Takes No Params + ${param values} ${result sets}= Call Stored Procedure no_params + Length Should Be ${param values} 0 + IF "${DB_MODULE}" in ["psycopg2", "psycopg3"] + Length Should Be ${result sets} 1 + Should Be Equal As Strings ${result sets}[0][0][0] ${EMPTY} + ELSE + Length Should Be ${result sets} 0 + END + +Procedure Returns Single Value As Param + IF "${DB_MODULE}" in ["psycopg2", "psycopg3"] + Skip PostgreSQL doesn't return single values as params, only as result sets + END + IF "${DB_MODULE}" in ["pymssql"] + Skip Returning values using OUT params in MS SQL is not supported, use result sets + END + @{params}= Create List Jerry OUTPUT + ${param values} ${result sets}= Call Stored Procedure get_second_name ${params} + Length Should Be ${result sets} 0 + Should Be Equal ${param values}[1] Schneider + +Procedure Returns Single Value As Result Set + IF "${DB_MODULE}" not in ["psycopg2", "psycopg3", "pymssql"] + Skip This test is not valid for '${DB_MODULE}' + END + @{params}= Create List Jerry + ${param values} ${result sets}= Call Stored Procedure get_second_name ${params} + Length Should Be ${param values} 1 + Should Be Equal ${param values}[0] Jerry + Length Should Be ${result sets} 1 + ${First result set}= Set Variable ${result sets}[0] + Length Should Be ${First result set} 1 + Should Be Equal ${First result set}[0][0] Schneider + +Procedure Returns Result Set Via CURSOR Param + IF "${DB_MODULE}" not in ["oracledb", "cx_Oracle"] + Skip This test is valid for Oracle only + END + @{params}= Create List CURSOR + ${param values} ${result sets}= Call Stored Procedure get_all_second_names ${params} + ${length of input params}= Get Length ${params} + Length Should Be ${param values} ${length of input params} + Length Should Be ${result sets} 1 + ${first result set}= Set Variable ${result sets}[0] + Length Should Be ${first result set} 2 + Should Be Equal ${first result set}[0][0] See + Should Be Equal ${first result set}[1][0] Schneider + +Procedure Returns Result Set Without CURSOR Param + IF "${DB_MODULE}" in ["oracledb", "cx_Oracle"] + Skip This test is not valid for Oracle + END + @{params}= Create List @{EMPTY} + ${param values} ${result sets}= Call Stored Procedure get_all_second_names ${params} + ${length of input params}= Get Length ${params} + Length Should Be ${param values} ${length of input params} + Length Should Be ${result sets} 1 + ${first result set}= Set Variable ${result sets}[0] + Length Should Be ${first result set} 2 + Should Be Equal ${first result set}[0][0] See + Should Be Equal ${first result set}[1][0] Schneider + +Procedure Returns Multiple Result Sets + IF "${DB_MODULE}" in ["oracledb", "cx_Oracle", "psycopg2", "psycopg3"] + @{params}= Create List CURSOR CURSOR + ELSE IF "${DB_MODULE}" in ["pymysql", "pymssql"] + @{params}= Create List @{EMPTY} + END + ${param values} ${result sets}= Call Stored Procedure get_all_first_and_second_names ${params} + ${length of input params}= Get Length ${params} + Length Should Be ${param values} ${length of input params} + Length Should Be ${result sets} 2 + ${first result set}= Set Variable ${result sets}[0] + Should Be Equal ${first result set}[0][0] Franz Allan + Should Be Equal ${first result set}[1][0] Jerry + ${second result set}= Set Variable ${result sets}[1] + Should Be Equal ${second result set}[0][0] See + Should Be Equal ${second result set}[1][0] Schneider + +Procedure With IF/ELSE Block + Call Stored Procedure check_condition + +MSSQL Procedure Returns OUT Param Without Result Sets + IF "${DB_MODULE}" not in ["pymssql"] + Skip This test is valid for pymssql only + END + @{params}= Create List give me 1 + @{out_params}= Create List ${9} + ${param values} ${result sets}= Call Stored Procedure return_out_param_without_result_sets + ... ${params} additional_output_params=${out_params} + Should Be Empty ${result sets} + Should Be Equal As Integers ${param values}[1] 1 + @{params}= Create List give me 0 + ${param values} ${result sets}= Call Stored Procedure return_out_param_without_result_sets + ... ${params} additional_output_params=${out_params} + Should Be Empty ${result sets} + Should Be Equal As Integers ${param values}[1] 0 + + +*** Keywords *** +Create And Fill Tables And Stored Procedures + Create Person Table And Insert Data + IF "${DB_MODULE}" in ["oracledb", "cx_Oracle"] + Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_oracle.sql + ELSE IF "${DB_MODULE}" in ["pymysql"] + Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_mysql.sql + ELSE IF "${DB_MODULE}" in ["psycopg2", "psycopg3"] + Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_postgres.sql + ELSE IF "${DB_MODULE}" in ["pymssql"] + Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_mssql.sql + ELSE + Skip Don't know how to create stored procedures for '${DB_MODULE}' + END diff --git a/test/tests/common_tests/transaction.robot b/test/tests/common_tests/transaction.robot new file mode 100644 index 00000000..cac6723b --- /dev/null +++ b/test/tests/common_tests/transaction.robot @@ -0,0 +1,93 @@ +*** Settings *** +Documentation Testing the transaction rollback requires savepoints - +... setting them is diffferent depending on the database + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +Transaction + IF "${DB_MODULE}" == "teradata" + Skip Teradata doesn't support savepoints + END + Begin first transaction + Add person in first transaction + Verify person in first transaction + Begin second transaction + Add person in second transaction + Verify persons in first and second transactions + Rollback second transaction + Verify second transaction rollback + Rollback first transaction + Verify first transaction rollback + + +*** Keywords *** +Begin first transaction + ${sql}= Set Variable SAVEPOINT first + IF "${DB_MODULE}" == "ibm_db_dbi" + ${sql}= Catenate ${sql} + ... ON ROLLBACK RETAIN CURSORS + ELSE IF "${DB_MODULE}" == "pymssql" + ${sql}= Set Variable SAVE TRANSACTION first + END + ${output}= Execute SQL String ${sql} True + Log ${output} + Should Be Equal As Strings ${output} None + +Add person in first transaction + ${output}= Execute SQL String INSERT INTO person VALUES(101,'Bilbo','Baggins') True + Log ${output} + Should Be Equal As Strings ${output} None + +Verify person in first transaction + Row Count is Equal to X SELECT * FROM person WHERE LAST_NAME= 'Baggins' 1 True + +Begin second transaction + ${sql}= Set Variable SAVEPOINT second + IF "${DB_MODULE}" == "ibm_db_dbi" + ${sql}= Catenate ${sql} + ... ON ROLLBACK RETAIN CURSORS + ELSE IF "${DB_MODULE}" == "pymssql" + ${sql}= Set Variable SAVE TRANSACTION second + END + ${output}= Execute SQL String ${sql} True + Log ${output} + Should Be Equal As Strings ${output} None + +Add person in second transaction + ${output}= Execute SQL String INSERT INTO person VALUES(102,'Frodo','Baggins') True + Log ${output} + Should Be Equal As Strings ${output} None + +Verify persons in first and second transactions + Row Count is Equal to X SELECT * FROM person WHERE LAST_NAME= 'Baggins' 2 True + +Rollback second transaction + ${sql}= Set Variable ROLLBACK TO SAVEPOINT second + IF "${DB_MODULE}" == "pymssql" + ${sql}= Set Variable ROLLBACK TRANSACTION second + END + ${output}= Execute SQL String ${sql} True + Log ${output} + Should Be Equal As Strings ${output} None + +Verify second transaction rollback + Row Count is Equal to X SELECT * FROM person WHERE LAST_NAME= 'Baggins' 1 True + +Rollback first transaction + ${sql}= Set Variable ROLLBACK TO SAVEPOINT first + IF "${DB_MODULE}" == "pymssql" + ${sql}= Set Variable ROLLBACK TRANSACTION first + END + ${output}= Execute SQL String ${sql} + Log ${output} + Should Be Equal As Strings ${output} None + +Verify first transaction rollback + Row Count is 0 SELECT * FROM person WHERE LAST_NAME= 'Baggins' True diff --git a/test/tests/custom_db_tests/db_update_in_background_commit.robot b/test/tests/custom_db_tests/db_update_in_background_commit.robot new file mode 100644 index 00000000..28ff9991 --- /dev/null +++ b/test/tests/custom_db_tests/db_update_in_background_commit.robot @@ -0,0 +1,17 @@ +*** Settings *** +Documentation Check if the SQL statement returns new results, if DB is being updated in the background - +... this requires a commit after each query. +... See https://github.com/MarketSquare/Robotframework-Database-Library/issues/237 + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table And Insert Data +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +Use Auto retry + [Documentation] Update the DB manually in the background and check if the query returns the new results + Check Query Result SELECT LAST_NAME FROM person ORDER BY id == Musk retry_timeout=30s diff --git a/test/Excel_DB_Test.robot b/test/tests/custom_db_tests/excel.robot similarity index 65% rename from test/Excel_DB_Test.robot rename to test/tests/custom_db_tests/excel.robot index 0a4d2873..f61b793a 100644 --- a/test/Excel_DB_Test.robot +++ b/test/tests/custom_db_tests/excel.robot @@ -1,294 +1,245 @@ *** Settings *** -Suite Setup Setup testing excel -Suite Teardown Cleanup testing excel -Library DatabaseLibrary -Library OperatingSystem -Library ExcelLibrary +Documentation These tests are mostly different from common tests for other database + +Resource ../../resources/common.resource +Library ExcelLibrary + +Suite Setup Setup testing excel +Suite Teardown Cleanup testing excel + *** Variables *** -${DBHost} dummy -${DBName} ${EXECDIR}/test/Test_Excel.xls -${DBPass} dummy -${DBPort} 80 -${DBUser} dummy +${DBHost} dummy +${DBName} ${CURDIR}/Test_Excel.xlsx +${DBPass} dummy +${DBPort} 80 +${DBUser} dummy + *** Test Cases *** Create person table - [Tags] db smoke - ${output} = Execute SQL String CREATE TABLE [person] (id integer,first_name varchar(20),last_name varchar(20)); + ${output} = Execute SQL String CREATE TABLE person (id integer,first_name varchar(20),last_name varchar(20)); Log ${output} Should Be Equal As Strings ${output} None Execute SQL Script - Insert Data person table - [Tags] db smoke - log to console ${DBName} - Comment ${output} = Execute SQL Script ${EXECDIR}/test/excel_db_test_insertData.sql - ${output} = Execute SQL Script ${EXECDIR}/test/excel_db_test_insertData.sql + log to console ${DBName} + ${output} = Execute SQL Script ${CURDIR}/../../resources/excel_db_test_insertData.sql Log ${output} Should Be Equal As Strings ${output} None Execute SQL String - Create Table - [Tags] db smoke ${output} = Execute SQL String create table [foobar] ([id] integer, [firstname] varchar(20)) Log ${output} Should Be Equal As Strings ${output} None Check If Exists In DB - Franz Allan - [Tags] db smoke Check If Exists In Database SELECT id FROM [person$] WHERE first_name = 'Franz Allan'; Check If Not Exists In DB - Joe - [Tags] db smoke Check If Not Exists In Database SELECT id FROM [person$] WHERE first_name = 'Joe'; - Verify Row Count is 0 - [Tags] db smoke Row Count is 0 SELECT * FROM [person$] WHERE first_name = 'NotHere'; Verify Row Count is Equal to X - [Tags] db smoke Row Count is Equal to X SELECT id FROM [person$]; 2 Verify Row Count is Less Than X - [Tags] db smoke Row Count is Less Than X SELECT id FROM [person$]; 3 Verify Row Count is Greater Than X - [Tags] db smoke Row Count is Greater Than X SELECT * FROM [person$]; 1 Retrieve Row Count - [Tags] db smoke ${output} = Row Count SELECT id FROM [person$]; Log ${output} Should Be Equal As Strings ${output} 2 Retrieve records from person table - [Tags] db smoke ${output} = Execute SQL String SELECT * FROM [person$]; Log ${output} Should Be Equal As Strings ${output} None Verify person Description - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description select TOP 1 * FROM [person$]; Log Many @{queryResults} ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('first_name', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('first_name', , None, 255, 255, 0, True) ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} ('last_name', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('last_name', , None, 255, 255, 0, True) ${NumColumns} = Get Length ${queryResults} Should Be Equal As Integers ${NumColumns} 3 Verify foobar Description - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description SELECT TOP 1 * FROM [foobar$]; Log Many @{queryResults} ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('firstname', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('firstname', , None, 255, 255, 0, True) ${NumColumns} = Get Length ${queryResults} Should Be Equal As Integers ${NumColumns} 2 Verify Query - Row Count person table - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM [person$]; Log ${output} - Should Be Equal As Strings ${output} [(2, )] + Should Be Equal As Integers ${output}[0][0] 2 Verify Query - Row Count foobar table - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM foobar; Log ${output} - Should Be Equal As Strings ${output} [(0, )] + Should Be Equal As Integers ${output}[0][0] 0 Verify Query - Get results as a list of dictionaries - [Tags] db smoke ${output} = Query SELECT * FROM [person$]; \ True Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry + Should Be Equal As Strings ${output[0]}[first_name] Franz Allan + Should Be Equal As Strings ${output[1]}[first_name] Jerry Verify Execute SQL String - Row Count person table - [Tags] db smoke ${output} = Execute SQL String SELECT COUNT(*) FROM [person$]; Log ${output} Should Be Equal As Strings ${output} None Verify Execute SQL String - Row Count foobar table - [Tags] db smoke ${output} = Execute SQL String SELECT COUNT(*) FROM [foobar$]; Log ${output} Should Be Equal As Strings ${output} None Insert Data Into Table foobar - [Tags] db smoke ${output} = Execute SQL String INSERT INTO [foobar$] VALUES(1,'Jerry'); Log ${output} Should Be Equal As Strings ${output} None Verify Query - Row Count foobar table 1 row - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM [foobar$]; Log ${output} - Should Be Equal As Strings ${output} [(1, )] - + Should Be Equal As Integers ${output}[0][0] 1 Add person in first transaction - [Tags] db smoke ${output} = Execute SQL String INSERT INTO [person$] VALUES(101,'Bilbo','Baggins'); True Log ${output} Should Be Equal As Strings ${output} None Verify person in first transaction - [Tags] db smoke Row Count is Equal to X SELECT * FROM [person$] WHERE last_name = 'Baggins'; 1 True -#Begin second transaction -# [Tags] db smoke -# ${output} = Execute SQL String SAVEPOINT second True -# Log ${output} -# Should Be Equal As Strings ${output} None - Add person in second transaction - [Tags] db smoke ${output} = Execute SQL String INSERT INTO [person$] VALUES(102,'Frodo','Baggins'); True Log ${output} Should Be Equal As Strings ${output} None Verify persons in first and second transactions - [Tags] db smoke Row Count is Equal to X SELECT * FROM [person$] WHERE last_name = 'Baggins'; 2 True Setup RO access to excel Disconnect From Database - Connect To Database excel ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} - + Connect To Database excel ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} Check If Exists In RODB - Franz Allan - [Tags] db smoke Check If Exists In Database SELECT id FROM [person$] WHERE first_name = 'Franz Allan'; Check If Not Exists In RODB - Joe - [Tags] db smoke Check If Not Exists In Database SELECT id FROM [person$] WHERE first_name = 'Joe'; - Verify Row Count is 0 RODB - [Tags] db smoke Row Count is 0 SELECT * FROM [person$] WHERE first_name = 'NotHere'; Verify Row Count is Equal to X RODB - [Tags] db smoke Row Count is Equal to X SELECT id FROM [person$]; 4 Verify Row Count is Less Than X RODB - [Tags] db smoke Row Count is Less Than X SELECT id FROM [person$]; 5 Verify Row Count is Greater Than X RODB - [Tags] db smoke Row Count is Greater Than X SELECT * FROM [person$]; 1 Retrieve Row Count RODB - [Tags] db smoke ${output} = Row Count SELECT id FROM [person$]; Log ${output} Should Be Equal As Strings ${output} 4 Retrieve records from person table RODB - [Tags] db smoke ${output} = Execute SQL String SELECT * FROM [person$]; Log ${output} Should Be Equal As Strings ${output} None Verify person Description RODB - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description select TOP 1 * FROM [person$]; Log Many @{queryResults} ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('first_name', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('first_name', , None, 255, 255, 0, True) ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} ('last_name', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('last_name', , None, 255, 255, 0, True) ${NumColumns} = Get Length ${queryResults} Should Be Equal As Integers ${NumColumns} 3 Verify foobar Description RODB - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description SELECT TOP 1 * FROM [foobar$]; Log Many @{queryResults} ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('id', , None, 255, 255, 0, True) ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ('firstname', , None, 255, 255, 0, True) + Should Be Equal As Strings ${output} ('firstname', , None, 255, 255, 0, True) ${NumColumns} = Get Length ${queryResults} Should Be Equal As Integers ${NumColumns} 2 Verify Query - Row Count person table RODB - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM [person$]; Log ${output} - Should Be Equal As Strings ${output} [(4, )] + Should Be Equal As Integers ${output}[0][0] 4 Verify Query - Row Count foobar table RODB - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM [foobar$]; Log ${output} - Should Be Equal As Strings ${output} [(1, )] + Should Be Equal As Integers ${output}[0][0] 1 Verify Query - Get results as a list of dictionaries RODB - [Tags] db smoke ${output} = Query SELECT * FROM [person$]; \ True Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry + Should Be Equal As Strings ${output[0]}[first_name] Franz Allan + Should Be Equal As Strings ${output[1]}[first_name] Jerry Verify Execute SQL String - Row Count person table RODB - [Tags] db smoke ${output} = Execute SQL String SELECT COUNT(*) FROM [person$]; Log ${output} Should Be Equal As Strings ${output} None Verify Execute SQL String - Row Count foobar table RODB - [Tags] db smoke ${output} = Execute SQL String SELECT COUNT(*) FROM [foobar$]; Log ${output} Should Be Equal As Strings ${output} None - Verify Query - Row Count foobar table 1 row RODB - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM [foobar$]; Log ${output} - Should Be Equal As Strings ${output} [(1, )] + Should Be Equal As Integers ${output}[0][0] 1 Setup RW access to excel Disconnect From Database - Connect To Database excelrw ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} + Connect To Database excelrw ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} Drop person and foobar tables - [Tags] db smoke ${output} = Execute SQL String DROP TABLE [person$],[foobar$] Log ${output} Should Be Equal As Strings ${output} None *** Keywords *** - Setup testing excel - Create Excel Workbook Test_Excel - Connect To Database excelrw ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} + Create Excel Document excel_db + Save Excel Document ${DBName} + Connect To Database excelrw ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} Cleanup testing excel Disconnect From Database - Remove File ${DBName} + Remove File ${DBName} diff --git a/test/tests/custom_db_tests/multiple_connections.robot b/test/tests/custom_db_tests/multiple_connections.robot new file mode 100644 index 00000000..540aee71 --- /dev/null +++ b/test/tests/custom_db_tests/multiple_connections.robot @@ -0,0 +1,62 @@ +*** Settings *** +Documentation Connections to two different databases can be handled separately. +... These tests require two databases running in parallel. + +Resource ../../resources/common.resource + +Suite Setup Connect To All Databases +Suite Teardown Disconnect From All Databases +Test Setup Create Tables +Test Teardown Drop Tables + + +*** Variables *** +${Table_1} table_1 +${Table_2} table_2 + +${Alias_1} first +${Alias_2} second + + +*** Test Cases *** +First Table Was Created In First Database Only + Table Must Exist ${Table_1} alias=${Alias_1} + Run Keyword And Expect Error Table '${Table_2}' does not exist in the db + ... Table Must Exist ${Table_2} alias=${Alias_1} + +Second Table Was Created In Second Database Only + Table Must Exist ${Table_2} alias=${Alias_2} + Run Keyword And Expect Error Table '${Table_1}' does not exist in the db + ... Table Must Exist ${Table_1} alias=${Alias_2} + +Switching Default Alias + Switch Database ${Alias_1} + Table Must Exist ${Table_1} + Run Keyword And Expect Error Table '${Table_2}' does not exist in the db + ... Table Must Exist ${Table_2} + Switch Database ${Alias_2} + Table Must Exist ${Table_2} + Run Keyword And Expect Error Table '${Table_1}' does not exist in the db + ... Table Must Exist ${Table_1} + + +*** Keywords *** +Connect To All Databases + Connect To Database psycopg2 db db_user pass 127.0.0.1 5432 + ... alias=${Alias_1} + Connect To Database pymysql db db_user pass 127.0.0.1 3306 + ... alias=${Alias_2} + +Create Tables + ${sql_1}= Catenate + ... CREATE TABLE ${Table_1} + ... (id integer not null unique, FIRST_NAME varchar(20), LAST_NAME varchar(20)) + ${sql_2}= Catenate + ... CREATE TABLE ${Table_2} + ... (id integer not null unique, FIRST_NAME varchar(20), LAST_NAME varchar(20)) + Execute Sql String ${sql_1} alias=${Alias_1} + Execute Sql String ${sql_2} alias=${Alias_2} + +Drop Tables + Execute Sql String DROP TABLE ${Table_1} alias=${Alias_1} + Execute Sql String DROP TABLE ${Table_2} alias=${Alias_2} diff --git a/test/tests/custom_db_tests/oracle_omit_semicolon.robot b/test/tests/custom_db_tests/oracle_omit_semicolon.robot new file mode 100644 index 00000000..cbc9ae85 --- /dev/null +++ b/test/tests/custom_db_tests/oracle_omit_semicolon.robot @@ -0,0 +1,67 @@ +*** Settings *** +Documentation Tests for the parameter _omitTrailingSemicolon_ in the keyword +... _Execute SQL String_ - special for the issue #184: +... https://github.com/MarketSquare/Robotframework-Database-Library/issues/184 +... The _PLSQL BLOCK_ is most likely valid for Oracle DB only. + +Resource ../../resources/common.resource + +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table And Insert Data +Test Teardown Drop Tables Person And Foobar + + +*** Variables *** +${NORMAL QUERY} SELECT * FROM person; +${PLSQL BLOCK} DECLARE ERRCODE NUMBER; ERRMSG VARCHAR2(200); BEGIN DBMS_OUTPUT.PUT_LINE('Hello!'); END; + +${ERROR SIMPLE QUERY} *ORA-03048: SQL reserved word ';' is not syntactically valid following* +${ERROR PLSQL} *PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following* + + +*** Test Cases *** +Explicitely Omit Semicolon - Simple Query + [Documentation] Check if it works for Oracle - explicitly omitting the semicolon + ... is equal to the default behavior + Execute Sql String ${NORMAL QUERY} omit_trailing_semicolon=True + +Explicitely Don't Omit Semicolon - Simple Query + [Documentation] Check if Oracle throws an error + + Run Keyword And Expect Error ${ERROR SIMPLE QUERY} + ... Execute Sql String ${NORMAL QUERY} omit_trailing_semicolon=False + +Explicitely Omit Semicolon - PLSQL Block + [Documentation] Check if Oracle throws an error + Run Keyword And Expect Error ${ERROR PLSQL} + ... Execute Sql String ${PLSQL BLOCK} omit_trailing_semicolon=True + +Explicitely Don't Omit Semicolon - PLSQL Block + [Documentation] Should run without errors, because the semicolon is needed + ... at the end of the PLSQL block even with Oracle + Execute Sql String ${PLSQL BLOCK} omit_trailing_semicolon=False + +Explicitely Omit Semicolon With Keyword - Simple Query + [Documentation] Check if it works for Oracle - explicitly omitting the semicolon + ... is equal to the default behavior + Set Omit Trailing Semicolon True + Execute Sql String ${NORMAL QUERY} + +Explicitely Don't Omit Semicolon With Keyword - Simple Query + [Documentation] Check if Oracle throws an error + Set Omit Trailing Semicolon False + Run Keyword And Expect Error ${ERROR SIMPLE QUERY} + ... Execute Sql String ${NORMAL QUERY} + +Explicitely Omit Semicolon With Keyword - PLSQL Block + [Documentation] Check if Oracle throws an error + Set Omit Trailing Semicolon True + Run Keyword And Expect Error ${ERROR PLSQL} + ... Execute Sql String ${PLSQL BLOCK} + +Explicitely Don't Omit Semicolon With Keyword - PLSQL Block + [Documentation] Should run without errors, because the semicolon is needed + ... at the end of the PLSQL block even with Oracle + Set Omit Trailing Semicolon False + Execute Sql String ${PLSQL BLOCK} diff --git a/test/tests/custom_db_tests/oracle_thick_mode.robot b/test/tests/custom_db_tests/oracle_thick_mode.robot new file mode 100644 index 00000000..73d03e20 --- /dev/null +++ b/test/tests/custom_db_tests/oracle_thick_mode.robot @@ -0,0 +1,65 @@ +*** Settings *** +Documentation Tests of switching between thin and thick mode of oracledb client. +... Require the oracle client libraries installed. +... See more here: https://python-oracledb.readthedocs.io/en/latest/user_guide/initialization.html#initialization +... +... Due to current limitations of the oracledb module it's not possible to switch between thick and thin modes +... during a test execution session - even in different suites. +... So theses tests should be run separated only. + +Resource ../../resources/common.resource +Test Teardown Drop Tables And Disconnect + + +*** Variables *** +${DB_MODULE} oracledb +${DB_HOST} 127.0.0.1 +${DB_PORT} 1521 +${DB_PASS} pass +${DB_USER} db_user +${DB_NAME} db +${ORACLE_LIB_DIR} ${EMPTY} + + +*** Test Cases *** +Thick Mode Without Client Dir Specified + [Documentation] No client dir --> oracledb will search it in usual places + Connect And Run Simple Query oracle_driver_mode=thick + +Thick Mode With Client Dir Specified + [Documentation] Client dir specified --> oracledb will search it in this place + Connect And Run Simple Query oracle_driver_mode=thick,lib_dir=${ORACLE_LIB_DIR} + +Thin Mode - Default + [Documentation] No mode specified --> thin mode is used + Connect And Run Simple Query + +Thin Mode Explicitely Specified + [Documentation] Thin mode specified --> thin mode is used + Connect And Run Simple Query oracle_driver_mode=thin + +Wrong Mode + [Documentation] Wrong mode --> proper error message from the library + Run Keyword And Expect Error ValueError: Invalid Oracle client mode provided: wrong + ... Connect And Run Simple Query oracle_driver_mode=wrong + + +*** Keywords *** +Connect And Run Simple Query + [Documentation] Connect using usual params and client mode if provided + [Arguments] &{Extra params} + Connect To Database + ... ${DB_MODULE} + ... ${DB_NAME} + ... ${DB_USER} + ... ${DB_PASS} + ... ${DB_HOST} + ... ${DB_PORT} + ... &{Extra params} + Create Person Table + Query SELECT * FROM person + +Drop Tables And Disconnect + [Documentation] Clean data and disconnect + Drop Tables Person And Foobar + Disconnect From Database diff --git a/test/tests/custom_db_tests/sql_script_split_commands.robot b/test/tests/custom_db_tests/sql_script_split_commands.robot new file mode 100644 index 00000000..e6223ccb --- /dev/null +++ b/test/tests/custom_db_tests/sql_script_split_commands.robot @@ -0,0 +1,22 @@ +*** Settings *** +Documentation Tests for the parameter _split_ in the keyword +... _Execute SQL Script_ - special for the issue #184: +... https://github.com/MarketSquare/Robotframework-Database-Library/issues/184 + +Resource ../../resources/common.resource +Suite Setup Connect To DB +Suite Teardown Disconnect From Database +Test Setup Create Person Table +Test Teardown Drop Tables Person And Foobar + + +*** Test Cases *** +Split Commands + [Documentation] Such a simple script works always, + ... just check if the logs if the parameter value was processed properly + Execute Sql Script ${CURDIR}/../../resources/insert_data_in_person_table.sql split=True + +Don't Split Commands + [Documentation] Running such a script as a single statement works for PostgreSQL, + ... but fails in Oracle. Check in the logs if the splitting was disabled. + Execute Sql Script ${CURDIR}/../../resources/insert_data_in_person_table.sql split=False diff --git a/test/tests/utests/__init__.py b/test/tests/utests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/test_connection_manager.py b/test/tests/utests/test_connection_manager.py new file mode 100644 index 00000000..f53d8f01 --- /dev/null +++ b/test/tests/utests/test_connection_manager.py @@ -0,0 +1,37 @@ +import re +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from DatabaseLibrary.connection_manager import ConnectionManager + +TEST_DATA = Path(__file__).parent / "test_data" + + +class TestConnectWithConfigFile: + def test_connect_with_empty_config(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "empty.cfg") + with pytest.raises( + ValueError, + match="Required parameter 'db_module' was not provided - neither in keyword arguments nor in config file", + ): + conn_manager.connect_to_database(config_file=config_path) + + def test_aliased_section(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "alias.cfg") + with patch("importlib.import_module", new=MagicMock()) as client: + conn_manager.connect_to_database( + "my_client", + db_user="name", + db_password="password", + db_host="host", + db_port=0, + config_file=config_path, + alias="alias2", + ) + client.return_value.connect.assert_called_with( + database="example", user="name", password="password", host="host", port=0 + ) diff --git a/test/tests/utests/test_data/alias.cfg b/test/tests/utests/test_data/alias.cfg new file mode 100644 index 00000000..51b5ee56 --- /dev/null +++ b/test/tests/utests/test_data/alias.cfg @@ -0,0 +1,2 @@ +[alias2] +db_name = example diff --git a/test/tests/utests/test_data/empty.cfg b/test/tests/utests/test_data/empty.cfg new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/test_data/no_option.cfg b/test/tests/utests/test_data/no_option.cfg new file mode 100644 index 00000000..53c0731f --- /dev/null +++ b/test/tests/utests/test_data/no_option.cfg @@ -0,0 +1,3 @@ +[default] +db_name = example +db_user = example \ No newline at end of file