6
0
mirror of https://github.com/FirebirdSQL/firebird-qa.git synced 2025-01-22 13:33:07 +01:00

Documentation (Sphinx)

This commit is contained in:
Pavel Císař 2022-02-24 19:18:17 +01:00
parent 1826c8cbec
commit 1ab7e9b1b7
14 changed files with 1997 additions and 0 deletions

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

643
docs/_static/basic.css vendored Normal file
View File

@ -0,0 +1,643 @@
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
word-wrap: break-word;
overflow-wrap : break-word;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar #searchbox input[type="text"] {
float: left;
width: 80%;
padding: 0.25em;
box-sizing: border-box;
}
div.sphinxsidebar #searchbox input[type="submit"] {
float: left;
width: 20%;
border-left: none;
padding: 0.25em;
box-sizing: border-box;
}
img {
border: 0;
max-width: 100%;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li div.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
margin-left: auto;
margin-right: auto;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable {
width: 100%;
}
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable ul {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
table.indextable > tbody > tr > td > ul {
padding-left: 0em;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
div.modindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
div.genindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
/* -- domain module index --------------------------------------------------- */
table.modindextable td {
padding: 2px;
border-collapse: collapse;
}
/* -- general body styles --------------------------------------------------- */
div.body {
min-width: 450px;
max-width: 1920px;
}
div.body p, div.body dd, div.body li, div.body blockquote {
-moz-hyphens: auto;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
a.headerlink {
visibility: hidden;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink,
caption:hover > a.headerlink,
p.caption:hover > a.headerlink,
div.code-block-caption:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
img.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px 7px 0 7px;
background-color: #ffe;
width: 40%;
float: right;
}
p.sidebar-title {
font-weight: bold;
}
/* -- topics ---------------------------------------------------------------- */
div.topic {
border: 1px solid #ccc;
padding: 7px 7px 0 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
div.admonition dl {
margin-bottom: 0;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
span.pre {
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
}
td.linenos pre {
padding: 5px 0px;
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
margin-left: 0.5em;
}
table.highlighttable td {
padding: 0 0.5em 0 0.5em;
}
div.code-block-caption {
padding: 2px 5px;
font-size: small;
}
div.code-block-caption code {
background-color: transparent;
}
div.code-block-caption + div > div.highlight > pre {
margin-top: 0;
}
div.code-block-caption span.caption-number {
padding: 0.1em 0.3em;
font-style: italic;
}
div.code-block-caption span.caption-text {
}
div.literal-block-wrapper {
padding: 1em 1em 0;
}
div.literal-block-wrapper div.highlight {
margin: 0;
}
code.descname {
background-color: transparent;
font-weight: bold;
font-size: 1.2em;
border-style: none;
padding: 0;
}
code.descclassname {
background-color: transparent;
border-style: none;
padding: 0;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;
border-style: none;
padding: 0;
}
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
span.eqno a.headerlink {
position: relative;
left: 0px;
z-index: 1;
}
div.math:hover a.headerlink {
visibility: visible;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}
/* -- My additions ---------------------------------------------------------- */
div.note {
color: black;
border: 2px solid #7a9eec;
border-right-style: none;
border-left-style: none;
padding: 10px 20px 0px 60px;
background: #e1ecfe url(dialog-note.png) no-repeat 10px 8px;
}
div.danger {
color: black;
border: 2px solid #fbc2c4;
border-right-style: none;
border-left-style: none;
padding: 10px 20px 0px 60px;
background: #fbe3e4 url(dialog-note.png) no-repeat 10px 8px;
}
div.attention {
color: black;
border: 2px solid #ffd324;
border-right-style: none;
border-left-style: none;
padding: 10px 20px 0px 60px;
background: #fff6bf url(dialog-note.png) no-repeat 10px 8px;
}
div.caution {
color: black;
border: 2px solid #ffd324;
border-right-style: none;
border-left-style: none;
padding: 10px 20px 0px 60px;
background: #fff6bf url(dialog-warning.png) no-repeat 10px 8px;
}
div.important {
color: black;
background: #fbe3e4 url(dialog-seealso.png) no-repeat 10px 8px;
border: 2px solid #fbc2c4;
border-left-style: none;
border-right-style: none;
padding: 10px 20px 0px 60px;
}
div.seealso {
color: black;
background: #fff6bf url(dialog-seealso.png) no-repeat 10px 8px;
border: 2px solid #ffd324;
border-left-style: none;
border-right-style: none;
padding: 10px 20px 0px 60px;
}
div.hint, div.tip {
color: black;
background: #eeffcc url(dialog-topic.png) no-repeat 10px 8px;
border: 2px solid #aacc99;
border-left-style: none;
border-right-style: none;
padding: 10px 20px 0px 60px;
}
div.admonition-example {
color: black;
background: white url(dialog-topic.png) no-repeat 10px 8px;
border: 2px solid #aacc99;
border-left-style: none;
border-right-style: none;
padding: 10px 0px 20px 60px;
}
div.warning, div.error {
color: black;
background: #fbe3e4 url(dialog-warning.png) no-repeat 10px 8px;
border: 2px solid #fbc2c4;
border-right-style: none;
border-left-style: none;
padding: 10px 20px 0px 60px;
}
p {
text-align: justify;
padding-bottom: 5px;
}
h1 {
background: #fff6bf;
border: 2px solid #ffd324;
border-left-style: none;
border-right-style: none;
padding: 10px 10px 10px 10px;
text-align: center;
}
h2 {
/* background: #eeffcc; */
border: 2px solid #aacc99;
border-left-style: none;
border-right-style: none;
border-top-style: none;
padding: 10px 0px 0px 0px;
/* text-align: center; */
}
h3 {
/* background: #eeffcc; */
border: 1px solid #7a9eec;
border-left-style: none;
border-right-style: none;
border-top-style: none;
padding: 0;
/* text-align: center; */
}
h4 {
background: #eeffcc;
/* border: 1px solid #aacc99; */
border-left-style: none;
border-right-style: none;
border-top-style: none;
padding: 5px 5px 5px 5px;
/* text-align: center; */
}
cite {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
border: 1px solid #e1e1e8;
background: #f7f7f9;
margin: 0 0 10px;
padding: 0 5px 0 5px;
font-size: 13px;
font-style: italic;
}
.program {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
border: 1px solid #e1e1e8;
background: #f7f7f9;
margin: 0 0 10px;
padding: 0 5px 0 5px;
font-size: 13px;
}
/* dt/dd on single line */
dl.field-list {
display: grid;
grid-template-columns: max-content auto;
}
dt.field-list {
grid-column-start: 1;
}
dt.field-odd:after {
content: ':';
}
dt.field-even:after {
content: ':';
}
dd.field-list {
grid-column-start: 2;
}

BIN
docs/_static/dialog-note.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
docs/_static/dialog-seealso.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
docs/_static/dialog-topic.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
docs/_static/dialog-warning.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

8
docs/changelog.txt Normal file
View File

@ -0,0 +1,8 @@
#########
Changelog
#########
Version 0.12.1
==============
Initial release.

199
docs/conf.py Normal file
View File

@ -0,0 +1,199 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import sphinx_bootstrap_theme
# -- Project information -----------------------------------------------------
project = 'Firebird QA'
copyright = '2022, Pavel Cisar'
author = 'Pavel Císař'
# The short X.Y version
version = '0.12.1'
# The full version, including alpha/beta/rc tags
release = '0.12.1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.napoleon',
'sphinx_autodoc_typehints',
'sphinx.ext.todo',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.txt'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
default_role = 'py:obj'
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
#html_theme = 'alabaster'
html_theme = "bootstrap"
html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
# bootstrap theme config
# (Optional) Logo. Should be small enough to fit the navbar (ideally 24x24).
# Path should be relative to the ``_static`` files directory.
#html_logo = "my_logo.png"
# Theme options are theme-specific and customize the look and feel of a
# theme further.
html_theme_options = {
# Navigation bar title. (Default: ``project`` value)
#'navbar_title': "Firebird-qa",
# Tab name for entire site. (Default: "Site")
'navbar_site_name': "Content",
# A list of tuples containing pages or urls to link to.
# Valid tuples should be in the following forms:
# (name, page) # a link to a page
# (name, "/aa/bb", 1) # a link to an arbitrary relative url
# (name, "http://example.com", True) # arbitrary absolute url
# Note the "1" or "True" value above as the third argument to indicate
# an arbitrary url.
'navbar_links': [
("Usage Guide", "usage-guide"),
("Reference", "reference"),
("Index", "genindex"),
],
# Render the next and previous page links in navbar. (Default: true)
'navbar_sidebarrel': False,
# Render the current pages TOC in the navbar. (Default: true)
#'navbar_pagenav': True,
# Tab name for the current pages TOC. (Default: "Page")
#'navbar_pagenav_name': "Page",
# Global TOC depth for "site" navbar tab. (Default: 1)
# Switching to -1 shows all levels.
'globaltoc_depth': 3,
# Include hidden TOCs in Site navbar?
#
# Note: If this is "false", you cannot have mixed ``:hidden:`` and
# non-hidden ``toctree`` directives in the same page, or else the build
# will break.
#
# Values: "true" (default) or "false"
'globaltoc_includehidden': "true",
# HTML navbar class (Default: "navbar") to attach to <div> element.
# For black navbar, do "navbar navbar-inverse"
'navbar_class': "navbar navbar-inverse",
# Fix navigation bar to top of page?
# Values: "true" (default) or "false"
'navbar_fixed_top': "true",
# Location of link to source.
# Options are "nav" (default), "footer" or anything else to exclude.
'source_link_position': "none",
# Bootswatch (http://bootswatch.com/) theme.
#
# Options are nothing (default) or the name of a valid theme
# such as "cosmo" or "sandstone".
#
# The set of valid themes depend on the version of Bootstrap
# that's used (the next config option).
#
# Currently, the supported themes are:
# - Bootstrap 2: https://bootswatch.com/2
# - Bootstrap 3: https://bootswatch.com/3
#'bootswatch_theme': "united", # cerulean, flatly, lumen, materia, united, yeti
'bootswatch_theme': "cerulean",
# Choose Bootstrap version.
# Values: "3" (default) or "2" (in quotes)
'bootstrap_version': "2",
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# -- Extension configuration -------------------------------------------------
# Autodoc options
# ---------------
autodoc_default_options = {
'content': 'both',
'members': True,
'member-order': 'groupwise',
'undoc-members': True,
'exclude-members': '__weakref__',
'show-inheritance': True,
'no-inherited-members': True,
}
set_type_checking_flag = True
#always_document_param_types = True
# Napoleon options
# ----------------
napoleon_include_init_with_doc = True
napoleon_include_private_with_doc = True
napoleon_include_special_with_doc = True
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = True
napoleon_use_admonition_for_references = True
napoleon_use_ivar = False
napoleon_use_rtype = True
napoleon_attr_annotations = True
# -- Options for intersphinx extension ---------------------------------------
# intersphinx
intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
'base': ('https://firebird-base.rtfd.io/en/latest', None),
'driver': ('https://firebird-driver.rtfd.io/en/latest', None),
'pytest': ('https://docs.pytest.org/en/latest', None),
}
intersphinx_disabled_reftypes = []
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

30
docs/index.txt Normal file
View File

@ -0,0 +1,30 @@
###############
The Firebird QA
###############
The `firebird-qa`_ package contains official QA suite of the Firebird Project.
.. seealso:: Documentation for `firebird-driver`_.
Content
*******
.. toctree::
:maxdepth: 2
:caption: Contents:
usage-guide
reference
changelog
license
Indices and tables
******************
* :ref:`genindex`
* :ref:`modindex`
.. _firebird-driver: https://firebird-driver.rtfd.io/en/latest
.. _firebird-qa: https://github.com/FirebirdSQL/firebird-qa

6
docs/license.txt Normal file
View File

@ -0,0 +1,6 @@
#######
License
#######
.. include:: ../LICENSE

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

128
docs/reference.txt Normal file
View File

@ -0,0 +1,128 @@
.. module:: firebird.qa.plugin
:synopsis: Main Firebird driver code
############################
Firebird-QA plugin Reference
############################
functions and classes for use in tests
======================================
db_factory
----------
.. autofunction:: db_factory
user_factory
------------
.. autofunction:: user_factory
role_factory
------------
.. autofunction:: role_factory
envar_factory
-------------
.. autofunction:: envar_factory
temp_file
---------
.. autofunction:: temp_file
temp_files
----------
.. autofunction:: temp_files
isql_act
--------
.. autofunction:: isql_act
python_act
----------
.. autofunction:: python_act
Database
--------
.. autoclass:: Database
User
----
.. autoclass:: User
Role
----
.. autoclass:: Role
Envar
-----
.. autoclass:: Envar
ServerKeeper
------------
.. autoclass:: ServerKeeper
Action
------
.. autoclass:: Action
ExecutionError
--------------
.. autoclass:: ExecutionError
pytest hooks
============
pytest_addoption
----------------
.. autofunction:: pytest_addoption
pytest_report_header
--------------------
.. autofunction:: pytest_report_header
pytest_configure
----------------
.. autofunction:: pytest_configure
pytest_collection_modifyitems
-----------------------------
.. autofunction:: pytest_collection_modifyitems
pytest_runtest_makereport
-------------------------
.. autofunction:: pytest_runtest_makereport
Internal functions
==================
log_session_context
-------------------
.. autofunction:: log_session_context
set_tool
--------
.. autofunction:: set_tool
substitute_macros
-----------------
.. autofunction:: substitute_macros
db_path
-------
.. autofunction:: db_path
trace_thread
------------
.. autofunction:: trace_thread
Internal classes
================
TraceSession
------------
.. autoclass:: TraceSession
QATerminalReporter
------------------
.. autoclass:: QATerminalReporter
.. _firebird-driver: https://firebird-driver.rtfd.io/en/latest

3
docs/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
sphinx-bootstrap-theme>=0.8.0
sphinx-autodoc-typehints>=1.17.0
.

925
docs/usage-guide.txt Normal file
View File

@ -0,0 +1,925 @@
===========
Usage Guide
===========
.. currentModule:: firebird.qa
Before you start using Firebird QA suite
========================================
The Firebird QA suite is based on pytest_. If you are not familiar with this testing framework,
you should read at least next sections from pytest documentation:
1. `How to invoke pytest <https://docs.pytest.org/en/latest/how-to/usage.html>`_
2. `Command-line Flags <https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags>`_
3. `Pytest customization <https://docs.pytest.org/en/latest/reference/customize.html>`_
The Firebird QA suite resides in firebird-qa_ repository at github. This repository
contains a `pytest` plugin, various support files, and a set of tests that uses this plugin
to test Firebird server(s). Currentlly, only local Firebird installations could be tested.
.. note::
The suite could NOT be used to test Firebird servers older than v3.
Installation
============
Requirements
------------
1. Requires Python_ 3.8 or newer.
2. Requires `pip` installer. You may check it's availability from command line with::
> pip --help
If `pip` is not installed, you may install it with::
> python -m ensurepip
3. It's **recommended** to create separate Python virtual environment to install and run
the QA suite, especially on Linux where Python `site-packages` are managed by Linux
distribution package manager.
There are multiple ways how to create and manage Python virtual environments, but we
recommend to use virtualenv_, together with virtualenvwrapper_ (for Linux) or
virtualenvwrapper-win_ (for Windows).
On Linux, the `virtualenv` and `virtualenvwrapper` are typically available for installation
from ditribution repository via package manager, which is also the preferred way to
install them on this platform.
On Windows, you should install `virtualenv` and `virtualenvwrapper-win` via `pip`.
Installation
------------
1. Open the command prompt / terminal.
2. Clone the firebird-qa repository::
> git clone git://github.com/FirebirdSQL/firebird-qa.git
3. Activate the Python virtual environment you created for QA, or skip this step if you
want to install everything into main site-packages.
4. Switch to directory with cloned `firebird-qa` repository.
.. note::
We'll refer to this directory as `QA root directory`.
5. Install the plugin with pip, running::
> pip install -e .
This will install Firebird QA plugin for `pytest`, along with required dependencies.
.. important::
You must re-install the plugin every time you see that `git pull` updated
the `setup.cfg` file!
Configuration
=============
Firebird-driver configuration
-----------------------------
The QA plugin uses firebird-driver_ to access the Firebird servers, and uses
`driver configuration object <firebird.driver.config.DriverConfig>` to set up various driver and server/database connection parameters.
The configuration object is initialized from `firebird-driver.conf` file, and plugin
specifically utilizes server sections in this file. When pytest is invoked, you must specify
tested server with **--server <name>** option, where `<name>` is name of server configuration
section in `firebird-driver.conf` file.
This file is stored in firebird-qa repository, and defines default configuration suitable
to most QA setups.
.. note::
The `firebird-driver.conf` file is located in QA root directory. In default setup, the
QA plugin is used to test local Firebird installation with default user name and password
(SYSDBA/masterkey) via `local` server (configuration section).
.. important::
The firebird-driver currently does not support specification of client library in server
sections. However, the QA plugin works around that limitation. If server section for
tested server contains `fb_client_library` option specification, it's copied to global
setting.
.. seealso::
See `configuration <https://firebird-driver.readthedocs.io/en/latest/usage-guide.html#configuration>`_
chapter in driver documentation for details.
Pytest configuration
--------------------
While it's not required, it's recommended to create `pytest configuration file
<https://docs.pytest.org/en/latest/reference/customize.html>`_ in QA root directory.
You may use this file to simplify your use of pytest with `addopts` option, or adjust
pytest behaviour.
.. tip::
Suggested options for pytest.ini::
console_output_style = count
testpaths = tests
addopts = --server local --install-terminal
Firebird server configuration
-----------------------------
Some tests in Firebird test suite require specific Firebird server configuration to work
properly (as designed). If possible, these tests check the configuration of tested server,
and mark itself to SKIP if required conditions are not met. However, it's not always possible
(or desirable) to perform such check. You have to cosult `Firebird QA README` for current
requirements on Firebird server configuration.
Running QA test suite
=====================
Basics
------
1. Open the terminal / command-line.
2. If you installed Firebird QA in Python virtual environment, **activate it**.
3. Switch to QA root directory.
4. To run all tests in suite against local Firebird server, invoke::
pytest --server local ./tests
.. tip::
If you created `pytest.ini` with recommended values, you can just invoke `pytest`
without additional parameters.
pytest report header
--------------------
When pytest is invoked, a report header is printed on terminal before individual tests
are executed. The QA plugin extend this header with next information:
* Python encodings
- system
- locale
- filesystem
* Information about tested Firebird server
- conf. section name
- version
- mode
- architecture
- home directory
- tools directory
- used client library
Example::
> pytest
====================================================== test session starts =======================================================
platform linux -- Python 3.8.12, pytest-7.0.0, pluggy-1.0.0 -- /home/job/python/envs/qa/bin/python
cachedir: .pytest_cache
System:
encodings: sys:utf-8 locale:UTF-8 filesystem:utf-8
Firebird:
server: local [v3.0.9.33562, SuperServer, Firebird/Linux/AMD/Intel/x64]
home: /opt/firebird
bin: /opt/firebird/bin
client library: libfbclient.so.2
rootdir: /home/job/python/projects/firebird-qa, configfile: pytest.ini, testpaths: tests
plugins: firebird-qa-0.12.1
collected 2385 items / 475 deselected / 1910 selected
issue.full-join-push-where-predicate PASSED [ 1/1910]
...
pytest switches installed by QA plugin
--------------------------------------
The QA plugin installs several pytest command-line switches. When you run `pytest --help`,
they are listed in `Firebird QA` section::
Firebird QA:
--server=SERVER Server configuration name
--bin-dir=PATH Path to directory with Firebird utilities
--protocol={xnet,inet,inet4,wnet}
Network protocol used for database attachments
--runslow Run slow tests
--save-output Save test std[out|err] output to files
--skip-deselected={platform,version,any}
SKIP tests instead deselection
--extend-xml Extend XML JUnit report with additional information
--install-terminal Use our own terminal reporter
server
~~~~~~
**REQUIRED option.** Section name in `firebird-driver.conf` with connection parameters for
tested server.
bin-dir
~~~~~~~
Normally, the QA plugin detects and properly sets the directory where Firebird tools are
installed. However, you can set this directory explicitly using the `--bin-dir` switch.
protocol
~~~~~~~~
Override for network protocol specified in `firebird-driver.conf` file (or default).
runslow
~~~~~~~
Tests that run for longer than 10 minutes on equipment used for regular Firebird QA are
marked as `slow`. They are not executed, unless this switch is specified.
.. note:: Currently, there are no slow tests in Firebird test suite.
save-output
~~~~~~~~~~~
**Experimental switch**
When this switch is specified, stdout/stderr output of external Firebird tool executed by
test is stored in `./out` subdirectory. Intended for test debugging.
skip-deselected
~~~~~~~~~~~~~~~
Tests that are not applicable to tested server (because they are for specific platform or
Firebird versions) are deselected during pytest collection phase. It means that they are
not shown in test session report. This switch changes the routine, so tests are marked to
skip (with message explaining why) instead deselection, so they show up is session report.
extend-xml
~~~~~~~~~~
When this switch is used together with `--junitxml` switch, the produced JUnitXML file
will contain additional metadata for `testsuite` and `testcase` elements recorded as
`property` sub-elements.
.. important::
Please note that using this feature will break schema verifications for the latest
JUnitXML schema. This might be a problem when used with some CI servers.
install-terminal
~~~~~~~~~~~~~~~~
This option changes default pytest terminal reporter that displays pytest NODE IDs, to custom
reporter that displays Firebord QA test IDs.
pytest node IDs are of the form `module.py::class::method` or `module.py::function`.
Firebord QA test IDs are defined in our test metadata.
.. important::
Right now, the custom terminal is `opt-in` feature. This will be changed in some future
release to `opt-out` using new switch.
Tests for Firebird engine
=========================
Test suite
----------
The Firebird QA test suite is located in `tests` subdirectory of QA root directory. Because
Firebird tests are written in Python, the test suite directory is a `Python package`_, so
each directory **must** contain `__init__.py` file.
Test files
----------
For pytest framework, a single test is a function or class method that is executed during
test session. Single Python module can contain arbitrary number of test functions/methods.
Firebird QA uses slightly different model, where each test is a separate Python file (module_)
that provides one or more specific test implementations as module-level test functions, and
only one function is selected by pytest for execution. The selection is typically performed
by marking tests to be executed only on certain platform and/or Firebird engine version
using `pytest.mark.version` or `pytest.mark.platform` decorators. The QA plugin then uses
these marks to deselect (or skip) test functions that are not applicateble to tested Firebird
engine.
Test files must have `.py` extension and name that either starts with `test_` or ends with
`_test`.
Test encoding
-------------
Test files must be encoded in utf-8, and first line must specify this encoding::
#coding:utf-8
Test metadata
-------------
Test files must have a docstring_ with test metadata. Each metadata item must start on
separate line starting with item tag followed by `colon`.
.. list-table:: **Currently supported metadata items**
:widths: 20 60 10 10
:header-rows: 1
* - Tag
- Description
- Required
- Multiline
* - ID
- Unique test identification. Can contain alphanumeric characters, dot, underscore and
hyphen. Must start with alphanum character.
- **Yes**
- No
* - TITLE
- Test title. Multiline titles are concatenated into single line (line breaks
removed and line contents separated with single space).
- **Yes**
- Yes
* - DESCRIPTION
- Test description
- No
- Yes
* - NOTES
- Notes for test (change log etc.)
- No
- Yes
* - ISSUE
- GitHub issue number
- No
- No
* - JIRA
- Legacy JIRA issue ID
- No
- No
* - FBTEST
- Legacy fbtest test ID
- No
- No
Test functions
--------------
Each test is implemented as module-level function(s) with name starting with `test_`.
.. important::
There could be multiple test variants implemented as separate test functions, but their
implementation must ensure that **only one** version is selected by pytest for execution!
Typical multi-variant scenario uses individual test variants marked for run on specific
platform, or against specific Firebird versions using `pytest.mark.version` or
`pytest.mark.platform` decorators.
Test functions typically use various :doc:`fixtures <pytest:explanation/fixtures>` provided
by QA plugin or pytest itself. In most cases, the test outcome is determined using `assert`
statements.
Fixtures
--------
The QA plugin implements fixture factories that provide resources and facilities frequently
used by Firebird tests. Fixtures that provide temporary resources (like databases, users,
files) ensure their initialization before test execution and removal when test finishes.
.. note::
Fixtures returned by fixture factories must be assigned to module-level variables.
Variable names are then used as parameter names of test functions.
Example::
# fixture that provides Action object used in test function
act = python_act('db')
# test function
def test_1(act: Action):
act.execute()
...
Fixture values are typically a class instance that allows access to provided resource.
Database
~~~~~~~~
Almost all tests need a database. The `.db_factory` function creates a fixture that provides
`.Database` object. Test may use this object to create connections, access database
parameters or perform other database-related actions.
User
~~~~
Some tests may need to use different user accounts than SYSDBA, or multiple user accounts.
The `.user_factory` function creates a fixture that provides `.User` object. Beside automatic
setup/teardown of temporary Firebird user account, tests may use this object to access
user parameters or perform other user-related actions.
Action
~~~~~~
The `.Action` object is a "Swiss army knife" provided by QA plugin to simplify implementation
of Firebird tests. There are two Action fixture factories:
* `.isql_act` for simple tests that use single ISQL test script.
* `.python_act` for more complex test implementations.
Role
~~~~
The `.role_factory` function creates a fixture that provides `.Role` object representing
SQL role associated with specified test database.
Envar
~~~~~
The `.envar_factory` function creates a fixture that could be used to temporary set value
to environment variable.
Temporary files
~~~~~~~~~~~~~~~
Although pytest provides fixtures for temporary files, QA plugin provides its own fixture
factories `.temp_file` and `.temp_files`.
Example test file
-----------------
Example test file `tests/issue/test_319.py`::
#coding:utf-8
"""
ID: issue-319
ISSUE: 319
JIRA: CORE-1
TITLE: Server shutdown
DESCRIPTION: Server shuts down when user password is attempted to be modified to a empty string
FBTEST: bugs.core_0001
"""
import pytest
from firebird.qa import *
# fixture providing test database
db = db_factory()
# fixture providing temporary user
user = user_factory('db', name='tmp$c0001', password='123')
# isql script executed to test Firebird
test_script = """
alter user tmp$c0001 password '';
commit;
"""
# fixture that provides Action object used in test function
act = isql_act('db', test_script)
# Expected stderr output from isql
expected_stderr = """
Statement failed, SQLSTATE = 42000
unsuccessful metadata update
-ALTER USER TMP$C0001 failed
-Password should not be empty string
"""
# Test function, marked to run on Firebird v3.0 or newer
@pytest.mark.version('>=3.0')
def test_1(act: Action, user: User):
act.expected_stderr = expected_stderr
act.execute()
# This evaluates test outcome
assert act.clean_stderr == act.clean_expected_stderr
How-to guides
=============
How to use databases in tests
-----------------------------
Database fixture
~~~~~~~~~~~~~~~~
It's recommend to use fixtures created by `.db_factory` function. Function arguments specify
how database is created, initialized and removed.
* If not specified otherwise, the fixture creates new empty database.
* To create database from backup file, use `from_backup` argument. File must be located in
`backups` directory.
* To use copy of prepared database, use `copy_of` argument. File must be located in `databases`
directory.
* The name of created temporary database could be specifid with `filename` argument. Default
database name is `test.fdb`.
* It's possible to specify `page_size` and `sql_dialect` of created database. These options
are ignored if database is created as a copy, or from backup.
* It's possible to specify database `charset` (not applid for backups and copies) that is
also default connection charset.
* After temporary database is created (by either method), it could be initialized with SQL
commands (executed via isql) specified using `init` argument.
* Database is created using default server user and password. It's possible to specify
alternate credentials with `user` and `password` arguments.
* The fixture ensures that database is created an initialized during test setup, and removed
during test teardown. To disable either phase (because create/drop is performed by test
itself), use `do_not_create` or `do_not_drop` arguments.
* By default, database is set to `async` write after creation to speed up database operations.
It's possible to change that with `async_write` argument.
* The database is `registered <firebird.driver.config.DriverConfig.register_database>` in firebird
driver configuration as `fbtest`. You can specify the configuration name explicitly with
`config` argument.
.. note::
The returned fixture must be assigned to module-level variable. Name of this variable
is important, as it's used to reference the fixture in other fixture-factory functions
that use the database, and the test function itself.
Examples::
# new empty database with default charset, page size and SQL dialect 3
db = db_factory()
# database created from backup
db = db_factory(from_backup='mon-stat-gathering-2_5.fbk')
# new empty database with default charset, page size and SQL dialect 1 initialized with
# isql script
init_script = """create table T1 (F1 char(4), F2 char(4));
create index T1_F1 on T1 (F1);
insert into T1 (F1, F2) values ('001', '001');
insert into T1 (F1, F2) values ('002', '002');
insert into T1 (F1, F2) values ('003', '003');
insert into T1 (F1, F2) values ('004', '004');
commit;
"""
db = db_factory(sql_dialect=1, init=init_script)
# new empty database with ISO8859_1 charset, SQL dialect 3 and default page size
db = db_factory(charset='ISO8859_1')
Primary test database
~~~~~~~~~~~~~~~~~~~~~
Because almost all Firebird tests need a database, the QA plugins works with concept of
`primary test database`. This fixture is typically named `db`, and is used by other fixtures
that work with database.
.. important::
Database fixture is referenced by other QA plugin fixtures `by name`, not `by value`,
so you have to pass the fixture name as string!
Example::
db = db_factory()
act = python_act('db')
.. note::
When test has multiple variants, these variants typically use database with the same
parameters and content, so they can use the single database fixture. In rare cases where
individual test variants need different databases, the usual naming scheme for primary
databases is **db_<number>**.
Test functions that use the `.Action` object provided by fixtures created with `.isql_act()`
and `.python_act()` does not need to use the primary test database directly, because its
exposed as `.Action.db` attribute.
Example::
db = db_factory()
act = python_act('db')
@pytest.mark.version('>=3.0')
def test_1(act: Action):
# SQL executed on primary test database
act.isql(switches=[], input="show database;")
# Using connection to primary test database
with act.db.connect() as con:
...
Additional databases
~~~~~~~~~~~~~~~~~~~~
Some tests need more than one database. Fixtures for these databases must be used directly
by test function.
Example::
db = db_factory()
act = python_act('db')
db_dml_sessions = db_factory(sql_dialect=3, init=init_script, filename='tmp_5087_dml_heavy.fdb')
@pytest.mark.version('>=3.0')
def test_1(act: Action, db_dml_sessions: Database):
# Using connection to primary test database
with act.db.connect() as con:
...
# Using connection to secondary test database
with db_dml_sessions.connect() as con:
...
The Database object
~~~~~~~~~~~~~~~~~~~
Database fixtures provide `.Database` instance that allows access to test database.
The `~.Database.connect()` method creates new `connection <firebird.driver.core.Connection>`
to database. It's recommended to manage connection using the `with` statement::
with act.db.connect() as con:
...
Database atributes are often needed to use the database with external tools:
* `~.Database.db_path`: Full path to test database.
* `~.Database.dsn`: DSN to test database.
* `~.Database.charset`: Name of database CHARACTER SET
* `~.Database.config_name`: firebird-driver database configuration name
* `~.Database.user`: User name
* `~.Database.password`: Password
.. seealso:: `.Database` documentation for full reference.
How to use the Action object
----------------------------
Action fixture
~~~~~~~~~~~~~~
The `.Action` object is provided by fixture created by `.isql_act()` or `.python_act()`
factory function. These functions are identical (it's in fact only one function available
under two names). It's a legacy from old `fbtest` QA system that had two types of tests, and
such distinction was retained during conversion of tests from old system to new one.
Although there is no difference, it's recommended to retain the distinction in new test by
using:
* `.isql_act` for simple tests that use single ISQL test script.
* `.python_act` for more complex test implementations.
This function has next arguments:
* `db_fixture_name`: REQUIRED. Name of database fixture (primary database).
* `script`: OptionalSQL script that tests the server.
* `substitutions`: Optional list of additional substitution for stdout/stderr.
.. note::
The returned fixture must be assigned to module-level variable. It's typically named `act`.
When test has multiple variants and these variants use the same primary database and
substitutions, they can use the single action fixture. In cases where
individual test variants need different actions, the usual naming scheme for them is
**act_<number>**.
Example::
db = db_factory()
act = python_act('db')
@pytest.mark.version('>=3.0')
def test_1(act: Action):
...
The Action class
~~~~~~~~~~~~~~~~
The `.Action` is multipurpose object, that could be used to:
* Execute external Firebird tools and capture their output with `~.Action.execute()`,
`~.Action.extract_meta()`, `~.Action.isql()`, `~.Action.gstat()`, `~.Action.gsec()`,
`~.Action.gbak()`, `~.Action.gfix()`, `~.Action.nbackup()` and `~.Action.svcmgr()`
* Access primary test database as `~.Action.db` attribute.
* Create `connection <firebird.driver.core.Server>` to Firebird service manager with
`~.Action.connect_server()`.
* Query server configuration with `~.Action.get_config()`.
* Prind data from `cursor <firebird.driver.core.Cursor>` with `~.Action.print_data()` and
`~.Action.print_data_list()`.
* Get content of Firebird server log with `~.Action.get_firebird_log()`.
* Check Firebird server version with `~.Action.is_version()`.
* Determine Firebird server architecture with `~.Action.get_server_architecture()`.
* Create arbitrary DSN for database connections with `~.Action.get_dsn()`.
* Check presence of regex patterns in string using `~.Action.match_any()`.
* Work with Firebird trace sessions using `~.Action.trace()` and `~.Action.trace_to_stdout()`.
* Temporary set environment variables with `~.Action.envar()`.
* Redirect output of services to stdout with `~.Action.print_callback`.
* Access test execution environment with `.Action` attributes and properties.
Using external Firebird tools
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
External Firebird tools could be executed with methods `~.Action.execute()`,
`~.Action.extract_meta()`, `~.Action.isql()`, `~.Action.gstat()`, `~.Action.gsec()`,
`~.Action.gbak()`, `~.Action.gfix()`, `~.Action.nbackup()` and `~.Action.svcmgr()`.
All these methods store results into `~.Action.stdout`, `~.Action.stderr` and
`~.Action.return_code` attributes. Test may assign expected outputs into `~.Action.expected_stdout`
and `~.Action.expected_stderr` attributes to perform asserts between real and expected output.
However, output must be often cleaned from unwanted or irrelevant parts (especially isql
output contains many "noise" parts). The `.Action` properties `~.Action.clean_stdout`,
`~.Action.clean_stderr`, `~.Action.clean_expected_stdout` and `~.Action.clean_expected_stderr`
provide such clean content.
The tool execution may fail, which could be expected or unexpected by test. Expected
fails must be indicated by assigning *ANY* string to `~.Action.expected_stderr` before
tool is executed. In such case no error is reported and test may assert that execution
failed in expected way. If failure is unexpected, an `.ExecutionError` exception is raised.
Example of test with expected failure::
import pytest
from firebird.qa import *
db = db_factory()
user = user_factory('db', name='tmp$c0001', password='123')
test_script = """
alter user tmp$c0001 password '';
commit;
"""
act = isql_act('db', test_script)
expected_stderr = """
Statement failed, SQLSTATE = 42000
unsuccessful metadata update
-ALTER USER TMP$C0001 failed
-Password should not be empty string
"""
@pytest.mark.version('>=3.0')
def test_1(act: Action, user: User):
act.expected_stderr = expected_stderr
act.execute()
assert act.clean_stderr == act.clean_expected_stderr
Example of test that will raise an exception of failure::
import pytest
from firebird.qa import *
db = db_factory()
test_script = """
set list on;
create table t1 (
campo1 numeric(15,2),
campo2 numeric(15,2)
);
commit;
set term ^;
create procedure teste
returns (
retorno numeric(15,2))
as
begin
execute statement 'select first 1 (campo1*campo2) from t1' into :retorno;
suspend;
end
^
set term ;^
commit;
insert into t1 (campo1, campo2) values (10.5, 5.5);
commit;
select * from teste;
"""
act = isql_act('db', test_script)
expected_stdout = """
RETORNO 57.75
"""
@pytest.mark.version('>=3')
def test_1(act: Action):
act.expected_stdout = expected_stdout
act.execute()
assert act.clean_stdout == act.clean_expected_stdout
.. important::
If test performs multiple executions, it's neccessary to call `.Action.reset()` to
reinitialize internal variables. Otherwise "clean" functions will return wrong values,
and you can experience other annomalies.
Example::
@pytest.mark.version('>=3.0')
def test_1(act: Action, tmp_file: Path):
tmp_file.write_bytes(non_ascii_ddl.encode('cp1251'))
# run without specifying charset
act.expected_stdout = expected_stdout_a
act.expected_stderr = expected_stderr_a_40 if act.is_version('>=4.0') else expected_stderr_a_30
act.isql(switches=['-q'], input_file=tmp_file, charset=None, io_enc='cp1251')
assert (act.clean_stdout == act.clean_expected_stdout and
act.clean_stderr == act.clean_expected_stderr)
# run with charset
act.reset() # <-- Necessary to reinitialize internal variables
act.isql(switches=['-q'], input_file=tmp_file, charset='win1251', io_enc='cp1251')
assert act.clean_stdout.endswith('Metadata created OK.')
Using trace
~~~~~~~~~~~
Test can use Firebird trace session using `~.Action.trace()` method. This method returns
`.TraceSession` context manager instance that runs trace session in separate thread.
There are two (mutually exclusive) methods how to configure the trace session:
1. Using `db_events` and/or `svc_events` lists, and optional `database` specification.
This method is more convenient and readable, as you don't need to worry about
proper construction of trace config string (brackets etc.)
2. Using `config`, when you specifically need to pass configuration in original "full" format.
.. important::
It's absolutely necessary to use the :ref:`with <with>` statement to manage the
trace session!
Example::
import pytest
import platform
from firebird.qa import *
db = db_factory()
act = python_act('db', substitutions=[('^((?!records fetched).)*$', '')])
expected_stdout = """
1 records fetched
"""
test_script = """
set list on;
-- statistics for this statement SHOULD appear in trace log:
select 1 k1 from rdb$database;
commit;
-- statistics for this statement should NOT appear in trace log:
select 2 k2 from rdb$types rows 2 /* no_trace*/;
-- statistics for this statement should NOT appear in trace log:
select 3 no_trace from rdb$types rows 3;
-- statistics for this statement should NOT appear in trace log:
set term ^;
execute block returns(k4 int) as
begin
for select 4 from rdb$types rows 4 into k4 do suspend;
end -- no_trace
^
set term ;^
"""
trace = ['log_connections = true',
'log_transactions = true',
'log_statement_finish = true',
'print_plan = true',
'print_perf = true',
'time_threshold = 0',
'exclude_filter = %no_trace%',
]
@pytest.mark.version('>=3.0')
def test_1(act: Action):
with act.trace(db_events=trace):
# Actions that would be traced
act.isql(switches=['-n'], input=test_script)
# Actions that are not traced
act.expected_stdout = expected_stdout
act.trace_to_stdout()
assert act.clean_stdout == act.clean_expected_stdout
How to use users
----------------
How to use roles
----------------
.. _Python: http://www.python.org
.. _virtualenv: https://virtualenv.pypa.io/en/latest/
.. _virtualenvwrapper: https://virtualenvwrapper.rtfd.io
.. _virtualenvwrapper-win: https://github.com/davidmarble/virtualenvwrapper-win/
.. _firebird-base: https://firebird-base.rtfd.io
.. _firebird-driver: https://firebird-driver.rtfd.io
.. _pytest: https://docs.pytest.org
.. _firebird-qa: https://github.com/FirebirdSQL/firebird-qa
.. _Python package: https://docs.python.org/3/tutorial/modules.html#packages
.. _module: https://docs.python.org/3/tutorial/modules.html
.. _docstring: https://docs.python.org/3/glossary.html#term-docstring