Skip to content

Commit b4de309

Browse files
committed
add release checklist
1 parent b5e4e85 commit b4de309

File tree

4 files changed

+266
-0
lines changed

4 files changed

+266
-0
lines changed

tests/run_release_checklist.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
Runs the release checklist.
3+
"""
4+
5+
import html
6+
import os
7+
from datetime import datetime
8+
from string import Template
9+
import wx
10+
from src.app_data import AppData
11+
import tests
12+
from tests.unit_tests.test_checklist import view_checklist
13+
from tests.unit_tests.test_checklist.view_checklist import ViewCheckList
14+
15+
16+
ITEMS = [
17+
{"label": "All issues in GitHub fixed", "type": bool, "pass_if": True, "result": None},
18+
{"label": "No Pylint issues", "type": bool, "pass_if": True, "result": None},
19+
{"label": "All unit tests passed", "type": bool, "pass_if": True, "result": None},
20+
{"label": "All test configuration runs passed", "type": bool, "pass_if": True, "result": None},
21+
{"label": "Duration test passed", "type": bool, "pass_if": True, "result": None},
22+
{"label": "Correct version in AppData", "type": bool, "pass_if": True, "result": None},
23+
{"label": "Documentation up to date", "type": bool, "pass_if": True, "result": None},
24+
{"label": "Set tag in git", "type": bool, "pass_if": True, "result": None},
25+
{"label": "Create deployment", "type": bool, "pass_if": True, "result": None},
26+
{"label": "Publish deployment on LilyTronics", "type": bool, "pass_if": True, "result": None},
27+
{"label": "Publish deployment on GitHub", "type": bool, "pass_if": True, "result": None}
28+
]
29+
REPORT_TIME_STAMP_FORMAT = "%Y%m%d_%H%M%S"
30+
REPORT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
31+
32+
33+
def _get_result_for_item(item, check):
34+
result = f"unknown type: {item["type"]}"
35+
if item["type"] is bool:
36+
if item["pass_if"] == check.GetValue():
37+
result = "PASSED"
38+
else:
39+
result = "FAILED"
40+
return result
41+
42+
43+
def check_callback(checks, remarks):
44+
now = datetime.now()
45+
timestamp = now.strftime(REPORT_TIME_STAMP_FORMAT)
46+
47+
template_values = {
48+
"start_message": f"Release checklist for application version: V{AppData.VERSION}",
49+
"date": now.strftime(REPORT_DATE_FORMAT),
50+
"result": "",
51+
"result_class": "",
52+
"result_message": "",
53+
"checklist_results": ""
54+
}
55+
56+
results = '<table class="results">\n'
57+
results += '<tr><th>Test</th><th class="center">Result</th><th>Remarks</th>\n'
58+
n_passed = 0
59+
for i, item in enumerate(ITEMS):
60+
result = _get_result_for_item(item, checks[i])
61+
if result == "PASSED":
62+
n_passed += 1
63+
results += f'<tr class="{result.lower()}">\n'
64+
results += f"<td>{html.escape(item["label"])}</td>"
65+
results += f'<td class="center">{html.escape(result)}</td>'
66+
results += f"<td>{html.escape(remarks[i].GetValue().strip())}</td>"
67+
results += "</tr>\n"
68+
results += "</table>"
69+
70+
template_values["result"] = "FAILED"
71+
if n_passed == len(ITEMS):
72+
template_values["result"] = "PASSED"
73+
ratio = 100 * n_passed / len(ITEMS)
74+
template_values["result_message"] = f"{n_passed} of {len(ITEMS)} passed ({ratio:.1f}%)"
75+
template_values["checklist_results"] = results
76+
template_values["result_class"] = template_values["result"].lower()
77+
78+
template_filename = os.path.join(os.path.dirname(view_checklist.__file__),
79+
"html_report_template.html")
80+
report_filename = os.path.join(os.path.dirname(tests.__file__), "test_reports",
81+
f"{timestamp}_release_checklist.html")
82+
with open(template_filename, "r", encoding="utf-8") as fp:
83+
template = fp.read()
84+
with open(report_filename, "w", encoding="utf-8") as fp:
85+
fp.write(Template(template).substitute(template_values))
86+
87+
88+
def show_checklist():
89+
app = wx.App(redirect=False)
90+
ViewCheckList(ITEMS, AppData.VERSION, check_callback).Show()
91+
app.MainLoop()
92+
93+
94+
if __name__ == "__main__":
95+
96+
import pylint
97+
98+
show_checklist()
99+
pylint.run_pylint([__file__])

tests/unit_tests/test_checklist/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
<style>
7+
html {
8+
margin: 0px;
9+
padding: 0px;
10+
}
11+
12+
body {
13+
margin: 0px;
14+
padding: 0px;
15+
font-family: sans-serif;
16+
font-size: 15px;
17+
line-height: 1.5;
18+
}
19+
20+
header {
21+
padding: 8px;
22+
background-color: #666;
23+
color: #fff;
24+
font-size: 1.5em;
25+
}
26+
27+
div {
28+
padding: 8px;
29+
}
30+
31+
span {
32+
padding: 2px 4px;
33+
}
34+
35+
table {
36+
border-collapse: collapse;
37+
}
38+
39+
td {
40+
padding: 0px 4px;
41+
}
42+
43+
th {
44+
color: #fff;
45+
background-color: #666;
46+
text-align: left;
47+
}
48+
49+
table.results {
50+
width: 80%
51+
}
52+
53+
table.results td, th {
54+
padding: 4px 8px;
55+
border: 1px solid #666;
56+
}
57+
58+
.center {
59+
text-align: center;
60+
}
61+
62+
.failed {
63+
background-color: #f66;
64+
}
65+
66+
.passed {
67+
background-color: #0c0;
68+
}
69+
</style>
70+
<title>$date Checklist $result</title>
71+
</head>
72+
<body>
73+
<header>
74+
$date - Checklist: $result
75+
</header>
76+
<div>
77+
<p>$start_message</p>
78+
<table>
79+
<tr><td>Date:</td><td>$date</td></tr>
80+
<tr><td>Result:</td><td><span class="$result_class">$result</span> $result_message</td></tr>
81+
</table>
82+
</div>
83+
84+
<div>
85+
$checklist_results
86+
</div>
87+
88+
<p>&nbsp;</p>
89+
</body>
90+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""
2+
Release checklist.
3+
"""
4+
5+
import wx
6+
7+
8+
class ViewCheckList(wx.Frame):
9+
10+
_GAP = 10
11+
_REMARKS_SIZE = (200, -1)
12+
_INITIAL_WIDTH = 500
13+
14+
def __init__(self, test_items, app_version, check_callback):
15+
self._remarks = []
16+
self._check = []
17+
self._check_callback = check_callback
18+
super().__init__(None, wx.ID_ANY, f"Release Checklist - Application V{app_version}")
19+
panel = wx.Panel(self)
20+
21+
grid = wx.GridBagSizer(self._GAP, self._GAP)
22+
grid.Add(wx.StaticText(panel, wx.ID_ANY, "Test"), (0, 0), wx.DefaultSpan)
23+
grid.Add(wx.StaticText(panel, wx.ID_ANY, "Result"), (0, 1), wx.DefaultSpan,
24+
wx.ALIGN_CENTER_HORIZONTAL)
25+
grid.Add(wx.StaticText(panel, wx.ID_ANY, "Remarks"), (0, 2), wx.DefaultSpan)
26+
for i, item in enumerate(test_items):
27+
lbl = wx.StaticText(panel, wx.ID_ANY, item["label"])
28+
if item["type"] is bool:
29+
ctrl = wx.CheckBox(panel, wx.ID_ANY)
30+
else:
31+
ctrl = wx.StaticText(panel, wx.ID_ANY, f"Invalid type: {item["type"]}")
32+
self._check.append(ctrl)
33+
self._remarks.append(wx.TextCtrl(panel, wx.ID_ANY, "", size=self._REMARKS_SIZE))
34+
row = i + 1
35+
grid.Add(lbl, (row, 0), wx.DefaultSpan, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
36+
grid.Add(self._check[i], (row, 1), wx.DefaultSpan, wx.EXPAND | wx.ALIGN_CENTER)
37+
grid.Add(self._remarks[i], (row, 2), wx.DefaultSpan,
38+
wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
39+
grid.AddGrowableCol(0)
40+
41+
btn = wx.Button(panel, wx.ID_ANY, "Check")
42+
btn.Bind(wx.EVT_BUTTON, self._on_check)
43+
44+
box = wx.BoxSizer(wx.VERTICAL)
45+
box.Add(grid, 1, wx.EXPAND | wx.ALL, self._GAP)
46+
box.Add(btn, 0, wx.CENTER | wx.ALL, self._GAP)
47+
panel.SetSizer(box)
48+
height = 120
49+
if len(test_items) > 0:
50+
height += len(test_items) * (self._remarks[0].GetSize()[1] + self._GAP)
51+
self.SetInitialSize((self._INITIAL_WIDTH, height))
52+
53+
##################
54+
# Event handlers #
55+
##################
56+
57+
def _on_check(self, event):
58+
self._check_callback(self._check, self._remarks)
59+
event.Skip()
60+
61+
62+
if __name__ == "__main__":
63+
64+
import pylint
65+
66+
_test_items = [
67+
{"label": "Test item boolean", "type": bool, "pass_if": True, "result": None},
68+
]
69+
70+
def _check_callback(*args):
71+
print(args)
72+
73+
app = wx.App(redirect=False)
74+
ViewCheckList(_test_items, "99.99", _check_callback).Show()
75+
app.MainLoop()
76+
77+
pylint.run_pylint([__file__])

0 commit comments

Comments
 (0)