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:
parent
1826c8cbec
commit
1ab7e9b1b7
20
docs/Makefile
Normal file
20
docs/Makefile
Normal 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
643
docs/_static/basic.css
vendored
Normal 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
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
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
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
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
8
docs/changelog.txt
Normal file
@ -0,0 +1,8 @@
|
||||
#########
|
||||
Changelog
|
||||
#########
|
||||
|
||||
Version 0.12.1
|
||||
==============
|
||||
|
||||
Initial release.
|
199
docs/conf.py
Normal file
199
docs/conf.py
Normal 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
30
docs/index.txt
Normal 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
6
docs/license.txt
Normal file
@ -0,0 +1,6 @@
|
||||
#######
|
||||
License
|
||||
#######
|
||||
|
||||
.. include:: ../LICENSE
|
||||
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal 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
128
docs/reference.txt
Normal 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
3
docs/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
sphinx-bootstrap-theme>=0.8.0
|
||||
sphinx-autodoc-typehints>=1.17.0
|
||||
.
|
925
docs/usage-guide.txt
Normal file
925
docs/usage-guide.txt
Normal 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
|
Loading…
Reference in New Issue
Block a user