Combined fe and be (#479)
* fix main.js conflicts * fix js test * fix resource (main.js) test * revert change to report extra * filters style * css and dom brush up * Buildable app * always store data in html * Always store data blob in file * json dump test data * read data from dom element * manually initialize state * minimalistic dataset * simplify included files * Handle report extras * Handle python report hooks * imgviewer * present name in image viewer and open img on click * setup linter for project * conform to styles * show video in imageviewer (#14) * show video in imageviewer * Chore: Pluralize extra (#15) * Add extras.HTML * Move outcome handling to backend (#18) * Move outcome handling to backend * Pass in text version of longrepr if present * make collapse/expand all functional (#20) * make collapse/expand all functional * only create links for text, url and json (#22) * make filter search case insensitive (#21) * make filter search case insensitive * use sessionStorage to prevent preferences to be reapplied on new reports * avoid multiple event bindings + fix filter bug * Collapse individual row Co-authored-by: Jim Brännlund <jim.brannlund@gmail.com> * add no log output captured string * Query params (#25) * Add query params * adjust tests (#26) * Duration format (#27) * adjust tests * build format handler * remove dependency --------- Co-authored-by: Jim Brännlund <jimbrannlund@fastmail.com> * Beyondevil/cleanup (#28) * chore: Cleanup branch before merge * chore: Fix duration and CI * Fix pre-commit issues --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Viktor Gustafsson <vikt.gust@gmail.com>
This commit is contained in:
parent
aa85f41296
commit
0408b0d504
|
@ -1,32 +1,30 @@
|
||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
"es2021": true
|
"es2021": true
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": [
|
||||||
|
"google"
|
||||||
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 12
|
"ecmaVersion": "latest"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"array-bracket-spacing": "error",
|
"array-bracket-spacing": "error",
|
||||||
"block-scoped-var": "error",
|
"block-scoped-var": "error",
|
||||||
"block-spacing": "error",
|
"block-spacing": "error",
|
||||||
"brace-style": "error",
|
"brace-style": "error",
|
||||||
"camelcase": "error",
|
"camelcase": "off",
|
||||||
"class-methods-use-this": "error",
|
"class-methods-use-this": "error",
|
||||||
"consistent-return": "error",
|
"consistent-return": "error",
|
||||||
"default-case": "error",
|
"default-case": "error",
|
||||||
"default-case-last": "error",
|
"default-case-last": "error",
|
||||||
"default-param-last": "error",
|
"default-param-last": "error",
|
||||||
"grouped-accessor-pairs": "error",
|
"grouped-accessor-pairs": "error",
|
||||||
"indent": [
|
"indent": [ "error", 4 ],
|
||||||
"error",
|
"linebreak-style": [ "error", "unix" ],
|
||||||
4
|
"max-len": ["error", { "code": 120 }],
|
||||||
],
|
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"no-caller": "error",
|
"no-caller": "error",
|
||||||
"no-console": "error",
|
"no-console": "error",
|
||||||
"no-empty-function": "error",
|
"no-empty-function": "error",
|
||||||
|
@ -43,17 +41,25 @@
|
||||||
"no-throw-literal": "error",
|
"no-throw-literal": "error",
|
||||||
"no-undefined": "error",
|
"no-undefined": "error",
|
||||||
"no-unreachable-loop": "error",
|
"no-unreachable-loop": "error",
|
||||||
"no-unused-expressions": "error",
|
"no-unused-expressions": "off",
|
||||||
"no-useless-backreference": "error",
|
"no-useless-backreference": "error",
|
||||||
"no-useless-concat": "error",
|
"no-useless-concat": "error",
|
||||||
"no-var": "error",
|
"no-var": "error",
|
||||||
|
"object-curly-spacing": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"arraysInObjects": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"prefer-const": "error",
|
"prefer-const": "error",
|
||||||
"prefer-promise-reject-errors": "error",
|
"prefer-promise-reject-errors": "error",
|
||||||
"require-atomic-updates": "error",
|
"require-atomic-updates": "error",
|
||||||
"require-await": "error",
|
"require-await": "error",
|
||||||
|
"require-jsdoc" : 0,
|
||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
"always"
|
"never"
|
||||||
],
|
],
|
||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"error",
|
||||||
|
|
|
@ -18,34 +18,36 @@ jobs:
|
||||||
name: Build Docs
|
name: Build Docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: '3.10'
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: python -m pip install --upgrade tox
|
run: python -m pip install --upgrade tox
|
||||||
- name: Build docs with tox
|
- name: Build docs with tox
|
||||||
run: python -m tox -e docs
|
run: tox -e docs
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
|
if: github.repository_owner == 'pytest-dev'
|
||||||
name: Publish to PyPI registry
|
name: Publish to PyPI registry
|
||||||
needs:
|
needs:
|
||||||
- tests
|
- tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PY_COLORS: 1
|
PY_COLORS: 1
|
||||||
TOXENV: packaging
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-python@v3
|
- name: Switch to using Python 3.10 by default
|
||||||
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: python -m pip install --user tox
|
run: python -m pip install --user tox
|
||||||
|
|
||||||
- name: Check out src from Git
|
- name: Check out src from Git
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
@ -62,6 +64,7 @@ jobs:
|
||||||
) &&
|
) &&
|
||||||
1 || 0
|
1 || 0
|
||||||
}}
|
}}
|
||||||
|
|
||||||
- name: Drop Git tags from HEAD for non-tag-create events
|
- name: Drop Git tags from HEAD for non-tag-create events
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name != 'create' ||
|
github.event_name != 'create' ||
|
||||||
|
@ -70,8 +73,16 @@ jobs:
|
||||||
git tag --points-at HEAD
|
git tag --points-at HEAD
|
||||||
|
|
|
|
||||||
xargs git tag --delete
|
xargs git tag --delete
|
||||||
- name: Build dists
|
|
||||||
run: python -m tox
|
- name: Build and Check Package
|
||||||
|
uses: hynek/build-and-inspect-python-package@v1
|
||||||
|
|
||||||
|
- name: Download Package
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Packages
|
||||||
|
path: dist
|
||||||
|
|
||||||
- name: Publish to test.pypi.org
|
- name: Publish to test.pypi.org
|
||||||
if: >-
|
if: >-
|
||||||
(
|
(
|
||||||
|
@ -84,14 +95,15 @@ jobs:
|
||||||
github.event_name == 'create' &&
|
github.event_name == 'create' &&
|
||||||
github.event.ref_type == 'tag'
|
github.event.ref_type == 'tag'
|
||||||
)
|
)
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
password: ${{ secrets.testpypi_password }}
|
password: ${{ secrets.testpypi_password }}
|
||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to pypi.org
|
- name: Publish to pypi.org
|
||||||
if: >- # "create" workflows run separately from "push" & "pull_request"
|
if: >- # "create" workflows run separately from "push" & "pull_request"
|
||||||
github.event_name == 'create' &&
|
github.event_name == 'create' &&
|
||||||
github.event.ref_type == 'tag'
|
github.event.ref_type == 'tag'
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
password: ${{ secrets.pypi_password }}
|
password: ${{ secrets.pypi_password }}
|
||||||
|
|
|
@ -5,123 +5,133 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_python:
|
test_python:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.python-version }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- tox-env: "py37"
|
||||||
name: py37-ubuntu
|
python-version: "3.7"
|
||||||
python-version: 3.7
|
|
||||||
|
|
||||||
- os: windows-latest
|
# https://github.com/pytest-dev/pytest-html/issues/585
|
||||||
name: py37-windows
|
# - os: windows-latest
|
||||||
python-version: 3.7
|
# name: py37-windows
|
||||||
|
# python-version: 3.7
|
||||||
|
|
||||||
- os: macOS-latest
|
# https://github.com/pytest-dev/pytest-html/issues/585
|
||||||
name: py37-mac
|
# - os: macOS-latest
|
||||||
python-version: 3.7
|
# name: py37-mac
|
||||||
|
# python-version: 3.7
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- tox-env: "py38"
|
||||||
name: py38-ubuntu
|
python-version: "3.8"
|
||||||
python-version: 3.8
|
|
||||||
|
|
||||||
- os: windows-latest
|
# https://github.com/pytest-dev/pytest-html/issues/585
|
||||||
name: py38-windows
|
# - os: windows-latest
|
||||||
python-version: 3.8
|
# name: py38-windows
|
||||||
|
# python-version: 3.8
|
||||||
|
|
||||||
- os: macOS-latest
|
# https://github.com/pytest-dev/pytest-html/issues/585
|
||||||
name: py38-mac
|
# - os: macOS-latest
|
||||||
python-version: 3.8
|
# name: py38-mac
|
||||||
|
# python-version: 3.8
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- tox-env: "py39"
|
||||||
name: py39-ubuntu
|
python-version: "3.9"
|
||||||
python-version: 3.9
|
|
||||||
|
|
||||||
- os: windows-latest
|
- tox-env: "py310"
|
||||||
name: py39-windows
|
python-version: "3.10"
|
||||||
python-version: 3.9
|
|
||||||
|
|
||||||
- os: macOS-latest
|
# https://github.com/pytest-dev/pytest-html/issues/585
|
||||||
name: py39-mac
|
# - os: windows-latest
|
||||||
python-version: 3.9
|
# name: py39-windows
|
||||||
|
# python-version: 3.9
|
||||||
|
|
||||||
- os: ubuntu-latest
|
# https://github.com/pytest-dev/pytest-html/issues/585
|
||||||
name: py310-ubuntu
|
# - os: macOS-latest
|
||||||
python-version: '3.10'
|
# name: py39-mac
|
||||||
|
# python-version: 3.9
|
||||||
|
|
||||||
- os: windows-latest
|
- tox-env: "pypy3"
|
||||||
name: py310-windows
|
python-version: "pypy3.9"
|
||||||
python-version: '3.10'
|
skip-coverage: true
|
||||||
|
|
||||||
- os: macOS-latest
|
# https://github.com/pytest-dev/pytest-html/issues/585
|
||||||
name: py310-mac
|
# - os: windows-latest
|
||||||
python-version: '3.10'
|
# name: pypy3-windows
|
||||||
|
# python-version: pypy3
|
||||||
- os: ubuntu-latest
|
|
||||||
name: pypy3-ubuntu
|
|
||||||
python-version: pypy-3.8
|
|
||||||
|
|
||||||
- os: windows-latest
|
|
||||||
name: pypy3-windows
|
|
||||||
python-version: pypy-3.8
|
|
||||||
|
|
||||||
# https://github.com/pytest-dev/pytest-html/issues/482
|
# https://github.com/pytest-dev/pytest-html/issues/482
|
||||||
- os: macOS-latest
|
# - os: macOS-latest
|
||||||
name: pypy3-mac
|
# name: pypy3-mac
|
||||||
python-version: pypy-3.8
|
# python-version: pypy-3.8
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- tox-env: "devel"
|
||||||
name: devel-ubuntu
|
python-version: "3.11-dev"
|
||||||
python-version: 3.9
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Newline Behavior
|
- name: Set Newline Behavior
|
||||||
run : git config --global core.autocrlf false
|
run: git config --global core.autocrlf false
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Start chrome
|
||||||
|
run: ./start
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
node-version: '16.x'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
run: npm run build:ci
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix['python-version'] }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: python -m pip install --upgrade tox
|
run: python -m pip install --upgrade tox
|
||||||
- name: Get Tox Environment Name From Matrix Name
|
|
||||||
uses: rishabhgupta/split-by@v1
|
|
||||||
id: split-matrix-name
|
|
||||||
with:
|
|
||||||
string: '${{ matrix.name }}'
|
|
||||||
split-by: '-'
|
|
||||||
- name: Test with coverage
|
- name: Test with coverage
|
||||||
if: "! contains(matrix.name, 'pypy3')"
|
if: ${{ ! matrix.skip-coverage }}
|
||||||
run: python -m tox -e ${{ steps.split-matrix-name.outputs._0}}-cov
|
run: tox -e ${{ matrix.tox-env }}-cov
|
||||||
|
|
||||||
- name: Test without coverage
|
- name: Test without coverage
|
||||||
if: "contains(matrix.name, 'pypy3')"
|
if: ${{ matrix.skip-coverage }}
|
||||||
run: python -m tox -e ${{ steps.split-matrix-name.outputs._0}}
|
run: tox -e ${{ matrix.tox-env }}
|
||||||
|
|
||||||
# TODO: https://github.com/pytest-dev/pytest-html/issues/481
|
# TODO: https://github.com/pytest-dev/pytest-html/issues/481
|
||||||
# - name: Upload coverage to codecov
|
- name: Upload coverage to codecov
|
||||||
# if: github.event.schedule == ''
|
if: >-
|
||||||
# uses: codecov/codecov-action@v2
|
${{
|
||||||
# with:
|
! github.event.schedule &&
|
||||||
# fail_ci_if_error: true
|
! matrix.skip-coverage &&
|
||||||
# file: ./coverage.xml
|
github.repository_owner == 'pytest-dev'
|
||||||
# flags: tests
|
}}
|
||||||
# name: ${{ matrix.py }} - ${{ matrix.os }}
|
uses: codecov/codecov-action@v3
|
||||||
# verbose: true
|
with:
|
||||||
|
fail_ci_if_error: true
|
||||||
|
files: ./coverage.xml
|
||||||
|
flags: tests
|
||||||
|
name: ${{ matrix.tox-env }}
|
||||||
|
verbose: true
|
||||||
|
|
||||||
test_javascript:
|
test_javascript:
|
||||||
name: grunt
|
name: mocha
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '16.x'
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm ci
|
||||||
- name: QUnit Tests
|
- name: Mocha Tests
|
||||||
run: npm test
|
run: npm run unit
|
||||||
|
|
|
@ -35,7 +35,6 @@ local.properties
|
||||||
# JS files/folders
|
# JS files/folders
|
||||||
## node / npm
|
## node / npm
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# MacOS files
|
# MacOS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -51,3 +50,6 @@ Pipfile.lock
|
||||||
|
|
||||||
# sphinx/read the docs
|
# sphinx/read the docs
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
||||||
|
*.html
|
||||||
|
assets/
|
||||||
|
|
|
@ -3,12 +3,14 @@ repos:
|
||||||
rev: 22.3.0
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet, --line-length=88]
|
||||||
|
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
- repo: https://github.com/asottile/blacken-docs
|
||||||
rev: v1.12.1
|
rev: v1.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
additional_dependencies: [black==22.3.0]
|
additional_dependencies: [black==22.3.0]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.1.0
|
rev: v4.1.0
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -19,6 +21,7 @@ repos:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
language_version: python3
|
language_version: python3
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 4.0.1
|
rev: 4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -27,16 +30,19 @@ repos:
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-builtins==1.5.3
|
- flake8-builtins==1.5.3
|
||||||
- flake8-typing-imports==1.12.0
|
- flake8-typing-imports==1.12.0
|
||||||
|
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
rev: v3.0.1
|
rev: v3.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: ["--application-directories=.:src:testing", --py3-plus]
|
args: ["--application-directories=.:src:testing", --py3-plus]
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.32.0
|
rev: v2.32.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py3-plus]
|
args: [--py3-plus]
|
||||||
|
|
||||||
# - repo: https://github.com/pre-commit/mirrors-eslint
|
# - repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
# rev: v7.13.0
|
# rev: v7.13.0
|
||||||
# hooks:
|
# hooks:
|
||||||
|
@ -44,6 +50,7 @@ repos:
|
||||||
# additional_dependencies:
|
# additional_dependencies:
|
||||||
# - eslint@7.13.0
|
# - eslint@7.13.0
|
||||||
# args: [src]
|
# args: [src]
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: rst
|
- id: rst
|
||||||
|
@ -52,6 +59,7 @@ repos:
|
||||||
files: ^(README.rst)$
|
files: ^(README.rst)$
|
||||||
language: python
|
language: python
|
||||||
additional_dependencies: [pygments, restructuredtext_lint]
|
additional_dependencies: [pygments, restructuredtext_lint]
|
||||||
|
|
||||||
- repo: https://github.com/elidupuis/mirrors-sass-lint
|
- repo: https://github.com/elidupuis/mirrors-sass-lint
|
||||||
rev: "5cc45653263b423398e4af2561fae362903dd45d"
|
rev: "5cc45653263b423398e4af2561fae362903dd45d"
|
||||||
hooks:
|
hooks:
|
||||||
|
|
15
Pipfile
15
Pipfile
|
@ -1,15 +0,0 @@
|
||||||
[[source]]
|
|
||||||
name = "pypi"
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
pytest = "*"
|
|
||||||
tox = "*"
|
|
||||||
flake8 = "==4.0.1" # also bump this in .pre-commit-config.yaml
|
|
||||||
black = "==22.3.0" # also bump this in .pre-commit-config.yaml
|
|
||||||
pre-commit = "*"
|
|
||||||
pytest-rerunfailures = "*"
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
pytest-html = {editable = true,path = "."}
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
chrome:
|
||||||
|
image: seleniarm/standalone-chromium:110.0
|
||||||
|
container_name: chrome
|
||||||
|
shm_size: '2gb'
|
||||||
|
ports:
|
||||||
|
- "4444:4444"
|
||||||
|
- "7900:7900"
|
||||||
|
volumes:
|
||||||
|
- "%%VOLUME%%:Z"
|
|
@ -93,11 +93,11 @@ Once `npm`_ is installed, you can install all needed dependencies by running:
|
||||||
|
|
||||||
$ npm install
|
$ npm install
|
||||||
|
|
||||||
Run the following to generate the CSS:
|
Run the following to build the application:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ npm run build:css
|
$ npm run build
|
||||||
|
|
||||||
Releasing a new version
|
Releasing a new version
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
|
@ -1,13 +1,18 @@
|
||||||
{
|
{
|
||||||
"main": "Gruntfile.js",
|
|
||||||
"devDependencies": {
|
|
||||||
"grunt": "^1.3.0",
|
|
||||||
"grunt-cli": "^1.3.2",
|
|
||||||
"grunt-contrib-qunit": "^4.0.0",
|
|
||||||
"sass": "^1.29.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "grunt test",
|
"unit": "mocha testing/**/unittest.js",
|
||||||
"build:css": "sass --no-source-map --no-error-css src/layout/css/style.scss src/pytest_html/resources/style.css"
|
"build:ci": "npm run build:css && npm run build:jsapp",
|
||||||
|
"build:css": "sass --no-source-map --no-error-css src/layout/css/style.scss src/pytest_html/resources/style.css",
|
||||||
|
"build:jsapp": "browserify ./src/pytest_html/scripts/index.js > ./src/pytest_html/resources/app.js",
|
||||||
|
"build": "npm run unit && npm run build:css && npm run build:jsapp"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"browserify": "^17.0.0",
|
||||||
|
"chai": "^4.3.6",
|
||||||
|
"eslint": "^8.20.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"mocha": "^10.0.0",
|
||||||
|
"sass": "^1.52.3",
|
||||||
|
"sinon": "^14.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,766 @@
|
||||||
|
[[package]]
|
||||||
|
name = "assertpy"
|
||||||
|
version = "1.1"
|
||||||
|
description = "Simple assertion library for unit testing in python with a fluent API"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "22.2.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
|
||||||
|
dev = ["attrs[docs,tests]"]
|
||||||
|
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
|
||||||
|
tests = ["attrs[tests-no-zope]", "zope.interface"]
|
||||||
|
tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "23.1.0"
|
||||||
|
description = "The uncompromising code formatter."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
packaging = ">=22.0"
|
||||||
|
pathspec = ">=0.9.0"
|
||||||
|
platformdirs = ">=2"
|
||||||
|
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||||
|
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
|
||||||
|
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
d = ["aiohttp (>=3.7.4)"]
|
||||||
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.3.1"
|
||||||
|
description = "Validate configuration and produce human readable error messages."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.3"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.3.6"
|
||||||
|
description = "Distribution utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "Backport of PEP 654 (exception groups)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "execnet"
|
||||||
|
version = "1.9.0"
|
||||||
|
description = "execnet: rapid multi-Python deployment"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["pre-commit"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.9.0"
|
||||||
|
description = "A platform independent file lock."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
|
||||||
|
testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8"
|
||||||
|
version = "5.0.4"
|
||||||
|
description = "the modular source code checker: pep8 pyflakes and co"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""}
|
||||||
|
mccabe = ">=0.7.0,<0.8.0"
|
||||||
|
pycodestyle = ">=2.9.0,<2.10.0"
|
||||||
|
pyflakes = ">=2.5.0,<2.6.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.5.17"
|
||||||
|
description = "File identification library for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
license = ["ukkonen"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "4.2.0"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"]
|
||||||
|
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "brain-dead simple config-ini parsing"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.2"
|
||||||
|
description = "A very fast and expressive template engine."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
i18n = ["Babel (>=2.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "2.1.2"
|
||||||
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "McCabe checker, plugin for flake8"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.7.0"
|
||||||
|
description = "Node.js virtual environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
setuptools = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "23.0"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.11.0"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "2.6.2"
|
||||||
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "plugin and hook calling mechanisms for python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox"]
|
||||||
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "2.21.0"
|
||||||
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cfgv = ">=2.0.0"
|
||||||
|
identify = ">=1.0.0"
|
||||||
|
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
nodeenv = ">=0.11.1"
|
||||||
|
pyyaml = ">=5.1"
|
||||||
|
virtualenv = ">=20.10.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "py"
|
||||||
|
version = "1.11.0"
|
||||||
|
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycodestyle"
|
||||||
|
version = "2.9.1"
|
||||||
|
description = "Python style guide checker"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyflakes"
|
||||||
|
version = "2.5.0"
|
||||||
|
description = "passive checker of Python programs"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "7.2.1"
|
||||||
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
attrs = ">=19.2.0"
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
iniconfig = "*"
|
||||||
|
packaging = "*"
|
||||||
|
pluggy = ">=0.12,<2.0"
|
||||||
|
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-metadata"
|
||||||
|
version = "2.0.4"
|
||||||
|
description = "pytest plugin for test session metadata"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7,<4.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=3.0.0,<8.0.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-mock"
|
||||||
|
version = "3.10.0"
|
||||||
|
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=5.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-xdist"
|
||||||
|
version = "3.2.0"
|
||||||
|
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
execnet = ">=1.1"
|
||||||
|
pytest = ">=6.2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
psutil = ["psutil (>=3.0)"]
|
||||||
|
setproctitle = ["setproctitle"]
|
||||||
|
testing = ["filelock"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0"
|
||||||
|
description = "YAML parser and emitter for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "67.2.0"
|
||||||
|
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||||
|
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||||
|
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tox"
|
||||||
|
version = "3.28.0"
|
||||||
|
description = "tox is a generic virtualenv management and test command line tool"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""}
|
||||||
|
filelock = ">=3.0.0"
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
packaging = ">=14"
|
||||||
|
pluggy = ">=0.12.0"
|
||||||
|
py = ">=1.4.17"
|
||||||
|
six = ">=1.14.0"
|
||||||
|
tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}
|
||||||
|
virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
|
||||||
|
testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-ast"
|
||||||
|
version = "1.5.4"
|
||||||
|
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.4.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.16.2"
|
||||||
|
description = "Virtual Python Environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
distlib = ">=0.3.1,<1"
|
||||||
|
filelock = ">=3.2,<4"
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
platformdirs = ">=2,<3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
|
||||||
|
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.13.0"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||||
|
testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "1.1"
|
||||||
|
python-versions = ">=3.7, <4.0.0"
|
||||||
|
content-hash = "288796311da5b8708fdc236a6a1e57c26f710715e13a2ff487beb9266eca38ac"
|
||||||
|
|
||||||
|
[metadata.files]
|
||||||
|
assertpy = [
|
||||||
|
{file = "assertpy-1.1.tar.gz", hash = "sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833"},
|
||||||
|
]
|
||||||
|
attrs = [
|
||||||
|
{file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
|
||||||
|
{file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
|
||||||
|
]
|
||||||
|
black = [
|
||||||
|
{file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"},
|
||||||
|
{file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"},
|
||||||
|
{file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"},
|
||||||
|
{file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"},
|
||||||
|
{file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"},
|
||||||
|
{file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"},
|
||||||
|
{file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"},
|
||||||
|
{file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"},
|
||||||
|
{file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"},
|
||||||
|
{file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"},
|
||||||
|
{file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"},
|
||||||
|
{file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"},
|
||||||
|
{file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"},
|
||||||
|
{file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"},
|
||||||
|
{file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"},
|
||||||
|
{file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"},
|
||||||
|
{file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"},
|
||||||
|
{file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"},
|
||||||
|
{file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"},
|
||||||
|
{file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"},
|
||||||
|
{file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"},
|
||||||
|
{file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"},
|
||||||
|
{file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"},
|
||||||
|
{file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"},
|
||||||
|
{file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"},
|
||||||
|
]
|
||||||
|
cfgv = [
|
||||||
|
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
|
||||||
|
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
|
||||||
|
]
|
||||||
|
click = [
|
||||||
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
]
|
||||||
|
colorama = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
distlib = [
|
||||||
|
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
||||||
|
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
|
||||||
|
]
|
||||||
|
exceptiongroup = [
|
||||||
|
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
|
||||||
|
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
|
||||||
|
]
|
||||||
|
execnet = [
|
||||||
|
{file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
|
||||||
|
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
|
||||||
|
]
|
||||||
|
filelock = [
|
||||||
|
{file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
|
||||||
|
{file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
|
||||||
|
]
|
||||||
|
flake8 = [
|
||||||
|
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
|
||||||
|
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
|
||||||
|
]
|
||||||
|
identify = [
|
||||||
|
{file = "identify-2.5.17-py2.py3-none-any.whl", hash = "sha256:7d526dd1283555aafcc91539acc061d8f6f59adb0a7bba462735b0a318bff7ed"},
|
||||||
|
{file = "identify-2.5.17.tar.gz", hash = "sha256:93cc61a861052de9d4c541a7acb7e3dcc9c11b398a2144f6e52ae5285f5f4f06"},
|
||||||
|
]
|
||||||
|
importlib-metadata = [
|
||||||
|
{file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"},
|
||||||
|
{file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"},
|
||||||
|
]
|
||||||
|
iniconfig = [
|
||||||
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
|
]
|
||||||
|
jinja2 = [
|
||||||
|
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||||
|
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||||
|
]
|
||||||
|
markupsafe = [
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
|
||||||
|
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
||||||
|
]
|
||||||
|
mccabe = [
|
||||||
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
|
]
|
||||||
|
mypy-extensions = [
|
||||||
|
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||||
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
|
]
|
||||||
|
nodeenv = [
|
||||||
|
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
|
||||||
|
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
|
||||||
|
]
|
||||||
|
packaging = [
|
||||||
|
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
|
||||||
|
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
||||||
|
]
|
||||||
|
pathspec = [
|
||||||
|
{file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"},
|
||||||
|
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
|
||||||
|
]
|
||||||
|
platformdirs = [
|
||||||
|
{file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
|
||||||
|
{file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
|
||||||
|
]
|
||||||
|
pluggy = [
|
||||||
|
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
|
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||||
|
]
|
||||||
|
pre-commit = [
|
||||||
|
{file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"},
|
||||||
|
{file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"},
|
||||||
|
]
|
||||||
|
py = [
|
||||||
|
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||||
|
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||||
|
]
|
||||||
|
pycodestyle = [
|
||||||
|
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
|
||||||
|
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
|
||||||
|
]
|
||||||
|
pyflakes = [
|
||||||
|
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
|
||||||
|
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
|
||||||
|
]
|
||||||
|
pytest = [
|
||||||
|
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
|
||||||
|
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
|
||||||
|
]
|
||||||
|
pytest-metadata = [
|
||||||
|
{file = "pytest_metadata-2.0.4-py3-none-any.whl", hash = "sha256:acb739f89fabb3d798c099e9e0c035003062367a441910aaaf2281bc1972ee14"},
|
||||||
|
{file = "pytest_metadata-2.0.4.tar.gz", hash = "sha256:fcc653f65fe3035b478820b5284fbf0f52803622ee3f60a2faed7a7d3ba1f41e"},
|
||||||
|
]
|
||||||
|
pytest-mock = [
|
||||||
|
{file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"},
|
||||||
|
{file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"},
|
||||||
|
]
|
||||||
|
pytest-xdist = [
|
||||||
|
{file = "pytest-xdist-3.2.0.tar.gz", hash = "sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"},
|
||||||
|
{file = "pytest_xdist-3.2.0-py3-none-any.whl", hash = "sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68"},
|
||||||
|
]
|
||||||
|
pyyaml = [
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||||
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
|
]
|
||||||
|
setuptools = [
|
||||||
|
{file = "setuptools-67.2.0-py3-none-any.whl", hash = "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c"},
|
||||||
|
{file = "setuptools-67.2.0.tar.gz", hash = "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"},
|
||||||
|
]
|
||||||
|
six = [
|
||||||
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
|
]
|
||||||
|
tomli = [
|
||||||
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
|
]
|
||||||
|
tox = [
|
||||||
|
{file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"},
|
||||||
|
{file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"},
|
||||||
|
]
|
||||||
|
typed-ast = [
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
||||||
|
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||||
|
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||||
|
]
|
||||||
|
virtualenv = [
|
||||||
|
{file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"},
|
||||||
|
{file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"},
|
||||||
|
]
|
||||||
|
zipp = [
|
||||||
|
{file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"},
|
||||||
|
{file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"},
|
||||||
|
]
|
|
@ -1,12 +1,64 @@
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = ["poetry-core>=1.0.0"]
|
||||||
"pip >= 19.3.1",
|
build-backend = "poetry.core.masonry.api"
|
||||||
"setuptools >= 42",
|
|
||||||
"setuptools_scm[toml] >= 3.5.0",
|
[tool.poetry]
|
||||||
"setuptools_scm_git_archive >= 1.1",
|
name = "pytest-html"
|
||||||
"wheel >= 0.33.6",
|
description = "pytest plugin for generating HTML reports"
|
||||||
|
version = "4.0.0-rc0"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
authors = [
|
||||||
|
"Dave Hunt <dhunt@mozilla.com>",
|
||||||
|
"Jim Brannlund <jimbrannlund@fastmail.com>"
|
||||||
]
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
readme = "README.rst"
|
||||||
|
homepage = "https://github.com/pytest-dev/pytest-html"
|
||||||
|
repository = "https://github.com/pytest-dev/pytest-html"
|
||||||
|
keywords = [
|
||||||
|
"pytest",
|
||||||
|
"html",
|
||||||
|
"report",
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Framework :: Pytest",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Operating System :: POSIX",
|
||||||
|
"Operating System :: Microsoft :: Windows",
|
||||||
|
"Operating System :: MacOS :: MacOS X",
|
||||||
|
"Topic :: Software Development :: Quality Assurance",
|
||||||
|
"Topic :: Software Development :: Testing",
|
||||||
|
"Topic :: Utilities",
|
||||||
|
]
|
||||||
|
packages = [
|
||||||
|
{ include = "pytest_html", from = "src" },
|
||||||
|
]
|
||||||
|
include = [
|
||||||
|
{ path = "testing", format = "sdist" },
|
||||||
|
{ path = "docs", format = "sdist" },
|
||||||
|
"src/pytest_html/resources",
|
||||||
|
"src/pytest_html/resources/app.js",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = ">=3.7"
|
||||||
|
pytest = ">=7.0.0"
|
||||||
|
pytest-metadata = ">=2.0.2"
|
||||||
|
Jinja2 = ">=3.0.0"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
assertpy = ">=1.1"
|
||||||
|
beautifulsoup4 = ">=4.11.1"
|
||||||
|
black = ">=22.1.0"
|
||||||
|
flake8 = ">=4.0.1"
|
||||||
|
pre-commit = ">=2.17.0"
|
||||||
|
pytest-xdist = ">=2.4.0"
|
||||||
|
pytest-mock = ">=3.7.0"
|
||||||
|
selenium = ">=4.3.0"
|
||||||
|
tox = ">=3.24.5"
|
||||||
|
|
||||||
|
[tool.poetry.plugins.pytest11]
|
||||||
|
html = "pytest_html.plugin"
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
local_scheme = "no-local-version"
|
local_scheme = "no-local-version"
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
const jsonData = {"title": "Next Gen Report", "collectedItems": 89, "environment": {"Python": "3.7.7", "Platform": "Darwin-19.6.0-x86_64-i386-64bit", "Packages": {"pytest": "6.1.2", "py": "1.10.0", "pluggy": "0.13.1"}, "Plugins": {"rerunfailures": "9.1.1", "metadata": "1.11.0", "xdist": "2.1.0", "mock": "3.3.1", "html": "2.1.2.dev80", "forked": "1.3.0"}}, "tests": []}
|
|
4
setup.py
4
setup.py
|
@ -10,10 +10,10 @@ setup(
|
||||||
url="https://github.com/pytest-dev/pytest-html",
|
url="https://github.com/pytest-dev/pytest-html",
|
||||||
package_dir={"": "src"},
|
package_dir={"": "src"},
|
||||||
packages=["pytest_html"],
|
packages=["pytest_html"],
|
||||||
package_data={"pytest_html": ["resources/*"]},
|
package_data={"pytest_html": ["resources/*", "scripts/*"]},
|
||||||
entry_points={"pytest11": ["html = pytest_html.plugin"]},
|
entry_points={"pytest11": ["html = pytest_html.plugin"]},
|
||||||
setup_requires=["setuptools_scm"],
|
setup_requires=["setuptools_scm"],
|
||||||
install_requires=["pytest>=5.0,!=6.0.0", "pytest-metadata"],
|
install_requires=["pytest>=5.0,!=6.0.0", "pytest-metadata", "jinja2>=3.0,<4.0"],
|
||||||
license="Mozilla Public License 2.0 (MPL 2.0)",
|
license="Mozilla Public License 2.0 (MPL 2.0)",
|
||||||
keywords="py.test pytest html report",
|
keywords="py.test pytest html report",
|
||||||
python_requires=">=3.6",
|
python_requires=">=3.6",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
|
||||||
# file generated by setuptools_scm. don't track in version control
|
# file generated by setuptools_scm. don't track in version control
|
||||||
pytest_html/__version.py
|
pytest_html/__version.py
|
||||||
|
|
||||||
|
# don't track built file
|
||||||
|
app.js
|
||||||
|
|
|
@ -82,9 +82,6 @@ span.xpassed,
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-result {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
.col-links__extra {
|
.col-links__extra {
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
|
@ -143,50 +140,74 @@ $extra-media-width: 320px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.image {
|
div.media {
|
||||||
border: $border-width solid #e6e6e6;
|
border: $border-width solid #e6e6e6;
|
||||||
float: right;
|
float: right;
|
||||||
height: $extra-height;
|
height: $extra-height;
|
||||||
margin-left: $spacing;
|
margin: 0 $spacing;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: $extra-media-width;
|
width: $extra-media-width;
|
||||||
|
|
||||||
img {
|
|
||||||
width: $extra-media-width;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.video {
|
.media-container {
|
||||||
border: $border-width solid #e6e6e6;
|
display: grid;
|
||||||
float: right;
|
grid-template-columns: 25px auto 25px;
|
||||||
height: $extra-height;
|
align-items: center;
|
||||||
margin-left: $spacing;
|
flex: 1 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: $extra-media-width;
|
height: 200px;
|
||||||
|
}
|
||||||
|
.media-container__nav--right,
|
||||||
|
.media-container__nav--left {
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-container__viewport {
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
height: inherit;
|
||||||
|
img,
|
||||||
video {
|
video {
|
||||||
overflow: hidden;
|
object-fit: cover;
|
||||||
width: $extra-media-width;
|
width: 100%;
|
||||||
height: $extra-height;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media__name,
|
||||||
|
.media__counter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex: 0 0 25px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.collapsed {
|
.collapsed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expander::after {
|
@mixin rowToggle {
|
||||||
content: ' (show details)';
|
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapser::after {
|
.col-result {
|
||||||
content: ' (hide details)';
|
|
||||||
color: #bbb;
|
|
||||||
font-style: italic;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
&:hover::after {
|
||||||
|
@include rowToggle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.col-result.collapser {
|
||||||
|
&:hover::after {
|
||||||
|
content: ' (hide details)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.col-result.expander {
|
||||||
|
&:hover::after {
|
||||||
|
content: ' (show details)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*------------------
|
/*------------------
|
||||||
|
@ -249,7 +270,6 @@ div.video {
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
display: flex;
|
|
||||||
&__data {
|
&__data {
|
||||||
flex: 0 0 550px;
|
flex: 0 0 550px;
|
||||||
}
|
}
|
||||||
|
@ -280,7 +300,25 @@ div.video {
|
||||||
flex: 0 0 550px;
|
flex: 0 0 550px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.controls {
|
||||||
input.filter {
|
display: flex;
|
||||||
margin-left: 10px;
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.filters,
|
||||||
|
.collapse {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
button {
|
||||||
|
color: #999;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
&:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter__label {
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,342 +0,0 @@
|
||||||
import bisect
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from py.xml import html
|
|
||||||
from py.xml import raw
|
|
||||||
|
|
||||||
from . import __pypi_url__
|
|
||||||
from . import __version__
|
|
||||||
from .outcome import Outcome
|
|
||||||
from .result import TestResult
|
|
||||||
from .util import ansi_support
|
|
||||||
|
|
||||||
|
|
||||||
class HTMLReport:
|
|
||||||
def __init__(self, logfile, config):
|
|
||||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
|
||||||
self.logfile = os.path.abspath(logfile)
|
|
||||||
self.test_logs = []
|
|
||||||
self.title = os.path.basename(self.logfile)
|
|
||||||
self.results = []
|
|
||||||
self.errors = self.failed = 0
|
|
||||||
self.passed = self.skipped = 0
|
|
||||||
self.xfailed = self.xpassed = 0
|
|
||||||
has_rerun = config.pluginmanager.hasplugin("rerunfailures")
|
|
||||||
self.rerun = 0 if has_rerun else None
|
|
||||||
self.self_contained = config.getoption("self_contained_html")
|
|
||||||
self.config = config
|
|
||||||
self.reports = defaultdict(list)
|
|
||||||
|
|
||||||
def _appendrow(self, outcome, report):
|
|
||||||
result = TestResult(outcome, report, self.logfile, self.config)
|
|
||||||
if result.row_table is not None:
|
|
||||||
index = bisect.bisect_right(self.results, result)
|
|
||||||
self.results.insert(index, result)
|
|
||||||
tbody = html.tbody(
|
|
||||||
result.row_table,
|
|
||||||
class_="{} results-table-row".format(result.outcome.lower()),
|
|
||||||
)
|
|
||||||
if result.row_extra is not None:
|
|
||||||
tbody.append(result.row_extra)
|
|
||||||
self.test_logs.insert(index, tbody)
|
|
||||||
|
|
||||||
def append_passed(self, report):
|
|
||||||
if report.when == "call":
|
|
||||||
if hasattr(report, "wasxfail"):
|
|
||||||
self.xpassed += 1
|
|
||||||
self._appendrow("XPassed", report)
|
|
||||||
else:
|
|
||||||
self.passed += 1
|
|
||||||
self._appendrow("Passed", report)
|
|
||||||
|
|
||||||
def append_failed(self, report):
|
|
||||||
if getattr(report, "when", None) == "call":
|
|
||||||
if hasattr(report, "wasxfail"):
|
|
||||||
# pytest < 3.0 marked xpasses as failures
|
|
||||||
self.xpassed += 1
|
|
||||||
self._appendrow("XPassed", report)
|
|
||||||
else:
|
|
||||||
self.failed += 1
|
|
||||||
self._appendrow("Failed", report)
|
|
||||||
else:
|
|
||||||
self.errors += 1
|
|
||||||
self._appendrow("Error", report)
|
|
||||||
|
|
||||||
def append_rerun(self, report):
|
|
||||||
self.rerun += 1
|
|
||||||
self._appendrow("Rerun", report)
|
|
||||||
|
|
||||||
def append_skipped(self, report):
|
|
||||||
if hasattr(report, "wasxfail"):
|
|
||||||
self.xfailed += 1
|
|
||||||
self._appendrow("XFailed", report)
|
|
||||||
else:
|
|
||||||
self.skipped += 1
|
|
||||||
self._appendrow("Skipped", report)
|
|
||||||
|
|
||||||
def _generate_report(self, session):
|
|
||||||
suite_stop_time = time.time()
|
|
||||||
suite_time_delta = suite_stop_time - self.suite_start_time
|
|
||||||
numtests = self.passed + self.failed + self.xpassed + self.xfailed
|
|
||||||
generated = datetime.datetime.now()
|
|
||||||
|
|
||||||
with open(
|
|
||||||
os.path.join(os.path.dirname(__file__), "resources", "style.css")
|
|
||||||
) as style_css_fp:
|
|
||||||
self.style_css = style_css_fp.read()
|
|
||||||
|
|
||||||
if ansi_support():
|
|
||||||
ansi_css = [
|
|
||||||
"\n/******************************",
|
|
||||||
" * ANSI2HTML STYLES",
|
|
||||||
" ******************************/\n",
|
|
||||||
]
|
|
||||||
ansi_css.extend([str(r) for r in ansi_support().style.get_styles()])
|
|
||||||
self.style_css += "\n".join(ansi_css)
|
|
||||||
|
|
||||||
# <DF> Add user-provided CSS
|
|
||||||
for path in self.config.getoption("css"):
|
|
||||||
self.style_css += "\n/******************************"
|
|
||||||
self.style_css += "\n * CUSTOM CSS"
|
|
||||||
self.style_css += f"\n * {path}"
|
|
||||||
self.style_css += "\n ******************************/\n\n"
|
|
||||||
with open(path) as f:
|
|
||||||
self.style_css += f.read()
|
|
||||||
|
|
||||||
css_href = "assets/style.css"
|
|
||||||
html_css = html.link(href=css_href, rel="stylesheet", type="text/css")
|
|
||||||
if self.self_contained:
|
|
||||||
html_css = html.style(raw(self.style_css))
|
|
||||||
|
|
||||||
session.config.hook.pytest_html_report_title(report=self)
|
|
||||||
|
|
||||||
head = html.head(html.meta(charset="utf-8"), html.title(self.title), html_css)
|
|
||||||
|
|
||||||
outcomes = [
|
|
||||||
Outcome("passed", self.passed),
|
|
||||||
Outcome("skipped", self.skipped),
|
|
||||||
Outcome("failed", self.failed),
|
|
||||||
Outcome("error", self.errors, label="errors"),
|
|
||||||
Outcome("xfailed", self.xfailed, label="expected failures"),
|
|
||||||
Outcome("xpassed", self.xpassed, label="unexpected passes"),
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.rerun is not None:
|
|
||||||
outcomes.append(Outcome("rerun", self.rerun))
|
|
||||||
|
|
||||||
summary = [
|
|
||||||
html.p(f"{numtests} tests ran in {suite_time_delta:.2f} seconds. "),
|
|
||||||
html.p(
|
|
||||||
"(Un)check the boxes to filter the results.",
|
|
||||||
class_="filter",
|
|
||||||
hidden="true",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, outcome in enumerate(outcomes, start=1):
|
|
||||||
summary.append(outcome.checkbox)
|
|
||||||
summary.append(outcome.summary_item)
|
|
||||||
if i < len(outcomes):
|
|
||||||
summary.append(", ")
|
|
||||||
|
|
||||||
cells = [
|
|
||||||
html.th("Result", class_="sortable result initial-sort", col="result"),
|
|
||||||
html.th("Test", class_="sortable", col="name"),
|
|
||||||
html.th("Duration", class_="sortable", col="duration"),
|
|
||||||
html.th("Links", class_="sortable links", col="links"),
|
|
||||||
]
|
|
||||||
session.config.hook.pytest_html_results_table_header(cells=cells)
|
|
||||||
|
|
||||||
results = [
|
|
||||||
html.h2("Results"),
|
|
||||||
html.table(
|
|
||||||
[
|
|
||||||
html.thead(
|
|
||||||
html.tr(cells),
|
|
||||||
html.tr(
|
|
||||||
[
|
|
||||||
html.th(
|
|
||||||
"No results found. Try to check the filters",
|
|
||||||
colspan=len(cells),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
id="not-found-message",
|
|
||||||
hidden="true",
|
|
||||||
),
|
|
||||||
id="results-table-head",
|
|
||||||
),
|
|
||||||
self.test_logs,
|
|
||||||
],
|
|
||||||
id="results-table",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
with open(
|
|
||||||
os.path.join(os.path.dirname(__file__), "resources", "old_main.js")
|
|
||||||
) as main_js_fp:
|
|
||||||
main_js = main_js_fp.read()
|
|
||||||
|
|
||||||
body = html.body(
|
|
||||||
html.script(raw(main_js)),
|
|
||||||
html.h1(self.title),
|
|
||||||
html.p(
|
|
||||||
"Report generated on {} at {} by ".format(
|
|
||||||
generated.strftime("%d-%b-%Y"), generated.strftime("%H:%M:%S")
|
|
||||||
),
|
|
||||||
html.a("pytest-html", href=__pypi_url__),
|
|
||||||
f" v{__version__}",
|
|
||||||
),
|
|
||||||
onLoad="init()",
|
|
||||||
)
|
|
||||||
|
|
||||||
body.extend(self._generate_environment(session.config))
|
|
||||||
|
|
||||||
summary_prefix, summary_postfix = [], []
|
|
||||||
session.config.hook.pytest_html_results_summary(
|
|
||||||
prefix=summary_prefix, summary=summary, postfix=summary_postfix
|
|
||||||
)
|
|
||||||
body.extend([html.h2("Summary")] + summary_prefix + summary + summary_postfix)
|
|
||||||
|
|
||||||
body.extend(results)
|
|
||||||
|
|
||||||
doc = html.html(head, body)
|
|
||||||
|
|
||||||
unicode_doc = "<!DOCTYPE html>\n{}".format(doc.unicode(indent=2))
|
|
||||||
|
|
||||||
# Fix encoding issues, e.g. with surrogates
|
|
||||||
unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace")
|
|
||||||
return unicode_doc.decode("utf-8")
|
|
||||||
|
|
||||||
def _generate_environment(self, config):
|
|
||||||
if not hasattr(config, "_metadata") or config._metadata is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
metadata = config._metadata
|
|
||||||
environment = [html.h2("Environment")]
|
|
||||||
rows = []
|
|
||||||
|
|
||||||
keys = [k for k in metadata.keys()]
|
|
||||||
if not isinstance(metadata, OrderedDict):
|
|
||||||
keys.sort()
|
|
||||||
|
|
||||||
for key in keys:
|
|
||||||
value = metadata[key]
|
|
||||||
if self._is_redactable_environment_variable(key, config):
|
|
||||||
black_box_ascii_value = 0x2593
|
|
||||||
value = "".join(chr(black_box_ascii_value) for char in str(value))
|
|
||||||
|
|
||||||
if isinstance(value, str) and value.startswith("http"):
|
|
||||||
value = html.a(value, href=value, target="_blank")
|
|
||||||
elif isinstance(value, (list, tuple, set)):
|
|
||||||
value = ", ".join(str(i) for i in sorted(map(str, value)))
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
sorted_dict = {k: value[k] for k in sorted(value)}
|
|
||||||
value = json.dumps(sorted_dict)
|
|
||||||
raw_value_string = raw(str(value))
|
|
||||||
rows.append(html.tr(html.td(key), html.td(raw_value_string)))
|
|
||||||
|
|
||||||
environment.append(html.table(rows, id="environment"))
|
|
||||||
return environment
|
|
||||||
|
|
||||||
def _is_redactable_environment_variable(self, environment_variable, config):
|
|
||||||
redactable_regexes = config.getini("environment_table_redact_list")
|
|
||||||
for redactable_regex in redactable_regexes:
|
|
||||||
if re.match(redactable_regex, environment_variable):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _save_report(self, report_content):
|
|
||||||
dir_name = os.path.dirname(self.logfile)
|
|
||||||
assets_dir = os.path.join(dir_name, "assets")
|
|
||||||
|
|
||||||
os.makedirs(dir_name, exist_ok=True)
|
|
||||||
if not self.self_contained:
|
|
||||||
os.makedirs(assets_dir, exist_ok=True)
|
|
||||||
|
|
||||||
with open(self.logfile, "w", encoding="utf-8") as f:
|
|
||||||
f.write(report_content)
|
|
||||||
if not self.self_contained:
|
|
||||||
style_path = os.path.join(assets_dir, "style.css")
|
|
||||||
with open(style_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(self.style_css)
|
|
||||||
|
|
||||||
def _post_process_reports(self):
|
|
||||||
for test_name, test_reports in self.reports.items():
|
|
||||||
report_outcome = "passed"
|
|
||||||
wasxfail = False
|
|
||||||
failure_when = None
|
|
||||||
full_text = ""
|
|
||||||
extras = []
|
|
||||||
duration = 0.0
|
|
||||||
|
|
||||||
# in theory the last one should have all logs so we just go
|
|
||||||
# through them all to figure out the outcome, xfail, duration,
|
|
||||||
# extras, and when it swapped from pass
|
|
||||||
for test_report in test_reports:
|
|
||||||
if test_report.outcome == "rerun":
|
|
||||||
# reruns are separate test runs for all intensive purposes
|
|
||||||
self.append_rerun(test_report)
|
|
||||||
else:
|
|
||||||
full_text += test_report.longreprtext
|
|
||||||
extras.extend(getattr(test_report, "extra", []))
|
|
||||||
duration += getattr(test_report, "duration", 0.0)
|
|
||||||
|
|
||||||
if (
|
|
||||||
test_report.outcome not in ("passed", "rerun")
|
|
||||||
and report_outcome == "passed"
|
|
||||||
):
|
|
||||||
report_outcome = test_report.outcome
|
|
||||||
failure_when = test_report.when
|
|
||||||
|
|
||||||
if hasattr(test_report, "wasxfail"):
|
|
||||||
wasxfail = True
|
|
||||||
|
|
||||||
# the following test_report.<X> = settings come at the end of us
|
|
||||||
# looping through all test_reports that make up a single
|
|
||||||
# case.
|
|
||||||
|
|
||||||
# outcome on the right comes from the outcome of the various
|
|
||||||
# test_reports that make up this test case
|
|
||||||
# we are just carrying it over to the final report.
|
|
||||||
test_report.outcome = report_outcome
|
|
||||||
test_report.when = "call"
|
|
||||||
test_report.nodeid = test_name
|
|
||||||
test_report.longrepr = full_text
|
|
||||||
test_report.extra = extras
|
|
||||||
test_report.duration = duration
|
|
||||||
|
|
||||||
if wasxfail:
|
|
||||||
test_report.wasxfail = True
|
|
||||||
|
|
||||||
if test_report.outcome == "passed":
|
|
||||||
self.append_passed(test_report)
|
|
||||||
elif test_report.outcome == "skipped":
|
|
||||||
self.append_skipped(test_report)
|
|
||||||
elif test_report.outcome == "failed":
|
|
||||||
test_report.when = failure_when
|
|
||||||
self.append_failed(test_report)
|
|
||||||
|
|
||||||
def pytest_runtest_logreport(self, report):
|
|
||||||
self.reports[report.nodeid].append(report)
|
|
||||||
|
|
||||||
def pytest_collectreport(self, report):
|
|
||||||
if report.failed:
|
|
||||||
self.append_failed(report)
|
|
||||||
|
|
||||||
def pytest_sessionstart(self, session):
|
|
||||||
self.suite_start_time = time.time()
|
|
||||||
|
|
||||||
def pytest_sessionfinish(self, session):
|
|
||||||
self._post_process_reports()
|
|
||||||
report_content = self._generate_report(session)
|
|
||||||
self._save_report(report_content)
|
|
||||||
|
|
||||||
def pytest_terminal_summary(self, terminalreporter):
|
|
||||||
terminalreporter.write_sep("-", f"generated html file: file://{self.logfile}")
|
|
|
@ -1,49 +1,241 @@
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
import os
|
||||||
from typing import Dict
|
import re
|
||||||
|
import warnings
|
||||||
|
from collections import defaultdict
|
||||||
|
from functools import partial
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from jinja2 import Environment
|
||||||
|
from jinja2 import FileSystemLoader
|
||||||
|
from jinja2 import select_autoescape
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
from . import extras
|
||||||
|
from .util import cleanup_unserializable
|
||||||
|
|
||||||
|
|
||||||
class NextGenReport:
|
try:
|
||||||
def __init__(self, config, data_file):
|
from ansi2html import Ansi2HTMLConverter, style
|
||||||
self._config = config
|
|
||||||
self._data_file = data_file
|
|
||||||
|
|
||||||
self._title = "Next Gen Report"
|
converter = Ansi2HTMLConverter(inline=False, escaped=False)
|
||||||
|
_handle_ansi = partial(converter.convert, full=False)
|
||||||
|
_ansi_styles = style.get_styles()
|
||||||
|
except ImportError:
|
||||||
|
from _pytest.logging import _remove_ansi_escape_sequences
|
||||||
|
|
||||||
|
_handle_ansi = _remove_ansi_escape_sequences
|
||||||
|
_ansi_styles = []
|
||||||
|
|
||||||
|
|
||||||
|
class BaseReport:
|
||||||
|
class Cells:
|
||||||
|
def __init__(self):
|
||||||
|
self._html = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
return self._html
|
||||||
|
|
||||||
|
def insert(self, index, html):
|
||||||
|
self._html[index] = html
|
||||||
|
|
||||||
|
class Report:
|
||||||
|
def __init__(self, title, duration_format):
|
||||||
self._data = {
|
self._data = {
|
||||||
"title": self._title,
|
"title": title,
|
||||||
"collectedItems": 0,
|
"collectedItems": 0,
|
||||||
|
"runningState": "not_started",
|
||||||
|
"durationFormat": duration_format,
|
||||||
"environment": {},
|
"environment": {},
|
||||||
"tests": [],
|
"tests": [],
|
||||||
|
"resultsTableHeader": {},
|
||||||
|
"additionalSummary": defaultdict(list),
|
||||||
}
|
}
|
||||||
|
|
||||||
self._data_file.parent.mkdir(parents=True, exist_ok=True)
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
def _write(self):
|
def set_data(self, key, value):
|
||||||
try:
|
self._data[key] = value
|
||||||
data = json.dumps(self._data)
|
|
||||||
except TypeError:
|
|
||||||
data = cleanup_unserializable(self._data)
|
|
||||||
data = json.dumps(data)
|
|
||||||
|
|
||||||
with self._data_file.open("w", buffering=1, encoding="UTF-8") as f:
|
@property
|
||||||
f.write("const jsonData = ")
|
def title(self):
|
||||||
f.write(data)
|
return self._data["title"]
|
||||||
f.write("\n")
|
|
||||||
|
@title.setter
|
||||||
|
def title(self, title):
|
||||||
|
self._data["title"] = title
|
||||||
|
|
||||||
|
def __init__(self, report_path, config, default_css="style.css"):
|
||||||
|
self._report_path = Path(os.path.expandvars(report_path)).expanduser()
|
||||||
|
self._report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._resources_path = Path(__file__).parent.joinpath("resources")
|
||||||
|
self._config = config
|
||||||
|
self._template = _read_template([self._resources_path])
|
||||||
|
self._css = _process_css(
|
||||||
|
Path(self._resources_path, default_css), self._config.getoption("css")
|
||||||
|
)
|
||||||
|
self._duration_format = config.getini("duration_format")
|
||||||
|
self._max_asset_filename_length = int(
|
||||||
|
config.getini("max_asset_filename_length")
|
||||||
|
)
|
||||||
|
self._report = self.Report(self._report_path.name, self._duration_format)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css(self):
|
||||||
|
# implement in subclasses
|
||||||
|
return
|
||||||
|
|
||||||
|
def _asset_filename(self, test_id, extra_index, test_index, file_extension):
|
||||||
|
return "{}_{}_{}.{}".format(
|
||||||
|
re.sub(r"[^\w.]", "_", test_id),
|
||||||
|
str(extra_index),
|
||||||
|
str(test_index),
|
||||||
|
file_extension,
|
||||||
|
)[-self._max_asset_filename_length :]
|
||||||
|
|
||||||
|
def _generate_report(self, self_contained=False):
|
||||||
|
generated = datetime.datetime.now()
|
||||||
|
rendered_report = self._render_html(
|
||||||
|
generated.strftime("%d-%b-%Y"),
|
||||||
|
generated.strftime("%H:%M:%S"),
|
||||||
|
__version__,
|
||||||
|
self.css,
|
||||||
|
self_contained=self_contained,
|
||||||
|
test_data=cleanup_unserializable(self._report.data),
|
||||||
|
prefix=self._report.data["additionalSummary"]["prefix"],
|
||||||
|
summary=self._report.data["additionalSummary"]["summary"],
|
||||||
|
postfix=self._report.data["additionalSummary"]["postfix"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self._write_report(rendered_report)
|
||||||
|
|
||||||
|
def _generate_environment(self):
|
||||||
|
metadata = self._config._metadata
|
||||||
|
for key in metadata.keys():
|
||||||
|
value = metadata[key]
|
||||||
|
if self._is_redactable_environment_variable(key):
|
||||||
|
black_box_ascii_value = 0x2593
|
||||||
|
metadata[key] = "".join(chr(black_box_ascii_value) for _ in str(value))
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def _is_redactable_environment_variable(self, environment_variable):
|
||||||
|
redactable_regexes = self._config.getini("environment_table_redact_list")
|
||||||
|
for redactable_regex in redactable_regexes:
|
||||||
|
if re.match(redactable_regex, environment_variable):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _data_content(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _media_content(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _process_extras(self, report, test_id):
|
||||||
|
test_index = hasattr(report, "rerun") and report.rerun + 1 or 0
|
||||||
|
report_extras = getattr(report, "extras", [])
|
||||||
|
for extra_index, extra in enumerate(report_extras):
|
||||||
|
content = extra["content"]
|
||||||
|
asset_name = self._asset_filename(
|
||||||
|
test_id.encode("utf-8").decode("unicode_escape"),
|
||||||
|
extra_index,
|
||||||
|
test_index,
|
||||||
|
extra["extension"],
|
||||||
|
)
|
||||||
|
if extra["format_type"] == extras.FORMAT_JSON:
|
||||||
|
content = json.dumps(content)
|
||||||
|
extra["content"] = self._data_content(
|
||||||
|
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if extra["format_type"] == extras.FORMAT_TEXT:
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
content = content.decode("utf-8")
|
||||||
|
extra["content"] = self._data_content(
|
||||||
|
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
extra["format_type"] == extras.FORMAT_IMAGE
|
||||||
|
or extra["format_type"] == extras.FORMAT_VIDEO
|
||||||
|
):
|
||||||
|
extra["content"] = self._media_content(
|
||||||
|
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return report_extras
|
||||||
|
|
||||||
|
def _render_html(
|
||||||
|
self,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
version,
|
||||||
|
styles,
|
||||||
|
self_contained,
|
||||||
|
test_data,
|
||||||
|
summary,
|
||||||
|
prefix,
|
||||||
|
postfix,
|
||||||
|
):
|
||||||
|
return self._template.render(
|
||||||
|
date=date,
|
||||||
|
time=time,
|
||||||
|
version=version,
|
||||||
|
styles=styles,
|
||||||
|
self_contained=self_contained,
|
||||||
|
test_data=json.dumps(test_data),
|
||||||
|
summary=summary,
|
||||||
|
prefix=prefix,
|
||||||
|
postfix=postfix,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_report(self, rendered_report):
|
||||||
|
with self._report_path.open("w", encoding="utf-8") as f:
|
||||||
|
f.write(rendered_report)
|
||||||
|
|
||||||
@pytest.hookimpl(trylast=True)
|
@pytest.hookimpl(trylast=True)
|
||||||
def pytest_sessionstart(self, session):
|
def pytest_sessionstart(self, session):
|
||||||
config = session.config
|
config = session.config
|
||||||
if hasattr(config, "_metadata") and config._metadata:
|
if hasattr(config, "_metadata") and config._metadata:
|
||||||
metadata = config._metadata
|
self._report.data["environment"] = self._generate_environment()
|
||||||
self._data["environment"] = metadata
|
|
||||||
self._write()
|
session.config.hook.pytest_html_report_title(report=self._report)
|
||||||
|
|
||||||
|
header_cells = self.Cells()
|
||||||
|
session.config.hook.pytest_html_results_table_header(cells=header_cells)
|
||||||
|
self._report.set_data("resultsTableHeader", header_cells.html)
|
||||||
|
|
||||||
|
self._report.data["runningState"] = "Started"
|
||||||
|
self._generate_report()
|
||||||
|
|
||||||
|
@pytest.hookimpl(trylast=True)
|
||||||
|
def pytest_sessionfinish(self, session):
|
||||||
|
session.config.hook.pytest_html_results_summary(
|
||||||
|
prefix=self._report.data["additionalSummary"]["prefix"],
|
||||||
|
summary=self._report.data["additionalSummary"]["summary"],
|
||||||
|
postfix=self._report.data["additionalSummary"]["postfix"],
|
||||||
|
)
|
||||||
|
self._report.data["runningState"] = "Finished"
|
||||||
|
self._generate_report()
|
||||||
|
|
||||||
|
@pytest.hookimpl(trylast=True)
|
||||||
|
def pytest_terminal_summary(self, terminalreporter):
|
||||||
|
terminalreporter.write_sep(
|
||||||
|
"-", f"Generated html report: file://{self._report_path.resolve()}"
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.hookimpl(trylast=True)
|
@pytest.hookimpl(trylast=True)
|
||||||
def pytest_collection_finish(self, session):
|
def pytest_collection_finish(self, session):
|
||||||
self._data["collectedItems"] = len(session.items)
|
self._report.data["collectedItems"] = len(session.items)
|
||||||
self._write()
|
|
||||||
|
|
||||||
@pytest.hookimpl(trylast=True)
|
@pytest.hookimpl(trylast=True)
|
||||||
def pytest_runtest_logreport(self, report):
|
def pytest_runtest_logreport(self, report):
|
||||||
|
@ -51,24 +243,138 @@ class NextGenReport:
|
||||||
config=self._config, report=report
|
config=self._config, report=report
|
||||||
)
|
)
|
||||||
|
|
||||||
# rename to "extras" since list
|
test_id = report.nodeid
|
||||||
if hasattr(report, "extra"):
|
if report.when != "call":
|
||||||
for extra in report.extra:
|
test_id += f"::{report.when}"
|
||||||
print(extra)
|
data["nodeid"] = test_id
|
||||||
if extra["mime_type"] is not None and "image" in extra["mime_type"]:
|
|
||||||
data.update({"extras": extra})
|
|
||||||
|
|
||||||
self._data["tests"].append(data)
|
# Order here matters!
|
||||||
self._write()
|
log = report.longreprtext or report.capstdout or "No log output captured."
|
||||||
|
data["longreprtext"] = _handle_ansi(log)
|
||||||
|
|
||||||
|
data["outcome"] = _process_outcome(report)
|
||||||
|
|
||||||
|
row_cells = self.Cells()
|
||||||
|
self._config.hook.pytest_html_results_table_row(report=report, cells=row_cells)
|
||||||
|
data.update({"resultsTableRow": row_cells.html})
|
||||||
|
|
||||||
|
table_html = []
|
||||||
|
self._config.hook.pytest_html_results_table_html(report=report, data=table_html)
|
||||||
|
data.update({"tableHtml": table_html})
|
||||||
|
|
||||||
|
data.update({"extras": self._process_extras(report, test_id)})
|
||||||
|
self._report.data["tests"].append(data)
|
||||||
|
self._generate_report()
|
||||||
|
|
||||||
|
|
||||||
def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]:
|
class NextGenReport(BaseReport):
|
||||||
"""Return new dict with entries that are not json serializable by their str()."""
|
def __init__(self, report_path, config):
|
||||||
result = {}
|
super().__init__(report_path, config)
|
||||||
for k, v in d.items():
|
self._assets_path = Path(self._report_path.parent, "assets")
|
||||||
|
self._assets_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._css_path = Path(self._assets_path, "style.css")
|
||||||
|
|
||||||
|
with self._css_path.open("w", encoding="utf-8") as f:
|
||||||
|
f.write(self._css)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css(self):
|
||||||
|
# print("woot", Path(self._assets_path.name, "style.css"))
|
||||||
|
# print("waat", self._css_path.relative_to(self._report_path.parent))
|
||||||
|
return Path(self._assets_path.name, "style.css")
|
||||||
|
|
||||||
|
def _data_content(self, content, asset_name, *args, **kwargs):
|
||||||
|
content = content.encode("utf-8")
|
||||||
|
return self._write_content(content, asset_name)
|
||||||
|
|
||||||
|
def _media_content(self, content, asset_name, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
json.dumps({k: v})
|
media_data = base64.b64decode(content.encode("utf-8"), validate=True)
|
||||||
except TypeError:
|
return self._write_content(media_data, asset_name)
|
||||||
v = str(v)
|
except binascii.Error:
|
||||||
result[k] = v
|
# if not base64 content, just return as it's a file or link
|
||||||
return result
|
return content
|
||||||
|
|
||||||
|
def _write_content(self, content, asset_name):
|
||||||
|
content_relative_path = Path(self._assets_path, asset_name)
|
||||||
|
content_relative_path.write_bytes(content)
|
||||||
|
return str(content_relative_path.relative_to(self._report_path.parent))
|
||||||
|
|
||||||
|
|
||||||
|
class NextGenSelfContainedReport(BaseReport):
|
||||||
|
def __init__(self, report_path, config):
|
||||||
|
super().__init__(report_path, config)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css(self):
|
||||||
|
return self._css
|
||||||
|
|
||||||
|
def _data_content(self, content, mime_type, *args, **kwargs):
|
||||||
|
charset = "utf-8"
|
||||||
|
data = base64.b64encode(content.encode(charset)).decode(charset)
|
||||||
|
return f"data:{mime_type};charset={charset};base64,{data}"
|
||||||
|
|
||||||
|
def _media_content(self, content, mime_type, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
# test if content is base64
|
||||||
|
base64.b64decode(content.encode("utf-8"), validate=True)
|
||||||
|
return f"data:{mime_type};base64,{content}"
|
||||||
|
except binascii.Error:
|
||||||
|
# if not base64, issue warning and just return as it's a file or link
|
||||||
|
warnings.warn(
|
||||||
|
"Self-contained HTML report "
|
||||||
|
"includes link to external "
|
||||||
|
f"resource: {content}"
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _generate_report(self, *args, **kwargs):
|
||||||
|
super()._generate_report(self_contained=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _process_css(default_css, extra_css):
|
||||||
|
with open(default_css, encoding="utf-8") as f:
|
||||||
|
css = f.read()
|
||||||
|
|
||||||
|
# Add user-provided CSS
|
||||||
|
for path in extra_css:
|
||||||
|
css += "\n/******************************"
|
||||||
|
css += "\n * CUSTOM CSS"
|
||||||
|
css += f"\n * {path}"
|
||||||
|
css += "\n ******************************/\n\n"
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
css += f.read()
|
||||||
|
|
||||||
|
# ANSI support
|
||||||
|
if _ansi_styles:
|
||||||
|
ansi_css = [
|
||||||
|
"\n/******************************",
|
||||||
|
" * ANSI2HTML STYLES",
|
||||||
|
" ******************************/\n",
|
||||||
|
]
|
||||||
|
ansi_css.extend([str(r) for r in _ansi_styles])
|
||||||
|
css += "\n".join(ansi_css)
|
||||||
|
|
||||||
|
return css
|
||||||
|
|
||||||
|
|
||||||
|
def _process_outcome(report):
|
||||||
|
if report.when in ["setup", "teardown"] and report.outcome == "failed":
|
||||||
|
return "Error"
|
||||||
|
if hasattr(report, "wasxfail"):
|
||||||
|
if report.outcome in ["passed", "failed"]:
|
||||||
|
return "XPassed"
|
||||||
|
if report.outcome == "skipped":
|
||||||
|
return "XFailed"
|
||||||
|
|
||||||
|
return report.outcome.capitalize()
|
||||||
|
|
||||||
|
|
||||||
|
def _read_template(search_paths, template_name="index.jinja2"):
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(search_paths),
|
||||||
|
autoescape=select_autoescape(
|
||||||
|
enabled_extensions=("jinja2",),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return env.get_template(template_name)
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
from py.xml import html
|
|
||||||
|
|
||||||
|
|
||||||
class Outcome:
|
|
||||||
def __init__(self, outcome, total=0, label=None, test_result=None, class_html=None):
|
|
||||||
self.outcome = outcome
|
|
||||||
self.label = label or outcome
|
|
||||||
self.class_html = class_html or outcome
|
|
||||||
self.total = total
|
|
||||||
self.test_result = test_result or outcome
|
|
||||||
|
|
||||||
self.generate_checkbox()
|
|
||||||
self.generate_summary_item()
|
|
||||||
|
|
||||||
def generate_checkbox(self):
|
|
||||||
checkbox_kwargs = {"data-test-result": self.test_result.lower()}
|
|
||||||
if self.total == 0:
|
|
||||||
checkbox_kwargs["disabled"] = "true"
|
|
||||||
|
|
||||||
self.checkbox = html.input(
|
|
||||||
type="checkbox",
|
|
||||||
checked="true",
|
|
||||||
onChange="filterTable(this)",
|
|
||||||
name="filter_checkbox",
|
|
||||||
class_="filter",
|
|
||||||
hidden="true",
|
|
||||||
**checkbox_kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_summary_item(self):
|
|
||||||
self.summary_item = html.span(
|
|
||||||
f"{self.total} {self.label}", class_=self.class_html
|
|
||||||
)
|
|
|
@ -1,14 +1,13 @@
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
import os
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.pathlib import Path
|
|
||||||
|
|
||||||
from . import extras # noqa: F401
|
|
||||||
from .html_report import HTMLReport
|
|
||||||
from .nextgen import NextGenReport
|
from .nextgen import NextGenReport
|
||||||
|
from .nextgen import NextGenSelfContainedReport
|
||||||
|
|
||||||
|
|
||||||
def pytest_addhooks(pluginmanager):
|
def pytest_addhooks(pluginmanager):
|
||||||
|
@ -43,6 +42,11 @@ def pytest_addoption(parser):
|
||||||
default=[],
|
default=[],
|
||||||
help="append given css file content to report style file.",
|
help="append given css file content to report style file.",
|
||||||
)
|
)
|
||||||
|
parser.addini(
|
||||||
|
"duration_format",
|
||||||
|
default=None,
|
||||||
|
help="the format for duration.",
|
||||||
|
)
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"render_collapsed",
|
"render_collapsed",
|
||||||
type="bool",
|
type="bool",
|
||||||
|
@ -64,40 +68,35 @@ def pytest_addoption(parser):
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
htmlpath = config.getoption("htmlpath")
|
html_path = config.getoption("htmlpath")
|
||||||
if htmlpath:
|
if html_path:
|
||||||
missing_css_files = []
|
missing_css_files = []
|
||||||
for csspath in config.getoption("css"):
|
for css_path in config.getoption("css"):
|
||||||
if not os.path.exists(csspath):
|
if not Path(css_path).exists():
|
||||||
missing_css_files.append(csspath)
|
missing_css_files.append(css_path)
|
||||||
|
|
||||||
if missing_css_files:
|
if missing_css_files:
|
||||||
oserror = (
|
os_error = (
|
||||||
f"Missing CSS file{'s' if len(missing_css_files) > 1 else ''}:"
|
f"Missing CSS file{'s' if len(missing_css_files) > 1 else ''}:"
|
||||||
f" {', '.join(missing_css_files)}"
|
f" {', '.join(missing_css_files)}"
|
||||||
)
|
)
|
||||||
raise OSError(oserror)
|
raise OSError(os_error)
|
||||||
|
|
||||||
if not hasattr(config, "workerinput"):
|
if not hasattr(config, "workerinput"):
|
||||||
# prevent opening htmlpath on worker nodes (xdist)
|
# prevent opening html_path on worker nodes (xdist)
|
||||||
config._html = HTMLReport(htmlpath, config)
|
if config.getoption("self_contained_html"):
|
||||||
|
html = NextGenSelfContainedReport(html_path, config)
|
||||||
|
else:
|
||||||
|
html = NextGenReport(html_path, config)
|
||||||
|
|
||||||
config._next_gen = NextGenReport(config, Path("nextgendata.js"))
|
config.pluginmanager.register(html)
|
||||||
config.pluginmanager.register(config._html)
|
|
||||||
config.pluginmanager.register(config._next_gen)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
def pytest_unconfigure(config):
|
||||||
html = getattr(config, "_html", None)
|
html = config.pluginmanager.getplugin("html")
|
||||||
if html:
|
if html:
|
||||||
del config._html
|
|
||||||
config.pluginmanager.unregister(html)
|
config.pluginmanager.unregister(html)
|
||||||
|
|
||||||
next_gen = getattr(config, "_next_gen", None)
|
|
||||||
if next_gen:
|
|
||||||
del config._next_gen
|
|
||||||
config.pluginmanager.unregister(next_gen)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
|
@ -105,8 +104,8 @@ def pytest_runtest_makereport(item, call):
|
||||||
report = outcome.get_result()
|
report = outcome.get_result()
|
||||||
if report.when == "call":
|
if report.when == "call":
|
||||||
fixture_extras = getattr(item.config, "extras", [])
|
fixture_extras = getattr(item.config, "extras", [])
|
||||||
plugin_extras = getattr(report, "extra", [])
|
plugin_extras = getattr(report, "extras", [])
|
||||||
report.extra = fixture_extras + plugin_extras
|
report.extras = fixture_extras + plugin_extras
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -119,7 +118,29 @@ def extra(pytestconfig):
|
||||||
|
|
||||||
|
|
||||||
def test_foo(extra):
|
def test_foo(extra):
|
||||||
extra.append(pytest_html.extras.url("http://www.example.com/"))
|
extra.append(pytest_html.extras.url("https://www.example.com/"))
|
||||||
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"The 'extra' fixture is deprecated and will be removed in a future release"
|
||||||
|
", use 'extras' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
pytestconfig.extras = []
|
||||||
|
yield pytestconfig.extras
|
||||||
|
del pytestconfig.extras[:]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def extras(pytestconfig):
|
||||||
|
"""Add details to the HTML reports.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import pytest_html
|
||||||
|
|
||||||
|
|
||||||
|
def test_foo(extras):
|
||||||
|
extras.append(pytest_html.extras.url("https://www.example.com/"))
|
||||||
"""
|
"""
|
||||||
pytestconfig.extras = []
|
pytestconfig.extras = []
|
||||||
yield pytestconfig.extras
|
yield pytestconfig.extras
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 122 KiB |
|
@ -1,82 +0,0 @@
|
||||||
// const templateEnvRow = find('#template_environment_row');
|
|
||||||
// const templateResult = find('#template_results-table__tbody');
|
|
||||||
// const aTag = find('#template_a');
|
|
||||||
// const aTagImg = find('#template_img');
|
|
||||||
// const listHeader = find('#template_results-table__head');
|
|
||||||
const dom = {
|
|
||||||
getStaticRow: (key, value) => {
|
|
||||||
var envRow = templateEnvRow.content.cloneNode(true);
|
|
||||||
const isObj = typeof value === 'object' && value !== null;
|
|
||||||
const values = isObj
|
|
||||||
? Object.keys(value).map((k) => `${k}: ${value[k]}`)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const valuesElement = htmlToElements(
|
|
||||||
values
|
|
||||||
? `<ul>${values.map((val) => `<li>${val}</li>`).join('')}<ul>`
|
|
||||||
: `<div>${value}</div>`
|
|
||||||
)[0];
|
|
||||||
var td = findAll('td', envRow);
|
|
||||||
td[0].textContent = key;
|
|
||||||
td[1].appendChild(valuesElement);
|
|
||||||
|
|
||||||
return envRow;
|
|
||||||
},
|
|
||||||
getListHeader: () => {
|
|
||||||
const header = listHeader.content.cloneNode(true);
|
|
||||||
const sortAttr = localStorage.getItem('sort');
|
|
||||||
const sortAsc = JSON.parse(localStorage.getItem('sortAsc'));
|
|
||||||
const sortables = ['outcome', 'nodeid', 'duration'];
|
|
||||||
|
|
||||||
sortables.forEach((sortCol) => {
|
|
||||||
if (sortCol === sortAttr) {
|
|
||||||
find(`[data-column-type="${sortCol}"]`, header).classList.add(
|
|
||||||
sortAsc ? 'desc' : 'asc'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return header;
|
|
||||||
},
|
|
||||||
getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true),
|
|
||||||
getResultTBody: ({ nodeid, longrepr, extras, duration }, outcome) => {
|
|
||||||
const isFail = outcome === 'failed';
|
|
||||||
const resultBody = templateResult.content.cloneNode(true);
|
|
||||||
find('tbody', resultBody).classList.add(outcome);
|
|
||||||
|
|
||||||
find('.col-result', resultBody).innerText = outcome;
|
|
||||||
find('.col-name', resultBody).innerText = nodeid;
|
|
||||||
find('.col-duration', resultBody).innerText = `${(duration * 1000).toFixed(
|
|
||||||
2
|
|
||||||
)}s`;
|
|
||||||
if (isFail) {
|
|
||||||
find('.log', resultBody).innerText = longrepr
|
|
||||||
? longrepr.reprtraceback.reprentries[0].data.lines.join('\n')
|
|
||||||
: '';
|
|
||||||
} else {
|
|
||||||
find('.extras-row', resultBody).classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
extras &&
|
|
||||||
extras.forEach(({ name, format_type, content }) => {
|
|
||||||
const extraLink = aTag.content.cloneNode(true);
|
|
||||||
const extraLinkItem = find('a', extraLink);
|
|
||||||
const folderItems = ['image', 'video', 'text', 'html', 'json'];
|
|
||||||
|
|
||||||
extraLinkItem.href = `${
|
|
||||||
folderItems.includes(format_type) ? 'assets/' : ''
|
|
||||||
}${content}`;
|
|
||||||
extraLinkItem.className = `col-links__extra ${format_type}`;
|
|
||||||
extraLinkItem.innerText = name;
|
|
||||||
find('.col-links', resultBody).appendChild(extraLinkItem);
|
|
||||||
if (format_type === 'image') {
|
|
||||||
const imgElTemp = aTagImg.content.cloneNode(true);
|
|
||||||
find('a', imgElTemp).href = `assets/${content}`;
|
|
||||||
find('img', imgElTemp).src = `assets/${content}`;
|
|
||||||
find('.extra .image', resultBody).appendChild(imgElTemp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return resultBody;
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,46 +0,0 @@
|
||||||
const dispatchEvent = (type, detail) => {
|
|
||||||
const newEvent = new CustomEvent(type, { detail });
|
|
||||||
|
|
||||||
document.dispatchEvent(newEvent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const doSort = (type) => {
|
|
||||||
const newSortType = localStorage.getItem('sort') !== type;
|
|
||||||
const currentAsc = JSON.parse(localStorage.getItem('sortAsc'));
|
|
||||||
const ascending = newSortType ? true : !currentAsc;
|
|
||||||
localStorage.setItem('sort', type);
|
|
||||||
localStorage.setItem('sortAsc', ascending);
|
|
||||||
dispatchEvent('sort', { type, ascending });
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('sort', (e) => {
|
|
||||||
const { type, ascending } = e.detail;
|
|
||||||
const sortedList = genericSort(renderData.tests, type, ascending);
|
|
||||||
renderData.tests = sortedList;
|
|
||||||
initRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
const doFilter = (type, apply) => {
|
|
||||||
const currentFilter = [
|
|
||||||
...new Set(JSON.parse(localStorage.getItem('filter'))),
|
|
||||||
];
|
|
||||||
if (apply) {
|
|
||||||
currentFilter.push(type);
|
|
||||||
} else {
|
|
||||||
const index = currentFilter.indexOf(type);
|
|
||||||
currentFilter.splice(index, 1);
|
|
||||||
}
|
|
||||||
localStorage.setItem('filter', JSON.stringify(currentFilter));
|
|
||||||
const filteredSubset = [];
|
|
||||||
if (currentFilter.length) {
|
|
||||||
currentFilter.forEach((filter) => {
|
|
||||||
filteredSubset.push(
|
|
||||||
...jsonData.tests.filter(({ outcome }) => outcome === filter)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
renderData.tests = filteredSubset;
|
|
||||||
} else {
|
|
||||||
renderData.tests = jsonData.tests;
|
|
||||||
}
|
|
||||||
initRender();
|
|
||||||
};
|
|
|
@ -1,100 +0,0 @@
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title>Test Report</title>
|
|
||||||
<link href="style.css" rel="stylesheet" type="text/css"/>
|
|
||||||
</head>
|
|
||||||
<body onLoad="init()">
|
|
||||||
<h1 id="title"></h1>
|
|
||||||
<p>Report generated on 17-Dec-2020 at 14:02:36 by <a href="https://pypi.python.org/pypi/pytest-html">pytest-html</a> v1.22.0</p>
|
|
||||||
<h2>Environment</h2>
|
|
||||||
<table id="environment"><table>
|
|
||||||
<!-- TEMPLATES -->
|
|
||||||
<template id="template_environment_row">
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template id="template_a">
|
|
||||||
<a target="_blank"></a>
|
|
||||||
</template>
|
|
||||||
<template id="template_img">
|
|
||||||
<a target="_blank">
|
|
||||||
<img />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template id="template_results-table__tbody">
|
|
||||||
<tbody class="results-table-row">
|
|
||||||
<tr>
|
|
||||||
<td class="col-result"></td>
|
|
||||||
<td class="col-name"></td>
|
|
||||||
<td class="col-duration"></td>
|
|
||||||
<td class="col-links"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="extras-row">
|
|
||||||
<td class="extra" colspan="4">
|
|
||||||
<div class="image"></div>
|
|
||||||
<div class="log"></div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</template>
|
|
||||||
<template id="template_results-table__head">
|
|
||||||
<thead id="results-table-head">
|
|
||||||
<tr>
|
|
||||||
<th class="sortable" data-column-type="outcome">Result</th>
|
|
||||||
<th class="sortable" data-column-type="nodeid">Test</th>
|
|
||||||
<th class="sortable" data-column-type="duration">Duration</th>
|
|
||||||
<th>Links</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</template>
|
|
||||||
<template id="template_results-table__head--empty">
|
|
||||||
<tr id="not-found-message">
|
|
||||||
<th colspan="4">No results found. Try to check the filters</th>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<!-- END TEMPLATES -->
|
|
||||||
<div class="summary">
|
|
||||||
<div class="summary__data">
|
|
||||||
<h2>Summary</h2>
|
|
||||||
<p class="run-count"></p>
|
|
||||||
<p class="filter">(Un)check the boxes to filter the results.</p>
|
|
||||||
|
|
||||||
<input checked="true" class="filter" data-test-result="error" name="filter_checkbox" type="checkbox"/><span class="error"></span>
|
|
||||||
<input checked="true" class="filter" data-test-result="failed" name="filter_checkbox" type="checkbox"/><span class="failed"></span>
|
|
||||||
<input checked="true" class="filter" data-test-result="rerun" name="filter_checkbox" type="checkbox"/><span class="rerun"></span>
|
|
||||||
<input checked="true" class="filter" data-test-result="xfailed" name="filter_checkbox" type="checkbox"/><span class="xfailed"></span>
|
|
||||||
<input checked="true" class="filter" data-test-result="xpassed" name="filter_checkbox" type="checkbox"/><span class="xpassed"></span>
|
|
||||||
<input checked="true" class="filter" data-test-result="passed" name="filter_checkbox" type="checkbox"/><span class="passed"></span>
|
|
||||||
<input checked="true" class="filter" data-test-result="skipped" name="filter_checkbox" type="checkbox"/><span class="skipped"></span>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="summary__reload">
|
|
||||||
<div class="summary__reload__button" onclick="location.reload()">
|
|
||||||
<div>There are still tests running. <br />Reload this page to ge the latest results!</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary__spacer">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2>Results</h2>
|
|
||||||
<table id="results-table">
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
<footer>
|
|
||||||
<script src="events.js"></script>
|
|
||||||
<script src="dom.js"></script>
|
|
||||||
<script src="nextgendata.js"></script>
|
|
||||||
<script src="sort.js"></script>
|
|
||||||
<script src="index.js"></script>
|
|
||||||
<script src="main.js"></script>
|
|
||||||
</footer>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title id="head-title"></title>
|
||||||
|
{% if self_contained %}
|
||||||
|
<style type="text/css">
|
||||||
|
{{- styles|safe }}
|
||||||
|
</style>
|
||||||
|
{% else %}
|
||||||
|
<link href="{{ styles }}" rel="stylesheet" type="text/css"/>
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 id="title"></h1>
|
||||||
|
<p>Report generated on {{ date }} at {{ time }} by <a href="https://pypi.python.org/pypi/pytest-html">pytest-html</a>
|
||||||
|
v{{ version }}</p>
|
||||||
|
<h2>Environment</h2>
|
||||||
|
<table id="environment"></table>
|
||||||
|
<!-- TEMPLATES -->
|
||||||
|
<template id="template_environment_row">
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template id="template_a">
|
||||||
|
<a target="_blank"></a>
|
||||||
|
</template>
|
||||||
|
<template id="template_results-table__tbody">
|
||||||
|
<tbody class="results-table-row">
|
||||||
|
<tr>
|
||||||
|
<td class="col-result"></td>
|
||||||
|
<td class="col-name"></td>
|
||||||
|
<td class="col-duration"></td>
|
||||||
|
<td class="col-links"></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="extras-row">
|
||||||
|
<td class="extra" colspan="4">
|
||||||
|
<div class="extraHTML"></div>
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-container">
|
||||||
|
<div class="media-container__nav--left"><</div>
|
||||||
|
<div class="media-container__viewport">
|
||||||
|
<img src="" />
|
||||||
|
<video controls>
|
||||||
|
<source src="" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="media-container__nav--right">></div>
|
||||||
|
</div>
|
||||||
|
<div class="media__name"></div>
|
||||||
|
<div class="media__counter"></div>
|
||||||
|
</div>
|
||||||
|
<div class="log"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
<template id="template_results-table__head">
|
||||||
|
<thead id="results-table-head">
|
||||||
|
<tr>
|
||||||
|
<th class="sortable" data-column-type="outcome">Result</th>
|
||||||
|
<th class="sortable" data-column-type="nodeid">Test</th>
|
||||||
|
<th class="sortable" data-column-type="duration">Duration</th>
|
||||||
|
<th>Links</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</template>
|
||||||
|
<template id="template_results-table__head--empty">
|
||||||
|
<tr id="not-found-message">
|
||||||
|
<th colspan="4">No results found. Check the filters.</th>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template id="template_table-colgroup">
|
||||||
|
<colgroup>
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
</colgroup>
|
||||||
|
</template>
|
||||||
|
<!-- END TEMPLATES -->
|
||||||
|
<div class="summary">
|
||||||
|
<div class="summary__data">
|
||||||
|
<h2>Summary</h2>
|
||||||
|
{% for p in prefix %}
|
||||||
|
{{ p|safe }}
|
||||||
|
{% endfor %}
|
||||||
|
<p class="run-count"></p>
|
||||||
|
<p class="filter">(Un)check the boxes to filter the results.</p>
|
||||||
|
<div class="summary__reload">
|
||||||
|
<div class="summary__reload__button" onclick="location.reload()">
|
||||||
|
<div>There are still tests running. <br />Reload this page to ge the latest results!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary__spacer"></div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="filters">
|
||||||
|
<input checked="true" class="filter" data-test-result="error" name="filter_checkbox" type="checkbox"/><span class="error"></span>
|
||||||
|
<input checked="true" class="filter" data-test-result="failed" name="filter_checkbox" type="checkbox"/><span class="failed"></span>
|
||||||
|
<input checked="true" class="filter" data-test-result="rerun" name="filter_checkbox" type="checkbox"/><span class="rerun"></span>
|
||||||
|
<input checked="true" class="filter" data-test-result="xfailed" name="filter_checkbox" type="checkbox"/><span class="xfailed"></span>
|
||||||
|
<input checked="true" class="filter" data-test-result="xpassed" name="filter_checkbox" type="checkbox"/><span class="xpassed"></span>
|
||||||
|
<input checked="true" class="filter" data-test-result="passed" name="filter_checkbox" type="checkbox"/><span class="passed"></span>
|
||||||
|
<input checked="true" class="filter" data-test-result="skipped" name="filter_checkbox" type="checkbox"/><span class="skipped"></span>
|
||||||
|
</div>
|
||||||
|
<div class="collapse">
|
||||||
|
<button id="show_all_details">Show all details</button> / <button id="hide_all_details">Hide all details</button>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for s in summary %}
|
||||||
|
{{ s|safe }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for p in postfix %}
|
||||||
|
{{ p|safe }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<table id="results-table"></table>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
<div id="data-container" data-jsonblob="{{ test_data }}"></div>
|
||||||
|
<script>
|
||||||
|
{% include "app.js" %}
|
||||||
|
</script>
|
||||||
|
</footer>
|
||||||
|
</html>
|
|
@ -1,134 +0,0 @@
|
||||||
function htmlToElements(html) {
|
|
||||||
let temp = document.createElement('template');
|
|
||||||
temp.innerHTML = html;
|
|
||||||
return temp.content.childNodes;
|
|
||||||
}
|
|
||||||
const find = (selector, elem) => {
|
|
||||||
if (!elem) {
|
|
||||||
elem = document;
|
|
||||||
}
|
|
||||||
return elem.querySelector(selector);
|
|
||||||
};
|
|
||||||
const findAll = (selector, elem) => {
|
|
||||||
if (!elem) {
|
|
||||||
elem = document;
|
|
||||||
}
|
|
||||||
return toArray(elem.querySelectorAll(selector));
|
|
||||||
};
|
|
||||||
const templateEnvRow = find('#template_environment_row');
|
|
||||||
const templateResult = find('#template_results-table__tbody');
|
|
||||||
const aTag = find('#template_a');
|
|
||||||
const aTagImg = find('#template_img');
|
|
||||||
const listHeader = find('#template_results-table__head');
|
|
||||||
const listHeaderEmpty = find('#template_results-table__head--empty');
|
|
||||||
|
|
||||||
const removeChildren = (node) => {
|
|
||||||
while (node.firstChild) {
|
|
||||||
node.removeChild(node.firstChild);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOutcome = ({ nodeid }, tests) => {
|
|
||||||
const relatedOutcome = tests
|
|
||||||
.filter((test) => test.nodeid === nodeid)
|
|
||||||
.map(({ outcome }) => outcome);
|
|
||||||
if (relatedOutcome.includes('failed')) {
|
|
||||||
return 'failed';
|
|
||||||
} else if (relatedOutcome.includes('error')) {
|
|
||||||
return 'error';
|
|
||||||
} else if (relatedOutcome.includes('xpassed')) {
|
|
||||||
return 'xpassed';
|
|
||||||
} else if (relatedOutcome.includes('xfailed')) {
|
|
||||||
return 'xfailed';
|
|
||||||
} else if (relatedOutcome.includes('skipped')) {
|
|
||||||
return 'skipped';
|
|
||||||
} else {
|
|
||||||
return 'passed';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStatic = (obj) => {
|
|
||||||
find('#title').innerText = obj.title;
|
|
||||||
const rows = Object.keys(obj.environment).map((key) =>
|
|
||||||
dom.getStaticRow(key, obj.environment[key])
|
|
||||||
);
|
|
||||||
|
|
||||||
const table = find('#environment');
|
|
||||||
removeChildren(table);
|
|
||||||
rows.forEach((row) => table.appendChild(row));
|
|
||||||
};
|
|
||||||
const renderContent = ({ tests }) => {
|
|
||||||
const renderSet = tests.filter(({ when }) => when === 'call');
|
|
||||||
|
|
||||||
const rows = renderSet.map((test) =>
|
|
||||||
dom.getResultTBody(test, getOutcome(test, tests))
|
|
||||||
);
|
|
||||||
const table = find('#results-table');
|
|
||||||
removeChildren(table);
|
|
||||||
const tableHeader = dom.getListHeader();
|
|
||||||
if (!rows.length) {
|
|
||||||
tableHeader.appendChild(dom.getListHeaderEmpty());
|
|
||||||
}
|
|
||||||
table.appendChild(tableHeader);
|
|
||||||
|
|
||||||
rows.forEach((row) => !!row && table.appendChild(row));
|
|
||||||
};
|
|
||||||
const renderDerrived = ({ tests, collectedItems }) => {
|
|
||||||
const renderSet = tests.filter(({ when }) => when === 'call');
|
|
||||||
|
|
||||||
const possibleOutcomes = [
|
|
||||||
'passed',
|
|
||||||
'skipped',
|
|
||||||
'failed',
|
|
||||||
'error',
|
|
||||||
'xfailed',
|
|
||||||
'xpassed',
|
|
||||||
'rerun',
|
|
||||||
];
|
|
||||||
possibleOutcomes.forEach((outcome) => {
|
|
||||||
const count = renderSet.filter((test) => test.outcome === outcome).length;
|
|
||||||
|
|
||||||
find(`.${outcome}`).innerText = `${count} ${outcome}`;
|
|
||||||
if (!count) {
|
|
||||||
find(`input[data-test-result="${outcome}"]`).disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let accTime = 0;
|
|
||||||
if (collectedItems === renderSet.length) {
|
|
||||||
tests.forEach(({ duration }) => (accTime += duration));
|
|
||||||
accTime = accTime.toFixed(2);
|
|
||||||
find(
|
|
||||||
'.run-count'
|
|
||||||
).innerText = `${renderSet.length} tests ran in ${accTime} seconds.`;
|
|
||||||
find('.summary__reload__button').classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
find(
|
|
||||||
'.run-count'
|
|
||||||
).innerText = `${renderSet.length} / ${collectedItems} tests done`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindEvents = () => {
|
|
||||||
findAll('.sortable').forEach((elem) => {
|
|
||||||
elem.addEventListener('click', (evt) => {
|
|
||||||
const { target: element } = evt;
|
|
||||||
const { columnType } = element.dataset;
|
|
||||||
|
|
||||||
doSort(columnType);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPage = (subset, full) => {
|
|
||||||
renderStatic(jsonData);
|
|
||||||
renderContent(renderData);
|
|
||||||
renderDerrived(jsonData);
|
|
||||||
};
|
|
||||||
let renderData = { ...jsonData };
|
|
||||||
const initRender = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
renderPage(renderData);
|
|
||||||
bindEvents();
|
|
||||||
}, 0);
|
|
||||||
};
|
|
|
@ -1,115 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
function is_all_rows_hidden(value) {
|
|
||||||
return value.hidden == false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filter_table(elem) {
|
|
||||||
var outcome_att = 'data-test-result';
|
|
||||||
var outcome = elem.getAttribute(outcome_att);
|
|
||||||
console.log(outcome);
|
|
||||||
class_outcome = outcome + ' results-table-row';
|
|
||||||
var outcome_rows = document.getElementsByClassName(class_outcome);
|
|
||||||
|
|
||||||
for (var i = 0; i < outcome_rows.length; i++) {
|
|
||||||
outcome_rows[i].hidden = !elem.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows = find_all('.results-table-row').filter(is_all_rows_hidden);
|
|
||||||
var all_rows_hidden = rows.length == 0 ? true : false;
|
|
||||||
var not_found_message = document.getElementById('not-found-message');
|
|
||||||
not_found_message.hidden = !all_rows_hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toArray(iter) {
|
|
||||||
if (iter === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Array.prototype.slice.call(iter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAllExtras() {
|
|
||||||
// eslint-disable-line no-unused-vars
|
|
||||||
findAll('.col-result').forEach(showExtras);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideAllExtras() {
|
|
||||||
// eslint-disable-line no-unused-vars
|
|
||||||
findAll('.col-result').forEach(hideExtras);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showExtras(colresultElem) {
|
|
||||||
const extras = colresultElem.parentNode.nextElementSibling;
|
|
||||||
const expandcollapse = colresultElem.firstElementChild;
|
|
||||||
extras.classList.remove('collapsed');
|
|
||||||
expandcollapse.classList.remove('expander');
|
|
||||||
expandcollapse.classList.add('collapser');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideExtras(colresultElem) {
|
|
||||||
const extras = colresultElem.parentNode.nextElementSibling;
|
|
||||||
const expandcollapse = colresultElem.firstElementChild;
|
|
||||||
extras.classList.add('collapsed');
|
|
||||||
expandcollapse.classList.remove('collapser');
|
|
||||||
expandcollapse.classList.add('expander');
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCollapse() {
|
|
||||||
// Add links for show/hide all
|
|
||||||
const resulttable = find('table#results-table');
|
|
||||||
const showhideall = document.createElement('p');
|
|
||||||
showhideall.innerHTML =
|
|
||||||
'<a href="javascript:showAllExtras()">Show all details</a> / ' +
|
|
||||||
'<a href="javascript:hideAllExtras()">Hide all details</a>';
|
|
||||||
resulttable.parentElement.insertBefore(showhideall, resulttable);
|
|
||||||
|
|
||||||
// Add show/hide link to each result
|
|
||||||
findAll('.col-result').forEach(function (elem) {
|
|
||||||
const collapsed = getQueryParameter('collapsed') || 'Passed';
|
|
||||||
const extras = elem.parentNode.nextElementSibling;
|
|
||||||
const expandcollapse = document.createElement('span');
|
|
||||||
if (extras.classList.contains('collapsed')) {
|
|
||||||
expandcollapse.classList.add('expander');
|
|
||||||
} else if (collapsed.includes(elem.innerHTML)) {
|
|
||||||
extras.classList.add('collapsed');
|
|
||||||
expandcollapse.classList.add('expander');
|
|
||||||
} else {
|
|
||||||
expandcollapse.classList.add('collapser');
|
|
||||||
}
|
|
||||||
elem.appendChild(expandcollapse);
|
|
||||||
|
|
||||||
elem.addEventListener('click', function (event) {
|
|
||||||
if (
|
|
||||||
event.currentTarget.parentNode.nextElementSibling.classList.contains(
|
|
||||||
'collapsed'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
showExtras(event.currentTarget);
|
|
||||||
} else {
|
|
||||||
hideExtras(event.currentTarget);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQueryParameter(name) {
|
|
||||||
const match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
|
|
||||||
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
|
|
||||||
}
|
|
||||||
function init() {
|
|
||||||
// eslint-disable-line no-unused-vars
|
|
||||||
setTimeout(() => {
|
|
||||||
findAll('input[name="filter_checkbox"]').forEach((elem) => {
|
|
||||||
elem.addEventListener('click', (evt) => {
|
|
||||||
const { target: element } = evt;
|
|
||||||
const { testResult } = element.dataset;
|
|
||||||
|
|
||||||
doFilter(testResult, element.checked);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
initRender();
|
|
||||||
addCollapse();
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,734 +0,0 @@
|
||||||
const jsonData = {
|
|
||||||
title: 'Next Gen Report',
|
|
||||||
collectedItems: 10,
|
|
||||||
environment: {
|
|
||||||
Python: '3.7.7',
|
|
||||||
Platform: 'Darwin-19.6.0-x86_64-i386-64bit',
|
|
||||||
Packages: { pytest: '6.1.2', py: '1.9.0', pluggy: '0.13.1' },
|
|
||||||
Plugins: { metadata: '1.11.0', reportlog: '0.1.2', html: '2.1.2.dev78' },
|
|
||||||
},
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url',
|
|
||||||
location: ['test_html.py', 3, 'test_url'],
|
|
||||||
keywords: { test_url: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.0001576979999999839,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url',
|
|
||||||
location: ['test_html.py', 3, 'test_url'],
|
|
||||||
keywords: { test_url: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'failed',
|
|
||||||
extras: [
|
|
||||||
{
|
|
||||||
name: 'Snapshot',
|
|
||||||
format_type: 'image',
|
|
||||||
content: 'snapshot_of_what_went_wrong.png',
|
|
||||||
mime_type: 'image/png',
|
|
||||||
extension: '.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Google',
|
|
||||||
format_type: 'url',
|
|
||||||
content: 'https://search.yahoo.com/',
|
|
||||||
mime_type: null,
|
|
||||||
extension: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
longrepr: {
|
|
||||||
reprcrash: {
|
|
||||||
path: '/Users/jimbrannlund/dev/pytest-dev/testing-html/test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'assert False',
|
|
||||||
},
|
|
||||||
reprtraceback: {
|
|
||||||
reprentries: [
|
|
||||||
{
|
|
||||||
type: 'ReprEntry',
|
|
||||||
data: {
|
|
||||||
lines: [
|
|
||||||
'universe = <pytest_setup.database.TestDataCollection object at 0x7fe2cf1c4dd8>',
|
|
||||||
'driver = <selenium.webdriver.firefox.webdriver.WebDriver (session="2c586c44-7cb6-8e48-86fd-ae2d2982a3bd")>',
|
|
||||||
'',
|
|
||||||
'@pytest.mark.user(name="Bob", account=\'Rolfs Account\')',
|
|
||||||
"@pytest.mark.setup_data({'EnterpriseProject': [{'name': 'Dependencies', 'user': 'Bob'}]},",
|
|
||||||
"{'Activity': [",
|
|
||||||
"{'name': 'Move',",
|
|
||||||
"'project': 'Dependencies',",
|
|
||||||
"'user': 'Bob',",
|
|
||||||
"'dates': {'start_date': 1, 'end_date': 7}},",
|
|
||||||
"{'name': 'Nope',",
|
|
||||||
"'project': 'Dependencies',",
|
|
||||||
"'user': 'Bob',",
|
|
||||||
"'dates': {'start_date': 8, 'end_date': 14}}]})",
|
|
||||||
'def test_dependency_forces_date_change(universe, driver):',
|
|
||||||
'from test_automation.tools.date_handling import convert_to_correct_date',
|
|
||||||
' ',
|
|
||||||
'_to_depend_on = "Move"',
|
|
||||||
"_expected_dates = {'start_date': convert_to_correct_date(15), 'end_date': convert_to_correct_date(21)}",
|
|
||||||
' ',
|
|
||||||
"_user = universe.get('User', 'Bob')",
|
|
||||||
"_act = universe.get('Activity', 'Move')",
|
|
||||||
' ',
|
|
||||||
'ActivityDetailsPane(driver).',
|
|
||||||
"go_to_page(universe.get('User', 'Bob'),",
|
|
||||||
"universe.get('EnterpriseProject', 'Dependencies'),",
|
|
||||||
'universe.get("Activity", "Nope")). ',
|
|
||||||
"open_activity('Move'). ",
|
|
||||||
"> open_activity('Nope'). ",
|
|
||||||
'open_widgets_section(). ',
|
|
||||||
"get_widget('planletdependencies').",
|
|
||||||
'add_predecessor(_to_depend_on).',
|
|
||||||
"get_details_pane('activity').",
|
|
||||||
'close_details_pane().',
|
|
||||||
'click_planlet_checkbox(_act.id).',
|
|
||||||
"get_widget('date').",
|
|
||||||
'verify_dates(_expected_dates)',
|
|
||||||
'',
|
|
||||||
'test_automation/tests/ui_tests/plan_tests/activity_details_pane_test.py:121: ',
|
|
||||||
'_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ',
|
|
||||||
'test_automation/page_objects/details_panes/activity_details_pane.py:241: in open_widgets_section',
|
|
||||||
'_wait_for_plus_sign()',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/basepage/decorators.py:112: in wait_handler',
|
|
||||||
'value = func(*args, **kwargs)',
|
|
||||||
'test_automation/page_objects/details_panes/activity_details_pane.py:236: in _wait_for_plus_sign',
|
|
||||||
'if self.get_present_element(self._plus_sign_locator):',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/basepage/base_page.py:422: in get_present_element',
|
|
||||||
'return self._get(locator, expected_condition, params, timeout, error_msg, parent)',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/basepage/base_page.py:569: in _get',
|
|
||||||
'return WebDriverWait(_driver, timeout).until(exp_cond, error_msg)',
|
|
||||||
'_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ',
|
|
||||||
'',
|
|
||||||
'self = <selenium.webdriver.support.wait.WebDriverWait (session="2c586c44-7cb6-8e48-86fd-ae2d2982a3bd")>',
|
|
||||||
'method = <selenium.webdriver.support.expected_conditions.presence_of_element_located object at 0x7fe2cf1a7240>',
|
|
||||||
'message = "Element was never present!\nLocator of type <CSS_SELECTOR> with selector <.pp-rounded-btn__icon.category-bck-3> with ...nExpected condition: <class \'selenium.webdriver.support.expected_conditions.presence_of_element_located\'>\nTimeout: 30"',
|
|
||||||
'',
|
|
||||||
"def until(self, method, message=''):",
|
|
||||||
'"""Calls the method provided with the driver as an argument until the ',
|
|
||||||
'return value is not False."""',
|
|
||||||
'screen = None',
|
|
||||||
'stacktrace = None',
|
|
||||||
' ',
|
|
||||||
'end_time = time.time() + self._timeout',
|
|
||||||
'while True:',
|
|
||||||
'try:',
|
|
||||||
'value = method(self._driver)',
|
|
||||||
'if value:',
|
|
||||||
'return value',
|
|
||||||
'except self._ignored_exceptions as exc:',
|
|
||||||
"screen = getattr(exc, 'screen', None)",
|
|
||||||
"stacktrace = getattr(exc, 'stacktrace', None)",
|
|
||||||
'time.sleep(self._poll)',
|
|
||||||
'if time.time() > end_time:',
|
|
||||||
'break',
|
|
||||||
'> raise TimeoutException(message, screen, stacktrace)',
|
|
||||||
'E selenium.common.exceptions.TimeoutException: Message: Element was never present!',
|
|
||||||
'E Locator of type <CSS_SELECTOR> with selector <.pp-rounded-btn__icon.category-bck-3> with params <None>',
|
|
||||||
"E Expected condition: <class 'selenium.webdriver.support.expected_conditions.presence_of_element_located'>",
|
|
||||||
'E Timeout: 30',
|
|
||||||
'',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/selenium/webdriver/support/wait.py:80: TimeoutException',
|
|
||||||
'------------------------------- pytest-selenium --------------------------------',
|
|
||||||
'Driver log: /private/var/folders/nt/zg2hyyxj6k77sf_qk34xtqf40000gn/T/pytest-of-vgustafsson/pytest-12/test_dependency_forces_date_ch0/driver.log',
|
|
||||||
'URL: https://local.rnd.projectplace.com/#project/79788/plan/28838',
|
|
||||||
'WARNING: Failed to gather log types: Message: HTTP method not allowed',
|
|
||||||
'',
|
|
||||||
'----------------------------- Additional Reporting -----------------------------',
|
|
||||||
'Window size: width <1680>, height <983>',
|
|
||||||
"WARNING: Failed to get HAR: 'NoneType' object has no attribute 'har'",
|
|
||||||
'Logs can be found here: http://elk.rnd.projectplace.com/goto/152df50b1a44ad0',
|
|
||||||
],
|
|
||||||
reprfuncargs: { args: [] },
|
|
||||||
reprlocals: null,
|
|
||||||
reprfileloc: {
|
|
||||||
path: 'test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'AssertionError',
|
|
||||||
},
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extraline: null,
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
sections: [],
|
|
||||||
chain: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
reprentries: [
|
|
||||||
{
|
|
||||||
type: 'ReprEntry',
|
|
||||||
data: {
|
|
||||||
lines: [
|
|
||||||
' def test_url():',
|
|
||||||
' """',
|
|
||||||
' bla bla bla bla',
|
|
||||||
' alb alb alb',
|
|
||||||
' @param: hello',
|
|
||||||
' :param just',
|
|
||||||
' """',
|
|
||||||
' # driver.get("https://www.google.com")',
|
|
||||||
'> assert False',
|
|
||||||
'E assert False',
|
|
||||||
],
|
|
||||||
reprfuncargs: { args: [] },
|
|
||||||
reprlocals: null,
|
|
||||||
reprfileloc: {
|
|
||||||
path: 'test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'AssertionError',
|
|
||||||
},
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extraline: null,
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path:
|
|
||||||
'/Users/jimbrannlund/dev/pytest-dev/testing-html/test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'assert False',
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00020797699999997032,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url',
|
|
||||||
location: ['test_html.py', 3, 'test_url'],
|
|
||||||
keywords: { test_url: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.0001561270000000059,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url2',
|
|
||||||
location: ['test_html.py', 14, 'test_url2'],
|
|
||||||
keywords: { test_url2: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url2',
|
|
||||||
location: ['test_html.py', 14, 'test_url2'],
|
|
||||||
keywords: { test_url2: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url2',
|
|
||||||
location: ['test_html.py', 14, 'test_url2'],
|
|
||||||
keywords: { test_url2: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url4',
|
|
||||||
location: ['test_html.py', 14, 'test_url4'],
|
|
||||||
keywords: { test_url4: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url4',
|
|
||||||
location: ['test_html.py', 14, 'test_url4'],
|
|
||||||
keywords: { test_url4: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url4',
|
|
||||||
location: ['test_html.py', 14, 'test_url4'],
|
|
||||||
keywords: { test_url4: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url5',
|
|
||||||
location: ['test_html.py', 14, 'test_url5'],
|
|
||||||
keywords: { test_url5: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url5',
|
|
||||||
location: ['test_html.py', 14, 'test_url5'],
|
|
||||||
keywords: { test_url5: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url5',
|
|
||||||
location: ['test_html.py', 14, 'test_url5'],
|
|
||||||
keywords: { test_url5: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url6',
|
|
||||||
location: ['test_html.py', 14, 'test_url6'],
|
|
||||||
keywords: { test_url6: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url6',
|
|
||||||
location: ['test_html.py', 14, 'test_url6'],
|
|
||||||
keywords: { test_url6: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url6',
|
|
||||||
location: ['test_html.py', 14, 'test_url6'],
|
|
||||||
keywords: { test_url6: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url7',
|
|
||||||
location: ['test_html.py', 14, 'test_url7'],
|
|
||||||
keywords: { test_url7: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url7',
|
|
||||||
location: ['test_html.py', 14, 'test_url7'],
|
|
||||||
keywords: { test_url7: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url7',
|
|
||||||
location: ['test_html.py', 14, 'test_url7'],
|
|
||||||
keywords: { test_url7: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url8',
|
|
||||||
location: ['test_html.py', 14, 'test_url8'],
|
|
||||||
keywords: { test_url8: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url8',
|
|
||||||
location: ['test_html.py', 14, 'test_url8'],
|
|
||||||
keywords: { test_url8: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url8',
|
|
||||||
location: ['test_html.py', 14, 'test_url8'],
|
|
||||||
keywords: { test_url8: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url9',
|
|
||||||
location: ['test_html.py', 14, 'test_url9'],
|
|
||||||
keywords: { test_url9: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url9',
|
|
||||||
location: ['test_html.py', 14, 'test_url9'],
|
|
||||||
keywords: { test_url9: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url9',
|
|
||||||
location: ['test_html.py', 14, 'test_url9'],
|
|
||||||
keywords: { test_url9: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url10',
|
|
||||||
location: ['test_html.py', 14, 'test_url10'],
|
|
||||||
keywords: { test_url10: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00010436599999996687,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url10',
|
|
||||||
location: ['test_html.py', 14, 'test_url10'],
|
|
||||||
keywords: { test_url10: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00011543000000002746,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url10',
|
|
||||||
location: ['test_html.py', 14, 'test_url10'],
|
|
||||||
keywords: { test_url10: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 9.126600000003426,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url112',
|
|
||||||
location: ['test_html.py', 3, 'test_url112'],
|
|
||||||
keywords: { test_url112: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'setup',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.0001576979999999839,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url112',
|
|
||||||
location: ['test_html.py', 3, 'test_url112'],
|
|
||||||
keywords: { test_url112: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'failed',
|
|
||||||
extras: [
|
|
||||||
{
|
|
||||||
name: 'Snapshot',
|
|
||||||
format_type: 'image',
|
|
||||||
content: 'snapshot_of_what_went_wrong.png',
|
|
||||||
mime_type: 'image/png',
|
|
||||||
extension: '.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Google',
|
|
||||||
format_type: 'url',
|
|
||||||
content: 'https://search.yahoo.com/',
|
|
||||||
mime_type: null,
|
|
||||||
extension: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
longrepr: {
|
|
||||||
reprcrash: {
|
|
||||||
path: '/Users/jimbrannlund/dev/pytest-dev/testing-html/test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'assert False',
|
|
||||||
},
|
|
||||||
reprtraceback: {
|
|
||||||
reprentries: [
|
|
||||||
{
|
|
||||||
type: 'ReprEntry',
|
|
||||||
data: {
|
|
||||||
lines: [
|
|
||||||
'universe = <pytest_setup.database.TestDataCollection object at 0x7fe2cf1c4dd8>',
|
|
||||||
'driver = <selenium.webdriver.firefox.webdriver.WebDriver (session="2c586c44-7cb6-8e48-86fd-ae2d2982a3bd")>',
|
|
||||||
'',
|
|
||||||
'@pytest.mark.user(name="Bob", account=\'Rolfs Account\')',
|
|
||||||
"@pytest.mark.setup_data({'EnterpriseProject': [{'name': 'Dependencies', 'user': 'Bob'}]},",
|
|
||||||
"{'Activity': [",
|
|
||||||
"{'name': 'Move',",
|
|
||||||
"'project': 'Dependencies',",
|
|
||||||
"'user': 'Bob',",
|
|
||||||
"'dates': {'start_date': 1, 'end_date': 7}},",
|
|
||||||
"{'name': 'Nope',",
|
|
||||||
"'project': 'Dependencies',",
|
|
||||||
"'user': 'Bob',",
|
|
||||||
"'dates': {'start_date': 8, 'end_date': 14}}]})",
|
|
||||||
'def test_dependency_forces_date_change(universe, driver):',
|
|
||||||
'from test_automation.tools.date_handling import convert_to_correct_date',
|
|
||||||
' ',
|
|
||||||
'_to_depend_on = "Move"',
|
|
||||||
"_expected_dates = {'start_date': convert_to_correct_date(15), 'end_date': convert_to_correct_date(21)}",
|
|
||||||
' ',
|
|
||||||
"_user = universe.get('User', 'Bob')",
|
|
||||||
"_act = universe.get('Activity', 'Move')",
|
|
||||||
' ',
|
|
||||||
'ActivityDetailsPane(driver).',
|
|
||||||
"go_to_page(universe.get('User', 'Bob'),",
|
|
||||||
"universe.get('EnterpriseProject', 'Dependencies'),",
|
|
||||||
'universe.get("Activity", "Nope")). ',
|
|
||||||
"open_activity('Move'). ",
|
|
||||||
"> open_activity('Nope'). ",
|
|
||||||
'open_widgets_section(). ',
|
|
||||||
"get_widget('planletdependencies').",
|
|
||||||
'add_predecessor(_to_depend_on).',
|
|
||||||
"get_details_pane('activity').",
|
|
||||||
'close_details_pane().',
|
|
||||||
'click_planlet_checkbox(_act.id).',
|
|
||||||
"get_widget('date').",
|
|
||||||
'verify_dates(_expected_dates)',
|
|
||||||
'',
|
|
||||||
'test_automation/tests/ui_tests/plan_tests/activity_details_pane_test.py:121: ',
|
|
||||||
'_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ',
|
|
||||||
'test_automation/page_objects/details_panes/activity_details_pane.py:241: in open_widgets_section',
|
|
||||||
'_wait_for_plus_sign()',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/basepage/decorators.py:112: in wait_handler',
|
|
||||||
'value = func(*args, **kwargs)',
|
|
||||||
'test_automation/page_objects/details_panes/activity_details_pane.py:236: in _wait_for_plus_sign',
|
|
||||||
'if self.get_present_element(self._plus_sign_locator):',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/basepage/base_page.py:422: in get_present_element',
|
|
||||||
'return self._get(locator, expected_condition, params, timeout, error_msg, parent)',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/basepage/base_page.py:569: in _get',
|
|
||||||
'return WebDriverWait(_driver, timeout).until(exp_cond, error_msg)',
|
|
||||||
'_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ',
|
|
||||||
'',
|
|
||||||
'self = <selenium.webdriver.support.wait.WebDriverWait (session="2c586c44-7cb6-8e48-86fd-ae2d2982a3bd")>',
|
|
||||||
'method = <selenium.webdriver.support.expected_conditions.presence_of_element_located object at 0x7fe2cf1a7240>',
|
|
||||||
'message = "Element was never present!\nLocator of type <CSS_SELECTOR> with selector <.pp-rounded-btn__icon.category-bck-3> with ...nExpected condition: <class \'selenium.webdriver.support.expected_conditions.presence_of_element_located\'>\nTimeout: 30"',
|
|
||||||
'',
|
|
||||||
"def until(self, method, message=''):",
|
|
||||||
'"""Calls the method provided with the driver as an argument until the ',
|
|
||||||
'return value is not False."""',
|
|
||||||
'screen = None',
|
|
||||||
'stacktrace = None',
|
|
||||||
' ',
|
|
||||||
'end_time = time.time() + self._timeout',
|
|
||||||
'while True:',
|
|
||||||
'try:',
|
|
||||||
'value = method(self._driver)',
|
|
||||||
'if value:',
|
|
||||||
'return value',
|
|
||||||
'except self._ignored_exceptions as exc:',
|
|
||||||
"screen = getattr(exc, 'screen', None)",
|
|
||||||
"stacktrace = getattr(exc, 'stacktrace', None)",
|
|
||||||
'time.sleep(self._poll)',
|
|
||||||
'if time.time() > end_time:',
|
|
||||||
'break',
|
|
||||||
'> raise TimeoutException(message, screen, stacktrace)',
|
|
||||||
'E selenium.common.exceptions.TimeoutException: Message: Element was never present!',
|
|
||||||
'E Locator of type <CSS_SELECTOR> with selector <.pp-rounded-btn__icon.category-bck-3> with params <None>',
|
|
||||||
"E Expected condition: <class 'selenium.webdriver.support.expected_conditions.presence_of_element_located'>",
|
|
||||||
'E Timeout: 30',
|
|
||||||
'',
|
|
||||||
'/Users/vgustafsson/.virtualenvs/pytest/lib/python3.7/site-packages/selenium/webdriver/support/wait.py:80: TimeoutException',
|
|
||||||
'------------------------------- pytest-selenium --------------------------------',
|
|
||||||
'Driver log: /private/var/folders/nt/zg2hyyxj6k77sf_qk34xtqf40000gn/T/pytest-of-vgustafsson/pytest-12/test_dependency_forces_date_ch0/driver.log',
|
|
||||||
'URL: https://local.rnd.projectplace.com/#project/79788/plan/28838',
|
|
||||||
'WARNING: Failed to gather log types: Message: HTTP method not allowed',
|
|
||||||
'',
|
|
||||||
'----------------------------- Additional Reporting -----------------------------',
|
|
||||||
'Window size: width <1680>, height <983>',
|
|
||||||
"WARNING: Failed to get HAR: 'NoneType' object has no attribute 'har'",
|
|
||||||
'Logs can be found here: http://elk.rnd.projectplace.com/goto/152df50b1a44ad0',
|
|
||||||
],
|
|
||||||
reprfuncargs: { args: [] },
|
|
||||||
reprlocals: null,
|
|
||||||
reprfileloc: {
|
|
||||||
path: 'test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'AssertionError',
|
|
||||||
},
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extraline: null,
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
sections: [],
|
|
||||||
chain: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
reprentries: [
|
|
||||||
{
|
|
||||||
type: 'ReprEntry',
|
|
||||||
data: {
|
|
||||||
lines: [
|
|
||||||
' def test_url112():',
|
|
||||||
' """',
|
|
||||||
' bla bla bla bla',
|
|
||||||
' alb alb alb',
|
|
||||||
' @param: hello',
|
|
||||||
' :param just',
|
|
||||||
' """',
|
|
||||||
' # driver.get("https://www.google.com")',
|
|
||||||
'> assert False',
|
|
||||||
'E assert False',
|
|
||||||
],
|
|
||||||
reprfuncargs: { args: [] },
|
|
||||||
reprlocals: null,
|
|
||||||
reprfileloc: {
|
|
||||||
path: 'test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'AssertionError',
|
|
||||||
},
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extraline: null,
|
|
||||||
style: 'long',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path:
|
|
||||||
'/Users/jimbrannlund/dev/pytest-dev/testing-html/test_html.py',
|
|
||||||
lineno: 12,
|
|
||||||
message: 'assert False',
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
when: 'call',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.00020797699999997032,
|
|
||||||
extra: [],
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeid: 'test_html.py::test_url112',
|
|
||||||
location: ['test_html.py', 3, 'test_url112'],
|
|
||||||
keywords: { test_url112: 1, 'testing-html': 1, 'test_html.py': 1 },
|
|
||||||
outcome: 'passed',
|
|
||||||
longrepr: null,
|
|
||||||
when: 'teardown',
|
|
||||||
user_properties: [],
|
|
||||||
sections: [],
|
|
||||||
duration: 0.0001561270000000059,
|
|
||||||
$report_type: 'TestReport',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,246 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
|
|
||||||
function toArray(iter) {
|
|
||||||
if (iter === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Array.prototype.slice.call(iter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function find(selector, elem) { // eslint-disable-line no-redeclare
|
|
||||||
if (!elem) {
|
|
||||||
elem = document;
|
|
||||||
}
|
|
||||||
return elem.querySelector(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAll(selector, elem) {
|
|
||||||
if (!elem) {
|
|
||||||
elem = document;
|
|
||||||
}
|
|
||||||
return toArray(elem.querySelectorAll(selector));
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortColumn(elem) {
|
|
||||||
toggleSortStates(elem);
|
|
||||||
const colIndex = toArray(elem.parentNode.childNodes).indexOf(elem);
|
|
||||||
let key;
|
|
||||||
if (elem.classList.contains('result')) {
|
|
||||||
key = keyResult;
|
|
||||||
} else if (elem.classList.contains('links')) {
|
|
||||||
key = keyLink;
|
|
||||||
} else {
|
|
||||||
key = keyAlpha;
|
|
||||||
}
|
|
||||||
sortTable(elem, key(colIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAllExtras() { // eslint-disable-line no-unused-vars
|
|
||||||
findAll('.col-result').forEach(showExtras);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideAllExtras() { // eslint-disable-line no-unused-vars
|
|
||||||
findAll('.col-result').forEach(hideExtras);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showExtras(colresultElem) {
|
|
||||||
const extras = colresultElem.parentNode.nextElementSibling;
|
|
||||||
const expandcollapse = colresultElem.firstElementChild;
|
|
||||||
extras.classList.remove('collapsed');
|
|
||||||
expandcollapse.classList.remove('expander');
|
|
||||||
expandcollapse.classList.add('collapser');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideExtras(colresultElem) {
|
|
||||||
const extras = colresultElem.parentNode.nextElementSibling;
|
|
||||||
const expandcollapse = colresultElem.firstElementChild;
|
|
||||||
extras.classList.add('collapsed');
|
|
||||||
expandcollapse.classList.remove('collapser');
|
|
||||||
expandcollapse.classList.add('expander');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFilters() {
|
|
||||||
let visibleString = getQueryParameter('visible') || 'all';
|
|
||||||
visibleString = visibleString.toLowerCase();
|
|
||||||
const checkedItems = visibleString.split(',');
|
|
||||||
|
|
||||||
const filterItems = document.getElementsByClassName('filter');
|
|
||||||
for (let i = 0; i < filterItems.length; i++) {
|
|
||||||
filterItems[i].hidden = false;
|
|
||||||
|
|
||||||
if (visibleString != 'all') {
|
|
||||||
filterItems[i].checked = checkedItems.includes(filterItems[i].getAttribute('data-test-result'));
|
|
||||||
filterTable(filterItems[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCollapse() {
|
|
||||||
// Add links for show/hide all
|
|
||||||
const resulttable = find('table#results-table');
|
|
||||||
const showhideall = document.createElement('p');
|
|
||||||
showhideall.innerHTML = '<a href="javascript:showAllExtras()">Show all details</a> / ' +
|
|
||||||
'<a href="javascript:hideAllExtras()">Hide all details</a>';
|
|
||||||
resulttable.parentElement.insertBefore(showhideall, resulttable);
|
|
||||||
|
|
||||||
// Add show/hide link to each result
|
|
||||||
findAll('.col-result').forEach(function(elem) {
|
|
||||||
const collapsed = getQueryParameter('collapsed') || 'Passed';
|
|
||||||
const extras = elem.parentNode.nextElementSibling;
|
|
||||||
const expandcollapse = document.createElement('span');
|
|
||||||
if (extras.classList.contains('collapsed')) {
|
|
||||||
expandcollapse.classList.add('expander');
|
|
||||||
} else if (collapsed.includes(elem.innerHTML)) {
|
|
||||||
extras.classList.add('collapsed');
|
|
||||||
expandcollapse.classList.add('expander');
|
|
||||||
} else {
|
|
||||||
expandcollapse.classList.add('collapser');
|
|
||||||
}
|
|
||||||
elem.appendChild(expandcollapse);
|
|
||||||
|
|
||||||
elem.addEventListener('click', function(event) {
|
|
||||||
if (event.currentTarget.parentNode.nextElementSibling.classList.contains('collapsed')) {
|
|
||||||
showExtras(event.currentTarget);
|
|
||||||
} else {
|
|
||||||
hideExtras(event.currentTarget);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQueryParameter(name) {
|
|
||||||
const match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
|
|
||||||
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
function init () { // eslint-disable-line no-unused-vars
|
|
||||||
resetSortHeaders();
|
|
||||||
|
|
||||||
addCollapse();
|
|
||||||
|
|
||||||
showFilters();
|
|
||||||
|
|
||||||
sortColumn(find('.initial-sort'));
|
|
||||||
|
|
||||||
findAll('.sortable').forEach(function(elem) {
|
|
||||||
elem.addEventListener('click',
|
|
||||||
function() {
|
|
||||||
sortColumn(elem);
|
|
||||||
}, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortTable(clicked, keyFunc) {
|
|
||||||
const rows = findAll('.results-table-row');
|
|
||||||
const reversed = !clicked.classList.contains('asc');
|
|
||||||
const sortedRows = sort(rows, keyFunc, reversed);
|
|
||||||
/* Whole table is removed here because browsers acts much slower
|
|
||||||
* when appending existing elements.
|
|
||||||
*/
|
|
||||||
const thead = document.getElementById('results-table-head');
|
|
||||||
document.getElementById('results-table').remove();
|
|
||||||
const parent = document.createElement('table');
|
|
||||||
parent.id = 'results-table';
|
|
||||||
parent.appendChild(thead);
|
|
||||||
sortedRows.forEach(function(elem) {
|
|
||||||
parent.appendChild(elem);
|
|
||||||
});
|
|
||||||
document.getElementsByTagName('BODY')[0].appendChild(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sort(items, keyFunc, reversed) {
|
|
||||||
const sortArray = items.map(function(item, i) {
|
|
||||||
return [keyFunc(item), i];
|
|
||||||
});
|
|
||||||
|
|
||||||
sortArray.sort(function(a, b) {
|
|
||||||
const keyA = a[0];
|
|
||||||
const keyB = b[0];
|
|
||||||
|
|
||||||
if (keyA == keyB) return 0;
|
|
||||||
|
|
||||||
if (reversed) {
|
|
||||||
return keyA < keyB ? 1 : -1;
|
|
||||||
} else {
|
|
||||||
return keyA > keyB ? 1 : -1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortArray.map(function(item) {
|
|
||||||
const index = item[1];
|
|
||||||
return items[index];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyAlpha(colIndex) {
|
|
||||||
return function(elem) {
|
|
||||||
return elem.childNodes[1].childNodes[colIndex].firstChild.data.toLowerCase();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyLink(colIndex) {
|
|
||||||
return function(elem) {
|
|
||||||
const dataCell = elem.childNodes[1].childNodes[colIndex].firstChild;
|
|
||||||
return dataCell == null ? '' : dataCell.innerText.toLowerCase();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyResult(colIndex) {
|
|
||||||
return function(elem) {
|
|
||||||
const strings = ['Error', 'Failed', 'Rerun', 'XFailed', 'XPassed',
|
|
||||||
'Skipped', 'Passed'];
|
|
||||||
return strings.indexOf(elem.childNodes[1].childNodes[colIndex].firstChild.data);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetSortHeaders() {
|
|
||||||
findAll('.sort-icon').forEach(function(elem) {
|
|
||||||
elem.parentNode.removeChild(elem);
|
|
||||||
});
|
|
||||||
findAll('.sortable').forEach(function(elem) {
|
|
||||||
const icon = document.createElement('div');
|
|
||||||
icon.className = 'sort-icon';
|
|
||||||
icon.textContent = 'vvv';
|
|
||||||
elem.insertBefore(icon, elem.firstChild);
|
|
||||||
elem.classList.remove('desc', 'active');
|
|
||||||
elem.classList.add('asc', 'inactive');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSortStates(elem) {
|
|
||||||
//if active, toggle between asc and desc
|
|
||||||
if (elem.classList.contains('active')) {
|
|
||||||
elem.classList.toggle('asc');
|
|
||||||
elem.classList.toggle('desc');
|
|
||||||
}
|
|
||||||
|
|
||||||
//if inactive, reset all other functions and add ascending active
|
|
||||||
if (elem.classList.contains('inactive')) {
|
|
||||||
resetSortHeaders();
|
|
||||||
elem.classList.remove('inactive');
|
|
||||||
elem.classList.add('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAllRowsHidden(value) {
|
|
||||||
return value.hidden == false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterTable(elem) { // eslint-disable-line no-unused-vars
|
|
||||||
const outcomeAtt = 'data-test-result';
|
|
||||||
const outcome = elem.getAttribute(outcomeAtt);
|
|
||||||
const classOutcome = outcome + ' results-table-row';
|
|
||||||
const outcomeRows = document.getElementsByClassName(classOutcome);
|
|
||||||
|
|
||||||
for(let i = 0; i < outcomeRows.length; i++){
|
|
||||||
outcomeRows[i].hidden = !elem.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = findAll('.results-table-row').filter(isAllRowsHidden);
|
|
||||||
const allRowsHidden = rows.length == 0 ? true : false;
|
|
||||||
const notFoundMessage = document.getElementById('not-found-message');
|
|
||||||
notFoundMessage.hidden = !allRowsHidden;
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
const genericSort = (list, key, ascending) => {
|
|
||||||
const sorted = list.sort((a, b) =>
|
|
||||||
a[key] === b[key] ? 0 : a[key] === b[key] ? 1 : -1
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ascending) {
|
|
||||||
sorted.reverse();
|
|
||||||
}
|
|
||||||
return sorted;
|
|
||||||
};
|
|
|
@ -70,10 +70,6 @@ span.xpassed,
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-result {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-links__extra {
|
.col-links__extra {
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
|
@ -123,48 +119,70 @@ span.xpassed,
|
||||||
height: inherit;
|
height: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.image {
|
div.media {
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
float: right;
|
float: right;
|
||||||
height: 240px;
|
height: 240px;
|
||||||
margin-left: 5px;
|
margin: 0 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
div.image img {
|
|
||||||
width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.video {
|
.media-container {
|
||||||
border: 1px solid #e6e6e6;
|
display: grid;
|
||||||
float: right;
|
grid-template-columns: 25px auto 25px;
|
||||||
height: 240px;
|
align-items: center;
|
||||||
margin-left: 5px;
|
flex: 1 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 320px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
div.video video {
|
|
||||||
overflow: hidden;
|
.media-container__nav--right,
|
||||||
width: 320px;
|
.media-container__nav--left {
|
||||||
height: 240px;
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-container__viewport {
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
.media-container__viewport img,
|
||||||
|
.media-container__viewport video {
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media__name,
|
||||||
|
.media__counter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex: 0 0 25px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed {
|
.collapsed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expander::after {
|
.col-result {
|
||||||
content: " (show details)";
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.col-result:hover::after {
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapser::after {
|
.col-result.collapser:hover::after {
|
||||||
content: " (hide details)";
|
content: " (hide details)";
|
||||||
color: #bbb;
|
}
|
||||||
font-style: italic;
|
|
||||||
cursor: pointer;
|
.col-result.expander:hover::after {
|
||||||
|
content: " (show details)";
|
||||||
}
|
}
|
||||||
|
|
||||||
/*------------------
|
/*------------------
|
||||||
|
@ -198,9 +216,6 @@ div.video video {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.summary__data {
|
.summary__data {
|
||||||
flex: 0 0 550px;
|
flex: 0 0 550px;
|
||||||
}
|
}
|
||||||
|
@ -228,6 +243,29 @@ div.video video {
|
||||||
flex: 0 0 550px;
|
flex: 0 0 550px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.filter {
|
.controls {
|
||||||
margin-left: 10px;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters,
|
||||||
|
.collapse {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.filters button,
|
||||||
|
.collapse button {
|
||||||
|
color: #999;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.filters button:hover,
|
||||||
|
.collapse button:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__label {
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,287 +0,0 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import warnings
|
|
||||||
from base64 import b64decode
|
|
||||||
from base64 import b64encode
|
|
||||||
from html import escape
|
|
||||||
from os.path import isfile
|
|
||||||
|
|
||||||
from _pytest.logging import _remove_ansi_escape_sequences
|
|
||||||
from py.xml import html
|
|
||||||
from py.xml import raw
|
|
||||||
|
|
||||||
from . import extras
|
|
||||||
from .util import ansi_support
|
|
||||||
|
|
||||||
|
|
||||||
class TestResult:
|
|
||||||
def __init__(self, outcome, report, logfile, config):
|
|
||||||
self.test_id = report.nodeid.encode("utf-8").decode("unicode_escape")
|
|
||||||
if getattr(report, "when", "call") != "call":
|
|
||||||
self.test_id = "::".join([report.nodeid, report.when])
|
|
||||||
self.time = getattr(report, "duration", 0.0)
|
|
||||||
self.formatted_time = self._format_time(report)
|
|
||||||
self.outcome = outcome
|
|
||||||
self.additional_html = []
|
|
||||||
self.links_html = []
|
|
||||||
self.self_contained = config.getoption("self_contained_html")
|
|
||||||
self.max_asset_filename_length = int(config.getini("max_asset_filename_length"))
|
|
||||||
self.logfile = logfile
|
|
||||||
self.config = config
|
|
||||||
self.row_table = self.row_extra = None
|
|
||||||
|
|
||||||
test_index = hasattr(report, "rerun") and report.rerun + 1 or 0
|
|
||||||
|
|
||||||
for extra_index, extra in enumerate(getattr(report, "extra", [])):
|
|
||||||
self.append_extra_html(extra, extra_index, test_index)
|
|
||||||
|
|
||||||
self.append_log_html(
|
|
||||||
report,
|
|
||||||
self.additional_html,
|
|
||||||
config.option.capture,
|
|
||||||
config.option.showcapture,
|
|
||||||
)
|
|
||||||
|
|
||||||
cells = [
|
|
||||||
html.td(self.outcome, class_="col-result"),
|
|
||||||
html.td(self.test_id, class_="col-name"),
|
|
||||||
html.td(self.formatted_time, class_="col-duration"),
|
|
||||||
html.td(self.links_html, class_="col-links"),
|
|
||||||
]
|
|
||||||
|
|
||||||
self.config.hook.pytest_html_results_table_row(report=report, cells=cells)
|
|
||||||
|
|
||||||
self.config.hook.pytest_html_results_table_html(
|
|
||||||
report=report, data=self.additional_html
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(cells) > 0:
|
|
||||||
tr_class = None
|
|
||||||
if self.config.getini("render_collapsed"):
|
|
||||||
tr_class = "collapsed"
|
|
||||||
self.row_table = html.tr(cells)
|
|
||||||
self.row_extra = html.tr(
|
|
||||||
html.td(self.additional_html, class_="extra", colspan=len(cells)),
|
|
||||||
class_=tr_class,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
order = (
|
|
||||||
"Error",
|
|
||||||
"Failed",
|
|
||||||
"Rerun",
|
|
||||||
"XFailed",
|
|
||||||
"XPassed",
|
|
||||||
"Skipped",
|
|
||||||
"Passed",
|
|
||||||
)
|
|
||||||
return order.index(self.outcome) < order.index(other.outcome)
|
|
||||||
|
|
||||||
def create_asset(self, content, extra_index, test_index, file_extension, mode="w"):
|
|
||||||
asset_file_name = "{}_{}_{}.{}".format(
|
|
||||||
re.sub(r"[^\w\.]", "_", self.test_id),
|
|
||||||
str(extra_index),
|
|
||||||
str(test_index),
|
|
||||||
file_extension,
|
|
||||||
)[-self.max_asset_filename_length :]
|
|
||||||
asset_path = os.path.join(
|
|
||||||
os.path.dirname(self.logfile), "assets", asset_file_name
|
|
||||||
)
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(asset_path), exist_ok=True)
|
|
||||||
|
|
||||||
relative_path = f"assets/{asset_file_name}"
|
|
||||||
|
|
||||||
kwargs = {"encoding": "utf-8"} if "b" not in mode else {}
|
|
||||||
with open(asset_path, mode, **kwargs) as f:
|
|
||||||
f.write(content)
|
|
||||||
return relative_path
|
|
||||||
|
|
||||||
def append_extra_html(self, extra, extra_index, test_index):
|
|
||||||
href = None
|
|
||||||
if extra.get("format_type") == extras.FORMAT_IMAGE:
|
|
||||||
self._append_image(extra, extra_index, test_index)
|
|
||||||
|
|
||||||
elif extra.get("format_type") == extras.FORMAT_HTML:
|
|
||||||
self.additional_html.append(html.div(raw(extra.get("content"))))
|
|
||||||
|
|
||||||
elif extra.get("format_type") == extras.FORMAT_JSON:
|
|
||||||
content = json.dumps(extra.get("content"))
|
|
||||||
if self.self_contained:
|
|
||||||
href = self._data_uri(content, mime_type=extra.get("mime_type"))
|
|
||||||
else:
|
|
||||||
href = self.create_asset(
|
|
||||||
content, extra_index, test_index, extra.get("extension")
|
|
||||||
)
|
|
||||||
|
|
||||||
elif extra.get("format_type") == extras.FORMAT_TEXT:
|
|
||||||
content = extra.get("content")
|
|
||||||
if isinstance(content, bytes):
|
|
||||||
content = content.decode("utf-8")
|
|
||||||
if self.self_contained:
|
|
||||||
href = self._data_uri(content)
|
|
||||||
else:
|
|
||||||
href = self.create_asset(
|
|
||||||
content, extra_index, test_index, extra.get("extension")
|
|
||||||
)
|
|
||||||
|
|
||||||
elif extra.get("format_type") == extras.FORMAT_URL:
|
|
||||||
href = extra.get("content")
|
|
||||||
|
|
||||||
elif extra.get("format_type") == extras.FORMAT_VIDEO:
|
|
||||||
self._append_video(extra, extra_index, test_index)
|
|
||||||
|
|
||||||
if href is not None:
|
|
||||||
self.links_html.append(
|
|
||||||
html.a(
|
|
||||||
extra.get("name"),
|
|
||||||
class_=extra.get("format_type"),
|
|
||||||
href=href,
|
|
||||||
target="_blank",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.links_html.append(" ")
|
|
||||||
|
|
||||||
def _format_time(self, report):
|
|
||||||
# parse the report duration into its display version and return
|
|
||||||
# it to the caller
|
|
||||||
duration = getattr(report, "duration", None)
|
|
||||||
if duration is None:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
duration_formatter = getattr(report, "duration_formatter", None)
|
|
||||||
string_duration = str(duration)
|
|
||||||
if duration_formatter is None:
|
|
||||||
if "." in string_duration:
|
|
||||||
split_duration = string_duration.split(".")
|
|
||||||
split_duration[1] = split_duration[1][0:2]
|
|
||||||
|
|
||||||
string_duration = ".".join(split_duration)
|
|
||||||
|
|
||||||
return string_duration
|
|
||||||
else:
|
|
||||||
# support %f, since time.strftime doesn't support it out of the box
|
|
||||||
# keep a precision of 2 for legacy reasons
|
|
||||||
formatted_milliseconds = "00"
|
|
||||||
if "." in string_duration:
|
|
||||||
milliseconds = string_duration.split(".")[1]
|
|
||||||
formatted_milliseconds = milliseconds[0:2]
|
|
||||||
|
|
||||||
duration_formatter = duration_formatter.replace(
|
|
||||||
"%f", formatted_milliseconds
|
|
||||||
)
|
|
||||||
duration_as_gmtime = time.gmtime(report.duration)
|
|
||||||
return time.strftime(duration_formatter, duration_as_gmtime)
|
|
||||||
|
|
||||||
def _populate_html_log_div(self, log, report):
|
|
||||||
if report.longrepr:
|
|
||||||
# longreprtext is only filled out on failure by pytest
|
|
||||||
# otherwise will be None.
|
|
||||||
# Use full_text if longreprtext is None-ish
|
|
||||||
# we added full_text elsewhere in this file.
|
|
||||||
text = report.longreprtext or report.full_text
|
|
||||||
for line in text.splitlines():
|
|
||||||
separator = line.startswith("_ " * 10)
|
|
||||||
if separator:
|
|
||||||
log.append(line[:80])
|
|
||||||
else:
|
|
||||||
exception = line.startswith("E ")
|
|
||||||
if exception:
|
|
||||||
log.append(html.span(raw(escape(line)), class_="error"))
|
|
||||||
else:
|
|
||||||
log.append(raw(escape(line)))
|
|
||||||
log.append(html.br())
|
|
||||||
|
|
||||||
for section in report.sections:
|
|
||||||
header, content = map(escape, section)
|
|
||||||
log.append(f" {header:-^80} ")
|
|
||||||
log.append(html.br())
|
|
||||||
|
|
||||||
if ansi_support():
|
|
||||||
converter = ansi_support().Ansi2HTMLConverter(
|
|
||||||
inline=False, escaped=False
|
|
||||||
)
|
|
||||||
content = converter.convert(content, full=False)
|
|
||||||
else:
|
|
||||||
content = _remove_ansi_escape_sequences(content)
|
|
||||||
|
|
||||||
log.append(raw(content))
|
|
||||||
log.append(html.br())
|
|
||||||
|
|
||||||
def append_log_html(
|
|
||||||
self,
|
|
||||||
report,
|
|
||||||
additional_html,
|
|
||||||
pytest_capture_value,
|
|
||||||
pytest_show_capture_value,
|
|
||||||
):
|
|
||||||
log = html.div(class_="log")
|
|
||||||
|
|
||||||
should_skip_captured_output = pytest_capture_value == "no"
|
|
||||||
if report.outcome == "failed" and not should_skip_captured_output:
|
|
||||||
should_skip_captured_output = pytest_show_capture_value == "no"
|
|
||||||
if not should_skip_captured_output:
|
|
||||||
self._populate_html_log_div(log, report)
|
|
||||||
|
|
||||||
if len(log) == 0:
|
|
||||||
log = html.div(class_="empty log")
|
|
||||||
log.append("No log output captured.")
|
|
||||||
|
|
||||||
additional_html.append(log)
|
|
||||||
|
|
||||||
def _make_media_html_div(
|
|
||||||
self, extra, extra_index, test_index, base_extra_string, base_extra_class
|
|
||||||
):
|
|
||||||
content = extra.get("content")
|
|
||||||
try:
|
|
||||||
is_uri_or_path = content.startswith(("file", "http")) or isfile(content)
|
|
||||||
except ValueError:
|
|
||||||
# On Windows, os.path.isfile throws this exception when
|
|
||||||
# passed a b64 encoded image.
|
|
||||||
is_uri_or_path = False
|
|
||||||
if is_uri_or_path:
|
|
||||||
if self.self_contained:
|
|
||||||
warnings.warn(
|
|
||||||
"Self-contained HTML report "
|
|
||||||
"includes link to external "
|
|
||||||
f"resource: {content}"
|
|
||||||
)
|
|
||||||
|
|
||||||
html_div = html.a(
|
|
||||||
raw(base_extra_string.format(extra.get("content"))), href=content
|
|
||||||
)
|
|
||||||
elif self.self_contained:
|
|
||||||
src = f"data:{extra.get('mime_type')};base64,{content}"
|
|
||||||
html_div = raw(base_extra_string.format(src))
|
|
||||||
else:
|
|
||||||
content = b64decode(content.encode("utf-8"))
|
|
||||||
href = src = self.create_asset(
|
|
||||||
content, extra_index, test_index, extra.get("extension"), "wb"
|
|
||||||
)
|
|
||||||
html_div = html.a(
|
|
||||||
raw(base_extra_string.format(src)),
|
|
||||||
class_=base_extra_class,
|
|
||||||
target="_blank",
|
|
||||||
href=href,
|
|
||||||
)
|
|
||||||
return html_div
|
|
||||||
|
|
||||||
def _append_image(self, extra, extra_index, test_index):
|
|
||||||
image_base = '<img src="{}"/>'
|
|
||||||
html_div = self._make_media_html_div(
|
|
||||||
extra, extra_index, test_index, image_base, "image"
|
|
||||||
)
|
|
||||||
self.additional_html.append(html.div(html_div, class_="image"))
|
|
||||||
|
|
||||||
def _append_video(self, extra, extra_index, test_index):
|
|
||||||
video_base = '<video controls><source src="{}" type="video/mp4"></video>'
|
|
||||||
html_div = self._make_media_html_div(
|
|
||||||
extra, extra_index, test_index, video_base, "video"
|
|
||||||
)
|
|
||||||
self.additional_html.append(html.div(html_div, class_="video"))
|
|
||||||
|
|
||||||
def _data_uri(self, content, mime_type="text/plain", charset="utf-8"):
|
|
||||||
data = b64encode(content.encode(charset)).decode("ascii")
|
|
||||||
return f"data:{mime_type};charset={charset};base64,{data}"
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
const { getCollapsedCategory } = require('./storage.js')
|
||||||
|
|
||||||
|
class DataManager {
|
||||||
|
setManager(data) {
|
||||||
|
const collapsedCategories = [...getCollapsedCategory(), 'passed']
|
||||||
|
const dataBlob = { ...data, tests: data.tests.map((test, index) => ({
|
||||||
|
...test,
|
||||||
|
id: `test_${index}`,
|
||||||
|
collapsed: collapsedCategories.includes(test.outcome.toLowerCase()),
|
||||||
|
})) }
|
||||||
|
this.data = { ...dataBlob }
|
||||||
|
this.renderData = { ...dataBlob }
|
||||||
|
}
|
||||||
|
|
||||||
|
get allData() {
|
||||||
|
return { ...this.data }
|
||||||
|
}
|
||||||
|
resetRender() {
|
||||||
|
this.renderData = { ...this.data }
|
||||||
|
}
|
||||||
|
setRender(data) {
|
||||||
|
this.renderData.tests = [...data]
|
||||||
|
}
|
||||||
|
toggleCollapsedItem(id) {
|
||||||
|
this.renderData.tests = this.renderData.tests.map((test) =>
|
||||||
|
test.id === id ? { ...test, collapsed: !test.collapsed } : test,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
set allCollapsed(collapsed) {
|
||||||
|
this.renderData = { ...this.renderData, tests: [...this.renderData.tests.map((test) => (
|
||||||
|
{ ...test, collapsed }
|
||||||
|
))] }
|
||||||
|
}
|
||||||
|
|
||||||
|
get testSubset() {
|
||||||
|
return [...this.renderData.tests]
|
||||||
|
}
|
||||||
|
get allTests() {
|
||||||
|
return [...this.data.tests]
|
||||||
|
}
|
||||||
|
get title() {
|
||||||
|
return this.renderData.title
|
||||||
|
}
|
||||||
|
get environment() {
|
||||||
|
return this.renderData.environment
|
||||||
|
}
|
||||||
|
get collectedItems() {
|
||||||
|
return this.renderData.collectedItems
|
||||||
|
}
|
||||||
|
get isFinished() {
|
||||||
|
return this.data.runningState === 'Finished'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
manager: new DataManager(),
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
const storageModule = require('./storage.js')
|
||||||
|
const { formatDuration } = require('./utils.js')
|
||||||
|
const mediaViewer = require('./mediaviewer.js')
|
||||||
|
const templateEnvRow = document.querySelector('#template_environment_row')
|
||||||
|
const templateCollGroup = document.querySelector('#template_table-colgroup')
|
||||||
|
const templateResult = document.querySelector('#template_results-table__tbody')
|
||||||
|
const aTag = document.querySelector('#template_a')
|
||||||
|
const listHeader = document.querySelector('#template_results-table__head')
|
||||||
|
const listHeaderEmpty = document.querySelector('#template_results-table__head--empty')
|
||||||
|
|
||||||
|
function htmlToElements(html) {
|
||||||
|
const temp = document.createElement('template')
|
||||||
|
temp.innerHTML = html
|
||||||
|
return temp.content.childNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
const find = (selector, elem) => {
|
||||||
|
if (!elem) {
|
||||||
|
elem = document
|
||||||
|
}
|
||||||
|
return elem.querySelector(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
const findAll = (selector, elem) => {
|
||||||
|
if (!elem) {
|
||||||
|
elem = document
|
||||||
|
}
|
||||||
|
return [...elem.querySelectorAll(selector)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertAdditionalHTML = (html, element, selector) => {
|
||||||
|
Object.keys(html).map((key) => {
|
||||||
|
element.querySelectorAll(selector).item(key).insertAdjacentHTML('beforebegin', html[key])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const dom = {
|
||||||
|
getStaticRow: (key, value) => {
|
||||||
|
const envRow = templateEnvRow.content.cloneNode(true)
|
||||||
|
const isObj = typeof value === 'object' && value !== null
|
||||||
|
const values = isObj ? Object.keys(value).map((k) => `${k}: ${value[k]}`) : null
|
||||||
|
|
||||||
|
const valuesElement = htmlToElements(
|
||||||
|
values ? `<ul>${values.map((val) => `<li>${val}</li>`).join('')}<ul>` : `<div>${value}</div>`)[0]
|
||||||
|
const td = findAll('td', envRow)
|
||||||
|
td[0].textContent = key
|
||||||
|
td[1].appendChild(valuesElement)
|
||||||
|
|
||||||
|
return envRow
|
||||||
|
},
|
||||||
|
getListHeader: ({ resultsTableHeader }) => {
|
||||||
|
const header = listHeader.content.cloneNode(true)
|
||||||
|
const sortAttr = storageModule.getSort()
|
||||||
|
const sortAsc = JSON.parse(storageModule.getSortDirection())
|
||||||
|
const sortables = ['outcome', 'nodeid', 'duration']
|
||||||
|
|
||||||
|
sortables.forEach((sortCol) => {
|
||||||
|
if (sortCol === sortAttr) {
|
||||||
|
header.querySelector(`[data-column-type="${sortCol}"]`).classList.add(sortAsc ? 'desc' : 'asc')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add custom html from the pytest_html_results_table_header hook
|
||||||
|
insertAdditionalHTML(resultsTableHeader, header, 'th')
|
||||||
|
|
||||||
|
return header
|
||||||
|
},
|
||||||
|
getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true),
|
||||||
|
getColGroup: () => templateCollGroup.content.cloneNode(true),
|
||||||
|
getResultTBody: ({ nodeid, id, longreprtext, duration, extras, resultsTableRow, tableHtml, outcome, collapsed }) => {
|
||||||
|
const outcomeLower = outcome.toLowerCase()
|
||||||
|
let formattedDuration = formatDuration(duration)
|
||||||
|
formattedDuration = formatDuration < 1 ? formattedDuration.ms : formattedDuration.formatted
|
||||||
|
const resultBody = templateResult.content.cloneNode(true)
|
||||||
|
resultBody.querySelector('tbody').classList.add(outcomeLower)
|
||||||
|
resultBody.querySelector('.col-result').innerText = outcome
|
||||||
|
resultBody.querySelector('.col-result').classList.add(`${collapsed ? 'expander' : 'collapser'}`)
|
||||||
|
resultBody.querySelector('.col-result').dataset.id = id
|
||||||
|
resultBody.querySelector('.col-name').innerText = nodeid
|
||||||
|
|
||||||
|
resultBody.querySelector('.col-duration').innerText = duration < 1 ? formatDuration(duration).ms : formatDuration(duration).formatted
|
||||||
|
|
||||||
|
|
||||||
|
if (longreprtext) {
|
||||||
|
// resultBody.querySelector('.log').innerText = longreprtext
|
||||||
|
resultBody.querySelector('.log').innerHTML = longreprtext
|
||||||
|
}
|
||||||
|
// if (collapsed || !longreprtext) {
|
||||||
|
if (collapsed) {
|
||||||
|
resultBody.querySelector('.extras-row').classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = []
|
||||||
|
extras?.forEach(({ name, format_type, content }) => {
|
||||||
|
if (['json', 'text', 'url'].includes(format_type)) {
|
||||||
|
const extraLink = aTag.content.cloneNode(true)
|
||||||
|
const extraLinkItem = extraLink.querySelector('a')
|
||||||
|
|
||||||
|
extraLinkItem.href = content
|
||||||
|
extraLinkItem.className = `col-links__extra ${format_type}`
|
||||||
|
extraLinkItem.innerText = name
|
||||||
|
resultBody.querySelector('.col-links').appendChild(extraLinkItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['image', 'video'].includes(format_type)) {
|
||||||
|
media.push({ path: content, name, format_type })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format_type === 'html') {
|
||||||
|
resultBody.querySelector('.extraHTML').insertAdjacentHTML('beforeend', `<div>${content}</div>`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mediaViewer.setUp(resultBody, media)
|
||||||
|
|
||||||
|
// Add custom html from the pytest_html_results_table_row hook
|
||||||
|
resultsTableRow && insertAdditionalHTML(resultsTableRow, resultBody, 'td')
|
||||||
|
|
||||||
|
// Add custom html from the pytest_html_results_table_html hook
|
||||||
|
tableHtml?.forEach((item) => {
|
||||||
|
resultBody.querySelector('td[class="extra"]').insertAdjacentHTML('beforeend', item)
|
||||||
|
})
|
||||||
|
|
||||||
|
return resultBody
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.dom = dom
|
||||||
|
exports.htmlToElements = htmlToElements
|
||||||
|
exports.find = find
|
||||||
|
exports.findAll = findAll
|
|
@ -0,0 +1,33 @@
|
||||||
|
const { manager } = require('./datamanager.js')
|
||||||
|
const storageModule = require('./storage.js')
|
||||||
|
|
||||||
|
const getFilteredSubSet = (filter) =>
|
||||||
|
manager.allData.tests.filter(({ outcome }) => filter.includes(outcome.toLowerCase()))
|
||||||
|
|
||||||
|
const doInitFilter = () => {
|
||||||
|
const currentFilter = storageModule.getVisible()
|
||||||
|
const filteredSubset = getFilteredSubSet(currentFilter)
|
||||||
|
manager.setRender(filteredSubset)
|
||||||
|
}
|
||||||
|
|
||||||
|
const doFilter = (type, show) => {
|
||||||
|
if (show) {
|
||||||
|
storageModule.showCategory(type)
|
||||||
|
} else {
|
||||||
|
storageModule.hideCategory(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFilter = storageModule.getVisible()
|
||||||
|
|
||||||
|
if (currentFilter.length) {
|
||||||
|
const filteredSubset = getFilteredSubSet(currentFilter)
|
||||||
|
manager.setRender(filteredSubset)
|
||||||
|
} else {
|
||||||
|
manager.resetRender()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
doFilter,
|
||||||
|
doInitFilter,
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
const { redraw, bindEvents } = require('./main.js')
|
||||||
|
const { doInitFilter } = require('./filter.js')
|
||||||
|
const { doInitSort } = require('./sort.js')
|
||||||
|
const { manager } = require('./datamanager.js')
|
||||||
|
const data = JSON.parse(document.querySelector('#data-container').dataset.jsonblob)
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
manager.setManager(data)
|
||||||
|
doInitFilter()
|
||||||
|
doInitSort()
|
||||||
|
redraw()
|
||||||
|
bindEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
|
@ -0,0 +1,134 @@
|
||||||
|
const { formatDuration } = require('./utils.js')
|
||||||
|
const { dom, findAll } = require('./dom.js')
|
||||||
|
const { manager } = require('./datamanager.js')
|
||||||
|
const { doSort } = require('./sort.js')
|
||||||
|
const { doFilter } = require('./filter.js')
|
||||||
|
const { getVisible } = require('./storage.js')
|
||||||
|
|
||||||
|
const removeChildren = (node) => {
|
||||||
|
while (node.firstChild) {
|
||||||
|
node.removeChild(node.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderStatic = () => {
|
||||||
|
const renderTitle = () => {
|
||||||
|
const title = manager.title
|
||||||
|
document.querySelector('#title').innerText = title
|
||||||
|
document.querySelector('#head-title').innerText = title
|
||||||
|
}
|
||||||
|
const renderTable = () => {
|
||||||
|
const environment = manager.environment
|
||||||
|
const rows = Object.keys(environment).map((key) => dom.getStaticRow(key, environment[key]))
|
||||||
|
const table = document.querySelector('#environment')
|
||||||
|
removeChildren(table)
|
||||||
|
rows.forEach((row) => table.appendChild(row))
|
||||||
|
}
|
||||||
|
renderTitle()
|
||||||
|
renderTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderContent = (tests) => {
|
||||||
|
const renderSet = tests.filter(({ when, outcome }) => when === 'call' || outcome === 'Error' )
|
||||||
|
const rows = renderSet.map(dom.getResultTBody)
|
||||||
|
const table = document.querySelector('#results-table')
|
||||||
|
removeChildren(table)
|
||||||
|
const tableHeader = dom.getListHeader(manager.renderData)
|
||||||
|
if (!rows.length) {
|
||||||
|
tableHeader.appendChild(dom.getListHeaderEmpty())
|
||||||
|
}
|
||||||
|
table.appendChild(dom.getColGroup())
|
||||||
|
table.appendChild(tableHeader)
|
||||||
|
|
||||||
|
rows.forEach((row) => !!row && table.appendChild(row))
|
||||||
|
|
||||||
|
table.querySelectorAll('.extra').forEach((item) => {
|
||||||
|
item.colSpan = document.querySelectorAll('th').length
|
||||||
|
})
|
||||||
|
findAll('.sortable').forEach((elem) => {
|
||||||
|
elem.addEventListener('click', (evt) => {
|
||||||
|
const { target: element } = evt
|
||||||
|
const { columnType } = element.dataset
|
||||||
|
doSort(columnType)
|
||||||
|
redraw()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
findAll('.col-result').forEach((elem) => {
|
||||||
|
elem.addEventListener('click', ({ target }) => {
|
||||||
|
manager.toggleCollapsedItem(target.dataset.id)
|
||||||
|
redraw()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderDerived = (tests, collectedItems, isFinished) => {
|
||||||
|
const renderSet = tests.filter(({ when, outcome }) => when === 'call' || outcome === 'Error')
|
||||||
|
|
||||||
|
const possibleOutcomes = [
|
||||||
|
{ outcome: 'passed', label: 'Passed' },
|
||||||
|
{ outcome: 'skipped', label: 'Skipped' },
|
||||||
|
{ outcome: 'failed', label: 'Failed' },
|
||||||
|
{ outcome: 'error', label: 'Errors' },
|
||||||
|
{ outcome: 'xfailed', label: 'Unexpected failures' },
|
||||||
|
{ outcome: 'xpassed', label: 'Unexpected passes' },
|
||||||
|
{ outcome: 'rerun', label: 'Reruns' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentFilter = getVisible()
|
||||||
|
possibleOutcomes.forEach(({ outcome, label }) => {
|
||||||
|
const count = renderSet.filter((test) => test.outcome.toLowerCase() === outcome).length
|
||||||
|
const input = document.querySelector(`input[data-test-result="${outcome}"]`)
|
||||||
|
document.querySelector(`.${outcome}`).innerText = `${count} ${label}`
|
||||||
|
|
||||||
|
input.disabled = !count
|
||||||
|
input.checked = currentFilter.includes(outcome)
|
||||||
|
})
|
||||||
|
|
||||||
|
const numberOfTests = renderSet.filter(({ outcome }) =>
|
||||||
|
['Passed', 'Failed', 'XPassed', 'XFailed'].includes(outcome)).length
|
||||||
|
|
||||||
|
if (isFinished) {
|
||||||
|
const accTime = tests.reduce((prev, { duration }) => prev + duration, 0)
|
||||||
|
const formattedAccTime = formatDuration(accTime)
|
||||||
|
const testWord = numberOfTests > 1 ? 'tests' : 'test'
|
||||||
|
const durationText = formattedAccTime.hasOwnProperty('ms') ? formattedAccTime.ms : formattedAccTime.seconds
|
||||||
|
|
||||||
|
document.querySelector('.run-count').innerText = `${numberOfTests} ${testWord} ran in ${durationText}.`
|
||||||
|
document.querySelector('.summary__reload__button').classList.add('hidden')
|
||||||
|
} else {
|
||||||
|
document.querySelector('.run-count').innerText = `${numberOfTests} / ${collectedItems} tests done`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindEvents = () => {
|
||||||
|
const filterColumn = (evt) => {
|
||||||
|
const { target: element } = evt
|
||||||
|
const { testResult } = element.dataset
|
||||||
|
|
||||||
|
doFilter(testResult, element.checked)
|
||||||
|
redraw()
|
||||||
|
}
|
||||||
|
findAll('input[name="filter_checkbox"]').forEach((elem) => {
|
||||||
|
elem.removeEventListener('click', filterColumn)
|
||||||
|
elem.addEventListener('click', filterColumn)
|
||||||
|
})
|
||||||
|
document.querySelector('#show_all_details').addEventListener('click', () => {
|
||||||
|
manager.allCollapsed = false
|
||||||
|
redraw()
|
||||||
|
})
|
||||||
|
document.querySelector('#hide_all_details').addEventListener('click', () => {
|
||||||
|
manager.allCollapsed = true
|
||||||
|
redraw()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const redraw = () => {
|
||||||
|
const { testSubset, allTests, collectedItems, isFinished } = manager
|
||||||
|
|
||||||
|
renderStatic()
|
||||||
|
renderContent(testSubset)
|
||||||
|
renderDerived(allTests, collectedItems, isFinished)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.redraw = redraw
|
||||||
|
exports.bindEvents = bindEvents
|
|
@ -0,0 +1,74 @@
|
||||||
|
class MediaViewer {
|
||||||
|
constructor(assets) {
|
||||||
|
this.assets = assets
|
||||||
|
this.index = 0
|
||||||
|
}
|
||||||
|
nextActive() {
|
||||||
|
this.index = this.index === this.assets.length - 1 ? 0 : this.index + 1
|
||||||
|
return [this.activeFile, this.index]
|
||||||
|
}
|
||||||
|
prevActive() {
|
||||||
|
this.index = this.index === 0 ? this.assets.length - 1 : this.index -1
|
||||||
|
return [this.activeFile, this.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentIndex() {
|
||||||
|
return this.index
|
||||||
|
}
|
||||||
|
get activeFile() {
|
||||||
|
return this.assets[this.index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const setUp = (resultBody, assets) => {
|
||||||
|
if (!assets.length) {
|
||||||
|
resultBody.querySelector('.media').classList.add('hidden')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaViewer = new MediaViewer(assets)
|
||||||
|
const leftArrow = resultBody.querySelector('.media-container__nav--left')
|
||||||
|
const rightArrow = resultBody.querySelector('.media-container__nav--right')
|
||||||
|
const mediaName = resultBody.querySelector('.media__name')
|
||||||
|
const counter = resultBody.querySelector('.media__counter')
|
||||||
|
const imageEl = resultBody.querySelector('img')
|
||||||
|
const sourceEl = resultBody.querySelector('source')
|
||||||
|
const videoEl = resultBody.querySelector('video')
|
||||||
|
|
||||||
|
const setImg = (media, index) => {
|
||||||
|
if (media?.format_type === 'image') {
|
||||||
|
imageEl.src = media.path
|
||||||
|
|
||||||
|
imageEl.classList.remove('hidden')
|
||||||
|
videoEl.classList.add('hidden')
|
||||||
|
} else if (media?.format_type === 'video') {
|
||||||
|
sourceEl.src = media.path
|
||||||
|
|
||||||
|
videoEl.classList.remove('hidden')
|
||||||
|
imageEl.classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaName.innerText = media?.name
|
||||||
|
counter.innerText = `${index + 1} / ${assets.length}`
|
||||||
|
}
|
||||||
|
setImg(mediaViewer.activeFile, mediaViewer.currentIndex)
|
||||||
|
|
||||||
|
const moveLeft = () => {
|
||||||
|
const [media, index] = mediaViewer.prevActive()
|
||||||
|
setImg(media, index)
|
||||||
|
}
|
||||||
|
const doRight = () => {
|
||||||
|
const [media, index] = mediaViewer.nextActive()
|
||||||
|
setImg(media, index)
|
||||||
|
}
|
||||||
|
const openImg = () => {
|
||||||
|
window.open(mediaViewer.activeFile.path, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
leftArrow.addEventListener('click', moveLeft)
|
||||||
|
rightArrow.addEventListener('click', doRight)
|
||||||
|
imageEl.addEventListener('click', openImg)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setUp = setUp
|
|
@ -0,0 +1,34 @@
|
||||||
|
const { manager } = require('./datamanager.js')
|
||||||
|
const storageModule = require('./storage.js')
|
||||||
|
|
||||||
|
const genericSort = (list, key, ascending) => {
|
||||||
|
const sorted = list.sort((a, b) => a[key] === b[key] ? 0 : a[key] > b[key] ? 1 : -1)
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
sorted.reverse()
|
||||||
|
}
|
||||||
|
return sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
const doInitSort = () => {
|
||||||
|
const type = storageModule.getSort()
|
||||||
|
const ascending = storageModule.getSortDirection()
|
||||||
|
const list = manager.testSubset
|
||||||
|
const sortedList = genericSort(list, type, ascending)
|
||||||
|
manager.setRender(sortedList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const doSort = (type) => {
|
||||||
|
const newSortType = storageModule.getSort() !== type
|
||||||
|
const currentAsc = storageModule.getSortDirection()
|
||||||
|
const ascending = newSortType ? true : !currentAsc
|
||||||
|
storageModule.setSort(type)
|
||||||
|
storageModule.setSortDirection(ascending)
|
||||||
|
const list = manager.testSubset
|
||||||
|
|
||||||
|
const sortedList = genericSort(list, type, ascending)
|
||||||
|
manager.setRender(sortedList)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.doSort = doSort
|
||||||
|
exports.doInitSort = doInitSort
|
|
@ -0,0 +1,78 @@
|
||||||
|
const possibleFiltes = ['passed', 'skipped', 'failed', 'error', 'xfailed', 'xpassed', 'rerun']
|
||||||
|
|
||||||
|
const getVisible = () => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const settings = new URLSearchParams(url.search).get('visible') || ''
|
||||||
|
return settings ?
|
||||||
|
[...new Set(settings.split(',').filter((filter) => possibleFiltes.includes(filter)))] : possibleFiltes
|
||||||
|
}
|
||||||
|
const hideCategory = (categoryToHide) => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const visibleParams = new URLSearchParams(url.search).get('visible')
|
||||||
|
const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFiltes]
|
||||||
|
const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',')
|
||||||
|
|
||||||
|
url.searchParams.set('visible', settings)
|
||||||
|
history.pushState({}, null, unescape(url.href))
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCategory = (categoryToShow) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',') || [...possibleFiltes]
|
||||||
|
const settings = [...new Set([categoryToShow, ...currentVisible])]
|
||||||
|
const noFilter = possibleFiltes.length === settings.length || !settings.length
|
||||||
|
|
||||||
|
noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(','))
|
||||||
|
history.pushState({}, null, unescape(url.href))
|
||||||
|
}
|
||||||
|
const setFilter = (currentFilter) => {
|
||||||
|
if (!possibleFiltes.includes(currentFilter)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const settings = [currentFilter, ...new Set(new URLSearchParams(url.search).get('filter').split(','))]
|
||||||
|
|
||||||
|
url.searchParams.set('filter', settings)
|
||||||
|
history.pushState({}, null, unescape(url.href))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSort = () => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
return new URLSearchParams(url.search).get('sort') || 'outcome'
|
||||||
|
}
|
||||||
|
const setSort = (type) => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.searchParams.set('sort', type)
|
||||||
|
history.pushState({}, null, unescape(url.href))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCollapsedCategory = () => {
|
||||||
|
let categotries
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const collapsedItems = new URLSearchParams(url.search).get('collapsed')
|
||||||
|
categotries = collapsedItems?.split(',') || []
|
||||||
|
} else {
|
||||||
|
categotries = []
|
||||||
|
}
|
||||||
|
return categotries
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc'))
|
||||||
|
|
||||||
|
const setSortDirection = (ascending) => sessionStorage.setItem('sortAsc', ascending)
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getVisible,
|
||||||
|
setFilter,
|
||||||
|
hideCategory,
|
||||||
|
showCategory,
|
||||||
|
getSort,
|
||||||
|
getSortDirection,
|
||||||
|
setSort,
|
||||||
|
setSortDirection,
|
||||||
|
getCollapsedCategory,
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
const formatedNumber = (number) =>
|
||||||
|
number.toLocaleString('en-US', {
|
||||||
|
minimumIntegerDigits: 2,
|
||||||
|
useGrouping: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatDuration = ( totalSeconds ) => {
|
||||||
|
if (totalSeconds < 1) {
|
||||||
|
return {ms: `${Math.round(totalSeconds * 1000)} ms`}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(totalSeconds / 3600)
|
||||||
|
let remainingSeconds = totalSeconds % 3600
|
||||||
|
const minutes = Math.floor(remainingSeconds / 60)
|
||||||
|
remainingSeconds = remainingSeconds % 60
|
||||||
|
const seconds = Math.round(remainingSeconds)
|
||||||
|
|
||||||
|
return {
|
||||||
|
seconds: `${Math.round(totalSeconds)} seconds`,
|
||||||
|
formatted: `${formatedNumber(hours)}:${formatedNumber(minutes)}:${formatedNumber(seconds)}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { formatDuration }
|
|
@ -1,5 +1,8 @@
|
||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
|
@ -10,3 +13,15 @@ def ansi_support():
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# ansi2html is not installed
|
# ansi2html is not installed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Return new dict with entries that are not json serializable by their str()."""
|
||||||
|
result = {}
|
||||||
|
for k, v in d.items():
|
||||||
|
try:
|
||||||
|
json.dumps({k: v})
|
||||||
|
except TypeError:
|
||||||
|
v = str(v)
|
||||||
|
result[k] = v
|
||||||
|
return result
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [[ $(uname) == "Darwin" ]]; then
|
||||||
|
volume="/private/var/folders:/reports/private/var/folders"
|
||||||
|
else
|
||||||
|
volume="${TMPDIR:-/tmp}:/reports${TMPDIR:-/tmp}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${1}" == "down" ]]; then
|
||||||
|
docker-compose -f <(sed -e "s;%%VOLUME%%;${volume};g" docker-compose.tmpl.yml) down
|
||||||
|
else
|
||||||
|
docker-compose -f <(sed -e "s;%%VOLUME%%;${volume};g" docker-compose.tmpl.yml) up -d
|
||||||
|
fi
|
|
@ -1,64 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width">
|
|
||||||
<title>QUnit Pytest-HTML</title>
|
|
||||||
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.0.1.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="qunit"></div>
|
|
||||||
<div id="qunit-fixture"></div>
|
|
||||||
<script src="https://code.jquery.com/qunit/qunit-2.0.1.js"></script>
|
|
||||||
<script src="https://raw.githubusercontent.com/alex-seville/blanket/master/dist/qunit/blanket.min.js"></script>
|
|
||||||
<script src="test.js"></script>
|
|
||||||
<script src="../src/pytest_html/resources/old_main.js" data-cover></script>
|
|
||||||
<div id="qunit-fixture">
|
|
||||||
<table id="results-table">
|
|
||||||
<thead id="results-table-head">
|
|
||||||
<tr>
|
|
||||||
<th class="sortable result initial-sort" col="result">Result</th>
|
|
||||||
<th class="sortable" col="name">Test</th>
|
|
||||||
<th class="sortable" col="duration">Duration</th>
|
|
||||||
<th class="sortable links" col="links">Links</th></tr>
|
|
||||||
<tr hidden="true" id="not-found-message">
|
|
||||||
<th colspan="5">No results found. Try to check the filters</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="rerun results-table-row">
|
|
||||||
<tr>
|
|
||||||
<td class="col-result">Rerun</td>
|
|
||||||
<td class="test-1 col-name">rerun.py::test_rexample_1</td>
|
|
||||||
<td class="col-duration">1.00</td>
|
|
||||||
<td class="col-links"><a class="url" href="http://www.google.com/" target="_blank">URL</a> </td></tr>
|
|
||||||
<tr>
|
|
||||||
<td class="extra" colspan="5">
|
|
||||||
<div class="log">@pytest.mark.flaky(reruns=5)<br/> def test_example():<br/> import random<br/>> assert random.choice([True, False])<br/><span class="error">E assert False</span><br/><span class="error">E + where False = <bound method Random.choice of <random.Random object at 0x7fe80b85f420>>([True, False])</span><br/><span class="error">E + where <bound method Random.choice of <random.Random object at 0x7fe80b85f420>> = <module 'random' from '/usr/local/Cellar/python/2.7.12/Frameworks/Python.framework/Versions/2.7/lib/python2.7/random.pyc'>.choice</span><br/><br/>rerun.py:6: AssertionError<br/></div></td></tr></tbody>
|
|
||||||
<tbody class="passed results-table-row">
|
|
||||||
<tr>
|
|
||||||
<td class="col-result">Passed</td>
|
|
||||||
<td class="test-2 col-name">rerun.py::test_example_2</td>
|
|
||||||
<td class="col-duration">0.00</td>
|
|
||||||
<td class="col-links"></td></tr>
|
|
||||||
<tr>
|
|
||||||
<td class="extra" colspan="5">
|
|
||||||
<div class="empty log">No log output captured.</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<tbody class="passed results-table-row">
|
|
||||||
<tr>
|
|
||||||
<td class="col-result">Passed</td>
|
|
||||||
<td class="test-3 col-name">rerun.py::test_example_3</td>
|
|
||||||
<td class="col-duration">0.00</td>
|
|
||||||
<td class="col-links"></td></tr>
|
|
||||||
<tr>
|
|
||||||
<td class="extra" colspan="5">
|
|
||||||
<div class="empty log">No log output captured.</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
134
testing/test.js
134
testing/test.js
|
@ -1,134 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
if (!String.prototype.includes) {
|
|
||||||
String.prototype.includes = function() {'use strict';
|
|
||||||
return String.prototype.indexOf.apply(this, arguments) !== -1;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
QUnit.module( 'module', {
|
|
||||||
beforeEach: function( assert ) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.test('sortColumn', function(assert){
|
|
||||||
function sortColumnTest(col_re, first_element_then, first_element_now) {
|
|
||||||
assert.equal(findAll('.results-table-row')[0].className, first_element_then);
|
|
||||||
var row_sort = find(col_re);
|
|
||||||
sortColumn(row_sort);
|
|
||||||
assert.equal(findAll('.results-table-row')[0].className, first_element_now);
|
|
||||||
}
|
|
||||||
|
|
||||||
//check col-name, tests should be in this order test-1 => (test-2 => test-3) on col-name
|
|
||||||
assert.equal(findAll('.col-name')[1].className, 'test-2 col-name');
|
|
||||||
|
|
||||||
//result
|
|
||||||
sortColumnTest('[col=result]',
|
|
||||||
'rerun results-table-row', 'passed results-table-row');
|
|
||||||
|
|
||||||
//make sure sorting the result column does not change the tests order in the col-name
|
|
||||||
//tests should be in this order (test-2 => test-3) => test1 on col-name
|
|
||||||
assert.equal(findAll('.col-name')[0].className, 'test-2 col-name');
|
|
||||||
|
|
||||||
sortColumnTest('[col=result]',
|
|
||||||
'passed results-table-row', 'rerun results-table-row');
|
|
||||||
|
|
||||||
|
|
||||||
//name
|
|
||||||
sortColumnTest('[col=name]',
|
|
||||||
'rerun results-table-row', 'passed results-table-row');
|
|
||||||
sortColumnTest('[col=name]',
|
|
||||||
'passed results-table-row', 'rerun results-table-row');
|
|
||||||
|
|
||||||
//duration
|
|
||||||
sortColumnTest('[col=duration]',
|
|
||||||
'rerun results-table-row', 'passed results-table-row');
|
|
||||||
sortColumnTest('[col=duration]',
|
|
||||||
'passed results-table-row', 'rerun results-table-row');
|
|
||||||
|
|
||||||
//links
|
|
||||||
sortColumnTest('[col=links]',
|
|
||||||
'rerun results-table-row', 'passed results-table-row');
|
|
||||||
sortColumnTest('[col=links]',
|
|
||||||
'passed results-table-row', 'rerun results-table-row');
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.test('filterTable', function(assert){
|
|
||||||
function filterTableTest(outcome, checked) {
|
|
||||||
var filter_input = document.createElement('input');
|
|
||||||
filter_input.setAttribute('data-test-result', outcome);
|
|
||||||
filter_input.checked = checked;
|
|
||||||
filterTable(filter_input);
|
|
||||||
|
|
||||||
var outcomes = findAll('.' + outcome);
|
|
||||||
for(var i = 0; i < outcomes.length; i++) {
|
|
||||||
assert.equal(outcomes[i].hidden, !checked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.equal(find('#not-found-message').hidden, true);
|
|
||||||
|
|
||||||
filterTableTest('rerun', false);
|
|
||||||
filterTableTest('passed', false);
|
|
||||||
assert.equal(find('#not-found-message').hidden, false);
|
|
||||||
|
|
||||||
filterTableTest('rerun', true);
|
|
||||||
assert.equal(find('#not-found-message').hidden, true);
|
|
||||||
|
|
||||||
filterTableTest('passed', true);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.test('showHideExtras', function(assert) {
|
|
||||||
function showExtrasTest(element){
|
|
||||||
assert.equal(element.parentNode.nextElementSibling.className, 'collapsed');
|
|
||||||
showExtras(element);
|
|
||||||
assert.notEqual(element.parentNode.nextElementSibling.className, 'collapsed');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideExtrasTest(element){
|
|
||||||
assert.notEqual(element.parentNode.nextElementSibling.className, 'collapsed');
|
|
||||||
hideExtras(element);
|
|
||||||
assert.equal(element.parentNode.nextElementSibling.className, 'collapsed');
|
|
||||||
}
|
|
||||||
//Passed results have log collapsed by default
|
|
||||||
showExtrasTest(find('.passed').firstElementChild.firstElementChild);
|
|
||||||
hideExtrasTest(find('.passed').firstElementChild.firstElementChild);
|
|
||||||
|
|
||||||
hideExtrasTest(find('.rerun').firstElementChild.firstElementChild);
|
|
||||||
showExtrasTest(find('.rerun').firstElementChild.firstElementChild);
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.test('showHideAllExtras', function(assert) {
|
|
||||||
function showAllExtrasTest(){
|
|
||||||
showAllExtras();
|
|
||||||
var extras = findAll('.extra');
|
|
||||||
for (var i = 0; i < extras.length; i++) {
|
|
||||||
assert.notEqual(extras[i].parentNode.className, 'collapsed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideAllExtrasTest(){
|
|
||||||
hideAllExtras();
|
|
||||||
var extras = findAll('.extra');
|
|
||||||
for (var i = 0; i < extras.length; i++) {
|
|
||||||
assert.equal(extras[i].parentNode.className, 'collapsed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showAllExtrasTest();
|
|
||||||
hideAllExtrasTest();
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.test('find', function (assert) {
|
|
||||||
assert.notEqual(find('#results-table-head'), null);
|
|
||||||
assert.notEqual(find('table#results-table'), null);
|
|
||||||
assert.equal(find('.not-in-table'), null);
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.test('findAll', function(assert) {
|
|
||||||
assert.equal(findAll('.sortable').length, 4);
|
|
||||||
assert.equal(findAll('.not-in-table').length, 0);
|
|
||||||
});
|
|
|
@ -0,0 +1,473 @@
|
||||||
|
import base64
|
||||||
|
import importlib.resources
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
from base64 import b64encode
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pkg_resources
|
||||||
|
import pytest
|
||||||
|
from assertpy import assert_that
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from selenium import webdriver
|
||||||
|
|
||||||
|
pytest_plugins = ("pytester",)
|
||||||
|
|
||||||
|
OUTCOMES = {
|
||||||
|
"passed": "Passed",
|
||||||
|
"skipped": "Skipped",
|
||||||
|
"failed": "Failed",
|
||||||
|
"error": "Errors",
|
||||||
|
"xfailed": "Unexpected failures",
|
||||||
|
"xpassed": "Unexpected passes",
|
||||||
|
"rerun": "Reruns",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run(pytester, path="report.html", *args):
|
||||||
|
path = pytester.path.joinpath(path)
|
||||||
|
pytester.runpytest("-s", "--html", path, *args)
|
||||||
|
|
||||||
|
chrome_options = webdriver.ChromeOptions()
|
||||||
|
chrome_options.add_argument("--headless")
|
||||||
|
chrome_options.add_argument("--window-size=1920x1080")
|
||||||
|
# chrome_options.add_argument("--allow-file-access-from-files")
|
||||||
|
driver = webdriver.Remote(
|
||||||
|
command_executor="http://127.0.0.1:4444", options=chrome_options
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# Begin workaround
|
||||||
|
# See: https://github.com/pytest-dev/pytest/issues/10738
|
||||||
|
path.chmod(0o755)
|
||||||
|
for parent in path.parents:
|
||||||
|
try:
|
||||||
|
os.chmod(parent, 0o755)
|
||||||
|
except PermissionError:
|
||||||
|
continue
|
||||||
|
# End workaround
|
||||||
|
|
||||||
|
driver.get(f"file:///reports{path}")
|
||||||
|
return BeautifulSoup(driver.page_source, "html.parser")
|
||||||
|
finally:
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
|
||||||
|
def assert_results(
|
||||||
|
page,
|
||||||
|
passed=0,
|
||||||
|
skipped=0,
|
||||||
|
failed=0,
|
||||||
|
error=0,
|
||||||
|
xfailed=0,
|
||||||
|
xpassed=0,
|
||||||
|
rerun=0,
|
||||||
|
total_tests=None,
|
||||||
|
):
|
||||||
|
args = locals()
|
||||||
|
number_of_tests = 0
|
||||||
|
for outcome, number in args.items():
|
||||||
|
if outcome == "total_tests":
|
||||||
|
continue
|
||||||
|
if isinstance(number, int):
|
||||||
|
number_of_tests += number
|
||||||
|
result = get_text(page, f"span[class={outcome}]")
|
||||||
|
assert_that(result).is_equal_to(f"{number} {OUTCOMES[outcome]}")
|
||||||
|
|
||||||
|
# if total_tests is not None:
|
||||||
|
# number_of_tests = total_tests
|
||||||
|
# total = get_text(page, "p[class='run-count']")
|
||||||
|
# expr = r"%d %s ran in \d+.\d+ seconds."
|
||||||
|
# % (number_of_tests, "tests" if number_of_tests > 1 else "test")
|
||||||
|
# assert_that(total).matches(expr)
|
||||||
|
|
||||||
|
|
||||||
|
def get_element(page, selector):
|
||||||
|
return page.select_one(selector)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text(page, selector):
|
||||||
|
return get_element(page, selector).string
|
||||||
|
|
||||||
|
|
||||||
|
def get_log(page):
|
||||||
|
# TODO(jim) move to get_text (use .contents)
|
||||||
|
log = get_element(page, ".summary div[class='log']")
|
||||||
|
all_text = ""
|
||||||
|
for text in log.strings:
|
||||||
|
all_text += text
|
||||||
|
|
||||||
|
return all_text
|
||||||
|
|
||||||
|
|
||||||
|
def file_content():
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
importlib.resources.files("pytest_html")
|
||||||
|
.joinpath("resources", "style.css")
|
||||||
|
.read_bytes()
|
||||||
|
.decode("utf-8")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
# Needed for python < 3.9
|
||||||
|
return pkg_resources.resource_string(
|
||||||
|
"pytest_html", os.path.join("resources", "style.css")
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTML:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pause, expectation",
|
||||||
|
[
|
||||||
|
(0.4, 400),
|
||||||
|
(1, r"^((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$)"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_durations(self, pytester, pause, expectation):
|
||||||
|
pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
import time
|
||||||
|
def test_sleep():
|
||||||
|
time.sleep({pause})
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
page = run(pytester)
|
||||||
|
duration = get_text(page, "#results-table td[class='col-duration']")
|
||||||
|
if pause < 1:
|
||||||
|
assert_that(int(duration.replace("ms", ""))).is_between(
|
||||||
|
expectation, expectation * 2
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert_that(duration).matches(expectation)
|
||||||
|
|
||||||
|
def test_pass(self, pytester):
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, passed=1)
|
||||||
|
|
||||||
|
def test_skip(self, pytester):
|
||||||
|
reason = str(random.random())
|
||||||
|
pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
def test_skip():
|
||||||
|
pytest.skip('{reason}')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, skipped=1, total_tests=0)
|
||||||
|
|
||||||
|
log = get_text(page, ".summary div[class='log']")
|
||||||
|
assert_that(log).contains(reason)
|
||||||
|
|
||||||
|
def test_fail(self, pytester):
|
||||||
|
pytester.makepyfile("def test_fail(): assert False")
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, failed=1)
|
||||||
|
assert_that(get_log(page)).contains("AssertionError")
|
||||||
|
|
||||||
|
def test_xfail(self, pytester):
|
||||||
|
reason = str(random.random())
|
||||||
|
pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
def test_xfail():
|
||||||
|
pytest.xfail('{reason}')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, xfailed=1)
|
||||||
|
assert_that(get_log(page)).contains(reason)
|
||||||
|
|
||||||
|
def test_xpass(self, pytester):
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.xfail()
|
||||||
|
def test_xpass():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, xpassed=1)
|
||||||
|
|
||||||
|
def test_rerun(self, pytester):
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
|
||||||
|
@pytest.mark.flaky(reruns=2)
|
||||||
|
def test_example():
|
||||||
|
time.sleep(0.2)
|
||||||
|
assert False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, failed=1, rerun=2, total_tests=1)
|
||||||
|
|
||||||
|
def test_conditional_xfails(self, pytester):
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.xfail(False, reason='reason')
|
||||||
|
def test_fail(): assert False
|
||||||
|
@pytest.mark.xfail(False, reason='reason')
|
||||||
|
def test_pass(): pass
|
||||||
|
@pytest.mark.xfail(True, reason='reason')
|
||||||
|
def test_xfail(): assert False
|
||||||
|
@pytest.mark.xfail(True, reason='reason')
|
||||||
|
def test_xpass(): pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, passed=1, failed=1, xfailed=1, xpassed=1)
|
||||||
|
|
||||||
|
def test_setup_error(self, pytester):
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
@pytest.fixture
|
||||||
|
def arg(request):
|
||||||
|
raise ValueError()
|
||||||
|
def test_function(arg):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
page = run(pytester)
|
||||||
|
assert_results(page, error=1, total_tests=0)
|
||||||
|
|
||||||
|
col_name = get_text(page, ".summary td[class='col-name']")
|
||||||
|
assert_that(col_name).contains("::setup")
|
||||||
|
assert_that(get_log(page)).contains("ValueError")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("title", ["", "Special Report"])
|
||||||
|
def test_report_title(self, pytester, title):
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
|
||||||
|
if title:
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
def pytest_html_report_title(report):
|
||||||
|
report.title = "{title}"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_title = title if title else "report.html"
|
||||||
|
page = run(pytester)
|
||||||
|
assert_that(get_text(page, "#head-title")).is_equal_to(expected_title)
|
||||||
|
assert_that(get_text(page, "h1[id='title']")).is_equal_to(expected_title)
|
||||||
|
|
||||||
|
def test_resources_inline_css(self, pytester):
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester, "report.html", "--self-contained-html")
|
||||||
|
|
||||||
|
content = file_content()
|
||||||
|
|
||||||
|
assert_that(get_text(page, "head style").strip()).contains(content)
|
||||||
|
|
||||||
|
def test_resources_css(self, pytester):
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester)
|
||||||
|
|
||||||
|
assert_that(page.select_one("head link")["href"]).is_equal_to(
|
||||||
|
str(Path("assets", "style.css"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_content_in_summary(self, pytester):
|
||||||
|
content = {
|
||||||
|
"prefix": str(random.random()),
|
||||||
|
"summary": str(random.random()),
|
||||||
|
"postfix": str(random.random()),
|
||||||
|
}
|
||||||
|
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def pytest_html_results_summary(prefix, summary, postfix):
|
||||||
|
prefix.append(r"<p>prefix is {content['prefix']}</p>")
|
||||||
|
summary.extend([r"<p>summary is {content['summary']}</p>"])
|
||||||
|
postfix.extend([r"<p>postfix is {content['postfix']}</p>"])
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester)
|
||||||
|
|
||||||
|
elements = page.select(".summary__data p:not(.run-count):not(.filter)")
|
||||||
|
assert_that(elements).is_length(3)
|
||||||
|
for element in elements:
|
||||||
|
key = re.search(r"(\w+).*", element.string).group(1)
|
||||||
|
value = content.pop(key)
|
||||||
|
assert_that(element.string).contains(value)
|
||||||
|
|
||||||
|
def test_extra_html(self, pytester):
|
||||||
|
content = str(random.random())
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
outcome = yield
|
||||||
|
report = outcome.get_result()
|
||||||
|
if report.when == 'call':
|
||||||
|
from pytest_html import extras
|
||||||
|
report.extras = [extras.html('<div>{content}</div>')]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester)
|
||||||
|
|
||||||
|
assert_that(page.select_one(".summary .extraHTML").string).is_equal_to(content)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"content, encoded",
|
||||||
|
[("u'\u0081'", "woE="), ("'foo'", "Zm9v"), ("b'\\xe2\\x80\\x93'", "4oCT")],
|
||||||
|
)
|
||||||
|
def test_extra_text(self, pytester, content, encoded):
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
outcome = yield
|
||||||
|
report = outcome.get_result()
|
||||||
|
if report.when == 'call':
|
||||||
|
from pytest_html import extras
|
||||||
|
report.extras = [extras.text({content})]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester, "report.html", "--self-contained-html")
|
||||||
|
|
||||||
|
element = page.select_one(".summary a[class='col-links__extra text']")
|
||||||
|
assert_that(element.string).is_equal_to("Text")
|
||||||
|
assert_that(element["href"]).is_equal_to(
|
||||||
|
f"data:text/plain;charset=utf-8;base64,{encoded}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_extra_json(self, pytester):
|
||||||
|
content = {str(random.random()): str(random.random())}
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
outcome = yield
|
||||||
|
report = outcome.get_result()
|
||||||
|
if report.when == 'call':
|
||||||
|
from pytest_html import extras
|
||||||
|
report.extras = [extras.json({content})]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester, "report.html", "--self-contained-html")
|
||||||
|
|
||||||
|
content_str = json.dumps(content)
|
||||||
|
data = b64encode(content_str.encode("utf-8")).decode("ascii")
|
||||||
|
|
||||||
|
element = page.select_one(".summary a[class='col-links__extra json']")
|
||||||
|
assert_that(element.string).is_equal_to("JSON")
|
||||||
|
assert_that(element["href"]).is_equal_to(
|
||||||
|
f"data:application/json;charset=utf-8;base64,{data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_extra_url(self, pytester):
|
||||||
|
content = str(random.random())
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
outcome = yield
|
||||||
|
report = outcome.get_result()
|
||||||
|
if report.when == 'call':
|
||||||
|
from pytest_html import extras
|
||||||
|
report.extras = [extras.url('{content}')]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester)
|
||||||
|
|
||||||
|
element = page.select_one(".summary a[class='col-links__extra url']")
|
||||||
|
assert_that(element.string).is_equal_to("URL")
|
||||||
|
assert_that(element["href"]).is_equal_to(content)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mime_type, extension",
|
||||||
|
[
|
||||||
|
("image/png", "png"),
|
||||||
|
("image/png", "image"),
|
||||||
|
("image/jpeg", "jpg"),
|
||||||
|
("image/svg+xml", "svg"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_extra_image(self, pytester, mime_type, extension):
|
||||||
|
content = str(random.random())
|
||||||
|
charset = "utf-8"
|
||||||
|
data = base64.b64encode(content.encode(charset)).decode(charset)
|
||||||
|
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
outcome = yield
|
||||||
|
report = outcome.get_result()
|
||||||
|
if report.when == 'call':
|
||||||
|
from pytest_html import extras
|
||||||
|
report.extras = [extras.{extension}('{data}')]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester, "report.html", "--self-contained-html")
|
||||||
|
|
||||||
|
# element = page.select_one(".summary a[class='col-links__extra image']")
|
||||||
|
src = f"data:{mime_type};base64,{data}"
|
||||||
|
# assert_that(element.string).is_equal_to("Image")
|
||||||
|
# assert_that(element["href"]).is_equal_to(src)
|
||||||
|
|
||||||
|
element = page.select_one(".summary .media img")
|
||||||
|
assert_that(str(element)).is_equal_to(f'<img src="{src}"/>')
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mime_type, extension", [("video/mp4", "mp4")])
|
||||||
|
def test_extra_video(self, pytester, mime_type, extension):
|
||||||
|
content = str(random.random())
|
||||||
|
charset = "utf-8"
|
||||||
|
data = base64.b64encode(content.encode(charset)).decode(charset)
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
outcome = yield
|
||||||
|
report = outcome.get_result()
|
||||||
|
if report.when == 'call':
|
||||||
|
from pytest_html import extras
|
||||||
|
report.extras = [extras.{extension}('{data}')]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile("def test_pass(): pass")
|
||||||
|
page = run(pytester, "report.html", "--self-contained-html")
|
||||||
|
|
||||||
|
# element = page.select_one(".summary a[class='col-links__extra video']")
|
||||||
|
src = f"data:{mime_type};base64,{data}"
|
||||||
|
# assert_that(element.string).is_equal_to("Video")
|
||||||
|
# assert_that(element["href"]).is_equal_to(src)
|
||||||
|
|
||||||
|
element = page.select_one(".summary .media video")
|
||||||
|
assert_that(str(element)).is_equal_to(
|
||||||
|
f'<video controls="">\n<source src="{src}" type="{mime_type}"/>\n</video>'
|
||||||
|
)
|
|
@ -0,0 +1,158 @@
|
||||||
|
const { expect } = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { doInitFilter, doFilter } = require('../src/pytest_html/scripts/filter.js')
|
||||||
|
const { doInitSort, doSort } = require('../src/pytest_html/scripts/sort.js')
|
||||||
|
const { formatDuration } = require('../src/pytest_html/scripts/utils.js')
|
||||||
|
const dataModule = require('../src/pytest_html/scripts/datamanager.js')
|
||||||
|
const storageModule = require('../src/pytest_html/scripts/storage.js')
|
||||||
|
|
||||||
|
|
||||||
|
const setTestData = () => {
|
||||||
|
const jsonDatan = {
|
||||||
|
'tests':
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'id': 'passed_1',
|
||||||
|
'outcome': 'passed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'failed_2',
|
||||||
|
'outcome': 'failed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'passed_3',
|
||||||
|
'outcome': 'passed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'passed_4',
|
||||||
|
'outcome': 'passed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'passed_5',
|
||||||
|
'outcome': 'passed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'passed_6',
|
||||||
|
'outcome': 'passed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
dataModule.manager.setManager(jsonDatan)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Filter tests', () => {
|
||||||
|
let getFilterMock
|
||||||
|
let managerSpy
|
||||||
|
|
||||||
|
beforeEach(setTestData)
|
||||||
|
afterEach(() => [getFilterMock, managerSpy].forEach((fn) => fn.restore()))
|
||||||
|
after(() => dataModule.manager.setManager({ tests: [] }))
|
||||||
|
|
||||||
|
describe('doInitFilter', () => {
|
||||||
|
it('has no stored filters', () => {
|
||||||
|
getFilterMock = sinon.stub(storageModule, 'getVisible').returns([])
|
||||||
|
managerSpy = sinon.spy(dataModule.manager, 'setRender')
|
||||||
|
|
||||||
|
doInitFilter()
|
||||||
|
expect(managerSpy.callCount).to.eql(1)
|
||||||
|
expect(dataModule.manager.testSubset.map(({ outcome }) => outcome)).to.eql([])
|
||||||
|
})
|
||||||
|
it('exclude passed', () => {
|
||||||
|
getFilterMock = sinon.stub(storageModule, 'getVisible').returns(['failed'])
|
||||||
|
managerSpy = sinon.spy(dataModule.manager, 'setRender')
|
||||||
|
|
||||||
|
doInitFilter()
|
||||||
|
expect(managerSpy.callCount).to.eql(1)
|
||||||
|
expect(dataModule.manager.testSubset.map(({ outcome }) => outcome)).to.eql(['failed'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('doFilter', () => {
|
||||||
|
let setFilterMock
|
||||||
|
afterEach(() => setFilterMock.restore())
|
||||||
|
it('removes all but passed', () => {
|
||||||
|
getFilterMock = sinon.stub(storageModule, 'getVisible').returns(['passed'])
|
||||||
|
setFilterMock = sinon.stub(storageModule, 'setFilter')
|
||||||
|
managerSpy = sinon.spy(dataModule.manager, 'setRender')
|
||||||
|
|
||||||
|
doFilter('passed', true)
|
||||||
|
expect(managerSpy.callCount).to.eql(1)
|
||||||
|
expect(dataModule.manager.testSubset.map(({ outcome }) => outcome)).to.eql([
|
||||||
|
'passed', 'passed', 'passed', 'passed', 'passed',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('Sort tests', () => {
|
||||||
|
beforeEach(setTestData)
|
||||||
|
after(() => dataModule.manager.setManager({ tests: [] }))
|
||||||
|
describe('doInitSort', () => {
|
||||||
|
let managerSpy
|
||||||
|
let sortMock
|
||||||
|
let sortDirectionMock
|
||||||
|
beforeEach(() => dataModule.manager.resetRender())
|
||||||
|
|
||||||
|
afterEach(() => [sortMock, sortDirectionMock, managerSpy].forEach((fn) => fn.restore()))
|
||||||
|
it('has no stored sort', () => {
|
||||||
|
sortMock = sinon.stub(storageModule, 'getSort').returns(null)
|
||||||
|
sortDirectionMock = sinon.stub(storageModule, 'getSortDirection').returns(null)
|
||||||
|
managerSpy = sinon.spy(dataModule.manager, 'setRender')
|
||||||
|
|
||||||
|
doInitSort()
|
||||||
|
expect(managerSpy.callCount).to.eql(1)
|
||||||
|
expect(dataModule.manager.testSubset.map(({ outcome }) => outcome)).to.eql([
|
||||||
|
'passed', 'failed', 'passed', 'passed', 'passed', 'passed',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
it('has stored sort preference', () => {
|
||||||
|
sortMock = sinon.stub(storageModule, 'getSort').returns('outcome')
|
||||||
|
sortDirectionMock = sinon.stub(storageModule, 'getSortDirection').returns(false)
|
||||||
|
managerSpy = sinon.spy(dataModule.manager, 'setRender')
|
||||||
|
|
||||||
|
doInitSort()
|
||||||
|
expect(managerSpy.callCount).to.eql(1)
|
||||||
|
expect(dataModule.manager.testSubset.map(({ outcome }) => outcome)).to.eql([
|
||||||
|
'failed', 'passed', 'passed', 'passed', 'passed', 'passed',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('doSort', () => {
|
||||||
|
let getSortMock
|
||||||
|
let setSortMock
|
||||||
|
let getSortDirectionMock
|
||||||
|
let setSortDirection
|
||||||
|
let managerSpy
|
||||||
|
|
||||||
|
afterEach(() => [
|
||||||
|
getSortMock, setSortMock, getSortDirectionMock, setSortDirection, managerSpy,
|
||||||
|
].forEach((fn) => fn.restore()))
|
||||||
|
it('sort on outcome', () => {
|
||||||
|
getSortMock = sinon.stub(storageModule, 'getSort').returns(null)
|
||||||
|
setSortMock = sinon.stub(storageModule, 'setSort')
|
||||||
|
getSortDirectionMock = sinon.stub(storageModule, 'getSortDirection').returns(null)
|
||||||
|
setSortDirection = sinon.stub(storageModule, 'setSortDirection')
|
||||||
|
managerSpy = sinon.spy(dataModule.manager, 'setRender')
|
||||||
|
|
||||||
|
doSort('outcome')
|
||||||
|
expect(managerSpy.callCount).to.eql(1)
|
||||||
|
expect(dataModule.manager.testSubset.map(({ outcome }) => outcome)).to.eql([
|
||||||
|
'passed', 'passed', 'passed', 'passed', 'passed', 'failed',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('utils tests', () => {
|
||||||
|
describe('formatDuration', () => {
|
||||||
|
it('handles small durations', () => {
|
||||||
|
expect(formatDuration(0.123).ms).to.eql('123 ms')
|
||||||
|
expect(formatDuration(0).ms).to.eql('0 ms')
|
||||||
|
expect(formatDuration(0.999).ms).to.eql('999 ms')
|
||||||
|
})
|
||||||
|
it('handles larger durations', () => {
|
||||||
|
expect(formatDuration(1.234).formatted).to.eql('00:00:01')
|
||||||
|
expect(formatDuration(12345.678).formatted).to.eql('03:25:46')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
45
tox.ini
45
tox.ini
|
@ -8,16 +8,20 @@ envlist = py{37,38,39,310,py3}, docs, linting
|
||||||
isolated_build = True
|
isolated_build = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
setenv = PYTHONDONTWRITEBYTECODE=1
|
setenv =
|
||||||
|
PYTHONDONTWRITEBYTECODE=1
|
||||||
deps =
|
deps =
|
||||||
|
assertpy
|
||||||
|
beautifulsoup4
|
||||||
pytest-xdist
|
pytest-xdist
|
||||||
pytest-rerunfailures
|
pytest-rerunfailures
|
||||||
pytest-mock
|
pytest-mock
|
||||||
|
selenium
|
||||||
ansi2html # soft-dependency
|
ansi2html # soft-dependency
|
||||||
cov: pytest-cov
|
cov: pytest-cov
|
||||||
commands =
|
commands =
|
||||||
!cov: pytest -v -r a --color=yes --html={envlogdir}/report.html --self-contained-html {posargs}
|
!cov: pytest -s -ra --color=yes --html={envlogdir}/report.html --self-contained-html {posargs}
|
||||||
cov: pytest -v -r a --color=yes --html={envlogdir}/report.html --self-contained-html --cov={envsitepackagesdir}/pytest_html --cov-report=term --cov-report=xml {posargs}
|
cov: pytest -s -ra --color=yes --html={envlogdir}/report.html --self-contained-html --cov={envsitepackagesdir}/pytest_html --cov-report=term --cov-report=xml {posargs}
|
||||||
|
|
||||||
[testenv:linting]
|
[testenv:linting]
|
||||||
skip_install = True
|
skip_install = True
|
||||||
|
@ -35,12 +39,6 @@ deps =
|
||||||
pytest-rerunfailures @ git+https://github.com/pytest-dev/pytest-rerunfailures.git
|
pytest-rerunfailures @ git+https://github.com/pytest-dev/pytest-rerunfailures.git
|
||||||
pytest @ git+https://github.com/pytest-dev/pytest.git
|
pytest @ git+https://github.com/pytest-dev/pytest.git
|
||||||
|
|
||||||
[testenv:devel-cov]
|
|
||||||
description = Tests with unreleased deps and coverage
|
|
||||||
basepython = {[testenv:devel]basepython}
|
|
||||||
pip_pre = {[testenv:devel]pip_pre}
|
|
||||||
deps = {[testenv:devel]deps}
|
|
||||||
|
|
||||||
[testenv:docs]
|
[testenv:docs]
|
||||||
# NOTE: The command for doc building was taken from readthedocs documentation
|
# NOTE: The command for doc building was taken from readthedocs documentation
|
||||||
# See https://docs.readthedocs.io/en/stable/builds.html#understanding-what-s-going-on
|
# See https://docs.readthedocs.io/en/stable/builds.html#understanding-what-s-going-on
|
||||||
|
@ -49,35 +47,6 @@ changedir = docs
|
||||||
deps = sphinx
|
deps = sphinx
|
||||||
commands = sphinx-build -b html . _build/html
|
commands = sphinx-build -b html . _build/html
|
||||||
|
|
||||||
[testenv:packaging]
|
|
||||||
description =
|
|
||||||
Do packaging/distribution. If tag is not present or PEP440 compliant upload to
|
|
||||||
PYPI could fail
|
|
||||||
# `usedevelop = true` overrides `skip_install` instruction, it's unwanted
|
|
||||||
usedevelop = false
|
|
||||||
# don't install package in this env
|
|
||||||
skip_install = true
|
|
||||||
deps =
|
|
||||||
collective.checkdocs >= 0.2
|
|
||||||
pep517 >= 0.8.2
|
|
||||||
pip >= 20.2.2
|
|
||||||
toml >= 0.10.1
|
|
||||||
twine >= 3.2.0
|
|
||||||
setenv =
|
|
||||||
commands =
|
|
||||||
rm -rfv {toxinidir}/dist/
|
|
||||||
python -m pep517.build \
|
|
||||||
--source \
|
|
||||||
--binary \
|
|
||||||
--out-dir {toxinidir}/dist/ \
|
|
||||||
{toxinidir}
|
|
||||||
# metadata validation
|
|
||||||
python setup.py check
|
|
||||||
sh -c "python -m twine check {toxinidir}/dist/*"
|
|
||||||
whitelist_externals =
|
|
||||||
rm
|
|
||||||
sh
|
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 88
|
max-line-length = 88
|
||||||
exclude = .eggs,.tox
|
exclude = .eggs,.tox
|
||||||
|
|
Loading…
Reference in New Issue