diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 00000000..d4bb2cbb
--- /dev/null
+++ b/docs/Makefile
@@ -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)
diff --git a/docs/_static/basic.css b/docs/_static/basic.css
new file mode 100644
index 00000000..ed50512c
--- /dev/null
+++ b/docs/_static/basic.css
@@ -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;
+}
diff --git a/docs/_static/dialog-note.png b/docs/_static/dialog-note.png
new file mode 100644
index 00000000..263fbd58
Binary files /dev/null and b/docs/_static/dialog-note.png differ
diff --git a/docs/_static/dialog-seealso.png b/docs/_static/dialog-seealso.png
new file mode 100644
index 00000000..3eb7b05c
Binary files /dev/null and b/docs/_static/dialog-seealso.png differ
diff --git a/docs/_static/dialog-topic.png b/docs/_static/dialog-topic.png
new file mode 100644
index 00000000..2ac57475
Binary files /dev/null and b/docs/_static/dialog-topic.png differ
diff --git a/docs/_static/dialog-warning.png b/docs/_static/dialog-warning.png
new file mode 100644
index 00000000..7233d45d
Binary files /dev/null and b/docs/_static/dialog-warning.png differ
diff --git a/docs/changelog.txt b/docs/changelog.txt
new file mode 100644
index 00000000..d3e990e3
--- /dev/null
+++ b/docs/changelog.txt
@@ -0,0 +1,8 @@
+#########
+Changelog
+#########
+
+Version 0.12.1
+==============
+
+Initial release.
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..6f842be4
--- /dev/null
+++ b/docs/conf.py
@@ -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
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
diff --git a/docs/index.txt b/docs/index.txt
new file mode 100644
index 00000000..3aebf574
--- /dev/null
+++ b/docs/index.txt
@@ -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
diff --git a/docs/license.txt b/docs/license.txt
new file mode 100644
index 00000000..935e89de
--- /dev/null
+++ b/docs/license.txt
@@ -0,0 +1,6 @@
+#######
+License
+#######
+
+.. include:: ../LICENSE
+
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 00000000..153be5e2
--- /dev/null
+++ b/docs/make.bat
@@ -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
diff --git a/docs/reference.txt b/docs/reference.txt
new file mode 100644
index 00000000..8f08fc18
--- /dev/null
+++ b/docs/reference.txt
@@ -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
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..b5712db4
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,3 @@
+sphinx-bootstrap-theme>=0.8.0
+sphinx-autodoc-typehints>=1.17.0
+.
diff --git a/docs/usage-guide.txt b/docs/usage-guide.txt
new file mode 100644
index 00000000..ba791d87
--- /dev/null
+++ b/docs/usage-guide.txt
@@ -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
`_
+2. `Command-line Flags `_
+3. `Pytest customization `_
+
+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 ` 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 ** option, where `` 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 `_
+ chapter in driver documentation for details.
+
+Pytest configuration
+--------------------
+
+While it's not required, it's recommended to create `pytest configuration file
+`_ 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 ` 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 ` 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_**.
+
+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 `
+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_**.
+
+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 ` to Firebird service manager with
+ `~.Action.connect_server()`.
+* Query server configuration with `~.Action.get_config()`.
+* Prind data from `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 ` 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