Skip to content

Commit 536900b

Browse files
committed
Add bootstrap template
Signed-off-by: Conor MacBride <[email protected]>
1 parent 4cbcf82 commit 536900b

File tree

14 files changed

+495
-0
lines changed

14 files changed

+495
-0
lines changed

pytest_mpl/summary/__init__.py

Whitespace-only changes.

pytest_mpl/summary/html.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import os
2+
import shutil
3+
4+
__all__ = ['generate_summary_html']
5+
6+
BTN_CLASS = {
7+
'passed': 'success',
8+
'failed': 'danger',
9+
'skipped': 'warning',
10+
'match': 'success',
11+
'diff': 'danger',
12+
'missing': 'warning',
13+
}
14+
15+
IMAGE_STATUS = {
16+
'match': 'Baseline image matches',
17+
'diff': 'Baseline image differs',
18+
'missing': 'Baseline image not found',
19+
}
20+
21+
HASH_STATUS = {
22+
'match': 'Baseline hash matches',
23+
'diff': 'Baseline hash differs',
24+
'missing': 'Baseline hash not found',
25+
}
26+
27+
28+
def template(name):
29+
file = os.path.join(os.path.dirname(__file__), 'templates', f'{name}.html')
30+
f = open(file, 'r')
31+
return f.read()
32+
33+
34+
BASE = template('base')
35+
NAVBAR = template('navbar')
36+
FILTER = template('filter')
37+
RESULT = template('result')
38+
RESULT_DIFFIMAGE = template('result_diffimage')
39+
RESULT_BADGE = template('result_badge')
40+
RESULT_BADGE_ICON = template('result_badge_icon')
41+
RESULT_IMAGES = template('result_images')
42+
43+
44+
def get_status(item, card_id, warn_missing):
45+
status = {
46+
'overall': None,
47+
'image': None,
48+
'hash': None,
49+
}
50+
51+
assert item['status'] in BTN_CLASS.keys()
52+
status['overall'] = item['status']
53+
54+
if item['rms'] is None and item['tolerance'] is not None:
55+
status['image'] = 'match'
56+
elif item['rms'] is not None:
57+
status['image'] = 'diff'
58+
elif item['baseline_image'] is None:
59+
status['image'] = 'missing'
60+
else:
61+
raise ValueError('Unknown image result.')
62+
63+
baseline_hash = item['baseline_hash']
64+
result_hash = item['result_hash']
65+
if baseline_hash is not None or result_hash is not None:
66+
if baseline_hash is None:
67+
status['hash'] = 'missing'
68+
elif baseline_hash == result_hash:
69+
status['hash'] = 'match'
70+
else:
71+
status['hash'] = 'diff'
72+
73+
classes = [f'{k}-{str(v).lower()}' for k, v in status.items()]
74+
75+
extra_badges = ''
76+
for test_type, status_dict in [('image', IMAGE_STATUS), ('hash', HASH_STATUS)]:
77+
if not warn_missing[f'baseline_{test_type}']:
78+
continue # not expected to exist
79+
if (
80+
(status[test_type] == 'missing') or
81+
(status['overall'] == 'failed' and status[test_type] == 'match') or
82+
(status['overall'] == 'passed' and status[test_type] == 'diff')
83+
):
84+
extra_badges += RESULT_BADGE_ICON.format(
85+
card_id=card_id,
86+
btn_class=BTN_CLASS[status[test_type]],
87+
svg=test_type,
88+
tooltip=status_dict[status[test_type]],
89+
)
90+
91+
badge = RESULT_BADGE.format(
92+
card_id=card_id,
93+
status=status['overall'].upper(),
94+
btn_class=BTN_CLASS[status['overall']],
95+
extra_badges=extra_badges,
96+
)
97+
98+
return status, classes, badge
99+
100+
101+
def card(name, item, warn_missing=None):
102+
card_id = name.replace('.', '-')
103+
test_name = name.split('.')[-1]
104+
module = '.'.join(name.split('.')[:-1])
105+
106+
status, classes, badge = get_status(item, card_id, warn_missing)
107+
108+
if item['diff_image'] is None:
109+
image = f'<img src="{item["result_image"]}" class="card-img-top" alt="result image">'
110+
else: # show overlapping diff and result images
111+
image = RESULT_DIFFIMAGE.format(diff=item['diff_image'], result=item["result_image"])
112+
113+
image_html = {}
114+
for image_type in ['baseline_image', 'diff_image', 'result_image']:
115+
if item[image_type] is not None:
116+
image_html[image_type] = f'<img src="{item[image_type]}" class="card-img-top" alt="">'
117+
else:
118+
image_html[image_type] = ''
119+
120+
if status['image'] == 'match':
121+
rms = '&lt; tolerance'
122+
else:
123+
rms = item['rms']
124+
125+
offcanvas = RESULT_IMAGES.format(
126+
127+
id=card_id,
128+
test_name=test_name,
129+
module=module,
130+
131+
baseline_image=image_html['baseline_image'],
132+
diff_image=image_html['diff_image'],
133+
result_image=image_html['result_image'],
134+
135+
status=status['overall'].upper(),
136+
btn_class=BTN_CLASS[status['overall']],
137+
status_msg=item['status_msg'],
138+
139+
image_status=IMAGE_STATUS[status['image']],
140+
image_btn_class=BTN_CLASS[status['image']],
141+
rms=rms,
142+
tolerance=item['tolerance'],
143+
144+
hash_status=HASH_STATUS[status['hash']],
145+
hash_btn_class=BTN_CLASS[status['hash']],
146+
baseline_hash=item['baseline_hash'],
147+
result_hash=item['result_hash'],
148+
149+
)
150+
151+
result_card = RESULT.format(
152+
153+
classes=" ".join(classes),
154+
155+
id=card_id,
156+
test_name=test_name,
157+
module=module,
158+
159+
image=image,
160+
badge=badge,
161+
offcanvas=offcanvas,
162+
163+
)
164+
165+
return result_card
166+
167+
168+
def generate_summary_html(results, results_dir):
169+
# If any baseline images or baseline hashes are present,
170+
# assume all results should have them
171+
warn_missing = {'baseline_image': False, 'baseline_hash': False}
172+
for key in warn_missing.keys():
173+
for result in results.values():
174+
if result[key] is not None:
175+
warn_missing[key] = True
176+
break
177+
178+
classes = []
179+
if warn_missing['baseline_hash'] is False:
180+
classes += ['no-hash-test']
181+
182+
# Generate result cards
183+
cards = ''
184+
for name, item in results.items():
185+
cards += card(name, item, warn_missing=warn_missing)
186+
187+
# Generate HTML
188+
html = BASE.format(
189+
title="Image comparison",
190+
navbar=NAVBAR,
191+
cards=cards,
192+
filter=FILTER,
193+
classes=" ".join(classes),
194+
)
195+
196+
# Write files
197+
for file in ['styles.css', 'extra.js', 'hash.svg', 'image.svg']:
198+
path = os.path.join(os.path.dirname(__file__), 'templates', file)
199+
shutil.copy(path, results_dir / file)
200+
html_file = results_dir / 'fig_comparison.html'
201+
with open(html_file, 'w') as f:
202+
f.write(html)
203+
204+
return html_file
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<link href="styles.css" rel="stylesheet">
7+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
8+
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
9+
<title>{title}</title>
10+
</head>
11+
<body class="{classes}">
12+
{navbar}
13+
<div class="album bg-light">
14+
<div class="container-fluid">
15+
<div class="row">
16+
{cards}
17+
</div>
18+
</div>
19+
</div>
20+
<div class="offcanvas offcanvas-start" tabindex="-1"></div>
21+
{filter}
22+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
23+
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
24+
crossorigin="anonymous"></script>
25+
<script src="extra.js"></script>
26+
</body>
27+
</html>

pytest_mpl/summary/templates/extra.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Remove all elements of class mpl-hash if hash test not run
2+
if (document.body.classList[0] == 'no-hash-test') {
3+
document.querySelectorAll('.mpl-hash').forEach(function (elem) {
4+
elem.parentElement.removeChild(elem);
5+
});
6+
}
7+
8+
// Enable tooltips
9+
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
10+
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
11+
return new bootstrap.Tooltip(tooltipTriggerEl)
12+
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasFilter" aria-labelledby="offcanvasFilterLabel">
2+
<div class="offcanvas-header">
3+
<h6>Filter results</h6>
4+
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
5+
</div>
6+
<div class="offcanvas-body">
7+
<h5>Sort tests by...</h5>
8+
<div class="btn-group m-2" role="group" aria-label="Select sorting">
9+
<input type="radio" class="btn-check" name="sort" id="sortstatus" autocomplete="off" checked>
10+
<label class="btn btn-outline-secondary" for="sortstatus">status</label>
11+
<input type="radio" class="btn-check" name="sort" id="sortname" autocomplete="off">
12+
<label class="btn btn-outline-secondary" for="sortname">name</label>
13+
</div>
14+
<h5>Show tests which have...</h5>
15+
<div class="list-group m-2">
16+
<label class="list-group-item">
17+
<input class="form-check-input me-1" type="checkbox" value="">
18+
passed
19+
</label>
20+
<label class="list-group-item">
21+
<input class="form-check-input me-1" type="checkbox" value="">
22+
failed
23+
</label>
24+
<label class="list-group-item">
25+
<input class="form-check-input me-1" type="checkbox" value="">
26+
skipped
27+
</label>
28+
</div>
29+
<div class="list-group m-2">
30+
<label class="list-group-item">
31+
<input class="form-check-input me-1" type="checkbox" value="">
32+
matching images
33+
</label>
34+
<label class="list-group-item">
35+
<input class="form-check-input me-1" type="checkbox" value="">
36+
differing images
37+
</label>
38+
</div>
39+
<div class="list-group m-2 mpl-hash">
40+
<label class="list-group-item">
41+
<input class="form-check-input me-1" type="checkbox" value="">
42+
matching hashes
43+
</label>
44+
<label class="list-group-item">
45+
<input class="form-check-input me-1" type="checkbox" value="">
46+
differing hashes
47+
</label>
48+
</div>
49+
<div class="list-group m-2">
50+
<label class="list-group-item mpl-hash">
51+
<input class="form-check-input me-1" type="checkbox" value="">
52+
no baseline hash
53+
</label>
54+
<label class="list-group-item">
55+
<input class="form-check-input me-1" type="checkbox" value="">
56+
no baseline image
57+
</label>
58+
</div>
59+
<div class="btn-group m-2" role="group" aria-label="Select condition">
60+
<input type="radio" class="btn-check" name="condition" id="conditionand" autocomplete="off" checked>
61+
<label class="btn btn-outline-secondary" for="conditionand">AND</label>
62+
<input type="radio" class="btn-check" name="condition" id="conditionor" autocomplete="off">
63+
<label class="btn btn-outline-secondary" for="conditionor">OR</label>
64+
</div>
65+
</div>
66+
</div>

pytest_mpl/summary/templates/hash.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<nav class="navbar navbar-expand-lg navbar-light bg-light">
2+
<div class="container-fluid">
3+
<a class="navbar-brand" href="https://github.com/matplotlib/pytest-mpl">matplotlib / pytest-mpl</a>
4+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
5+
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
6+
<span class="navbar-toggler-icon"></span>
7+
</button>
8+
<div class="collapse navbar-collapse" id="navbarSupportedContent">
9+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
10+
<!--<li class="nav-item">
11+
<a class="nav-link active" aria-current="page" href="#">Home</a>
12+
</li>
13+
<li class="nav-item">
14+
<a class="nav-link" href="#">Link</a>
15+
</li>
16+
<li class="nav-item dropdown">
17+
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
18+
data-bs-toggle="dropdown" aria-expanded="false">
19+
Dropdown
20+
</a>
21+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
22+
<li><a class="dropdown-item" href="#">Action</a></li>
23+
<li><a class="dropdown-item" href="#">Another action</a></li>
24+
<li>
25+
<hr class="dropdown-divider">
26+
</li>
27+
<li><a class="dropdown-item" href="#">Something else here</a></li>
28+
</ul>
29+
</li>
30+
<li class="nav-item">
31+
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
32+
</li>-->
33+
</ul>
34+
<form class="d-flex">
35+
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
36+
<button class="btn btn-outline-success me-2" type="submit">Search</button>
37+
</form>
38+
<button class="btn btn-outline-primary" aria-label="Filter" type="button" data-bs-toggle="offcanvas"
39+
data-bs-target="#offcanvasFilter" aria-controls="offcanvasFilter">
40+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel"
41+
viewBox="0 0 16 16">
42+
<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
43+
</svg>
44+
</button>
45+
</div>
46+
</div>
47+
</nav>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="py-2 col-sm-6 col-lg-4 col-xxl-3 result {classes}">
2+
<div class="card">
3+
<a class="btn" data-bs-toggle="offcanvas" href="#offcanvas{id}" role="button" aria-controls="offcanvas{id}">{image}</a>
4+
{offcanvas}
5+
<div class="card-body">
6+
<h6><small class="text-muted">{module}</small></h6>
7+
<h5 class="card-title">{test_name}</h5>
8+
<div class="d-flex justify-content-between align-items-center">
9+
{badge}
10+
</div>
11+
</div>
12+
</div>
13+
</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div class="btn-group status-badge" role="group" aria-label="status">
2+
<button class="btn btn-sm btn-{btn_class}" type="button"
3+
data-bs-toggle="offcanvas" data-bs-target="#offcanvas{card_id}" aria-controls="offcanvas{card_id}">
4+
{status}
5+
</button>
6+
{extra_badges}
7+
</div>

0 commit comments

Comments
 (0)