Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions assets/css/taskcat_reporting.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

@import url(https://fonts.googleapis.com/css?family=Roboto:400,500,700,300,100);
@import url(https://fonts.googleapis.com/css?family=Comfortaa:700);
html {} body {

body {
background-color: #ecf0f1;
font-family: "Roboto", helvetica, arial, sans-serif;
font-size: 12px;
Expand Down Expand Up @@ -153,7 +154,7 @@ tr:last-child td:last-child {
}
td {
background: #ffffff;
padding: 12;
padding: 12px;
text-align: left;
vertical-align: middle;
font-weight: 300;
Expand Down
17 changes: 8 additions & 9 deletions taskcat/_cfn/_log_stack_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,12 @@ def write_logs(self, stack: Stack, logpath: Path):
return

if len(cfnlogs) != 0:
if cfnlogs[0]["ResourceStatus"] != "CREATE_COMPLETE":
if "ResourceStatusReason" in cfnlogs[0]:
reason = cfnlogs[0]["ResourceStatusReason"]
else:
reason = "Unknown"
else:
if cfnlogs[0]["ResourceStatus"] == "CREATE_COMPLETE":
reason = "Stack launch was successful"
elif "ResourceStatusReason" in cfnlogs[0]:
reason = cfnlogs[0]["ResourceStatusReason"]
else:
reason = "Unknown"

with open(str(logpath), "a", encoding="utf-8") as log_output:
log_output.write(
Expand All @@ -83,7 +82,7 @@ def write_logs(self, stack: Stack, logpath: Path):
"******************************************************************"
"***********\n"
)
log_output.write("ResourceStatusReason: \n")
log_output.write("ResourceStatusReason:")
log_output.write(textwrap.fill(str(reason), 85) + "\n")
log_output.write(
"******************************************************************"
Expand All @@ -93,7 +92,7 @@ def write_logs(self, stack: Stack, logpath: Path):
"******************************************************************"
"***********\n"
)
log_output.write("Events: \n")
log_output.write("Events:\n")
log_output.writelines(tabulate.tabulate(cfnlogs, headers="keys"))
log_output.write(
"\n****************************************************************"
Expand All @@ -118,5 +117,5 @@ def write_logs(self, stack: Stack, logpath: Path):
self.write_logs(child, logpath)
else:
LOG.error(
"No event logs found. Something went wrong at describe event " "call."
"No event logs found. Something went wrong at describe event call."
)
253 changes: 133 additions & 120 deletions taskcat/_generate_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,136 +12,149 @@
LOG = logging.getLogger(__name__)


REPO_LINK = "https://github.com/aws-ia/taskcat"
DOC_LINK = "http://taskcat.io"


class ReportBuilder:
"""
This class generates the test report.
"""This class generates the test report."""

"""
STYLE_SHEET_URL = "https://raw.githubusercontent.com/aws-ia/taskcat/main/assets/css/taskcat_reporting.css"

def __init__(self, stacks: Stacker, output_file: Path, version: str = "0.9.0"):
def __init__(self, stacks: Stacker, output_file: Path, version: str):
self._stacks = stacks
self._output_file = output_file
self._version = version

# TODO: refactor for readability
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-glenn I may have gone too far with the refactor here. If you don't mind just giving this a quick glance to see if you want me to break it up or not. (I do still need to do a little clean up on the tests.)

As it is right now, I have made the following improvements:

  • fixed HTML validation issues
  • fixed version footer
  • added fallback stylesheet if GH is unreachable (or transient network errors)
  • added R/G colorblind helper icons
  • refactored the yattag functions/contexts to be a bit more concise/readable (and prefered as per docs)

def generate_report( # noqa: C901
self,
): # pylint: disable=too-many-locals, too-many-statements
doc = yattag.Doc()

# Type of cfn log return cfn log file
def get_output_file(region, stack_name, resource_type):
extension = ".txt"
if resource_type == "cfnlog":
location = f"{stack_name}-{region}-cfnlogs{extension}"
return str(location)
if resource_type == "resource_log":
location = f"{stack_name}-{region}-resources{extension}"
return str(location)
return None

def get_teststate(stack: Stack):
rstatus = stack.status
if rstatus == "CREATE_COMPLETE":
status_css = "class=test-green"
elif rstatus == "CREATE_FAILED":
status_css = "class=test-red"
else:
status_css = "class=test-red"
return rstatus, status_css

tag = doc.tag
text = doc.text
logo = "taskcat"
repo_link = "https://github.com/aws-ia/taskcat"
css_url = (
"https://raw.githubusercontent.com/aws-ia/taskcat/main/"
"assets/css/taskcat_reporting.css"
)
output_css = requests.get(css_url, timeout=3).text
doc_link = "http://taskcat.io"
def _get_output_file(self, region, stack_name, resource_type):
"""Determine the output file name to use."""

extension = ".txt"
if resource_type == "cfnlog":
location = f"{stack_name}-{region}-cfnlogs{extension}"
return str(location)
if resource_type == "resource_log":
location = f"{stack_name}-{region}-resources{extension}"
return str(location)
return None

def _get_stack_status_badge(self, stack: Stack):
"""Generate a status and associated class from the stack status."""

status = stack.status
status_class = "test-red"
status_icon = "❌"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some attempt at accessibility for R/G colorblind.

if status == "CREATE_COMPLETE":
status_icon = "✅"
status_class = "test-green"
return f"{status_icon} {status}", status_class

def _get_css(self):
"""Gets the latest CSS for the report from this github repo when building the report."""

stylesheet = """th, td {
border: 1px solid black;
table {
border-collapse: collapse;
}
td.test-green {
background-color: #98FF98;
}
td.test-red {
background-color: #FCB3BC;
}
"""
Comment on lines +55 to +66
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most basic stylesheet to convey the point of the test results with no fluff.

r = requests.get(url=self.__class__.STYLE_SHEET_URL, timeout=3)
try:
r.raise_for_status()
stylesheet = r.text
except requests.exceptions.HTTPError as e:
LOG.error(
f"Failed to fetch stylesheet, falling back to default stylesheet: {e}"
)

return stylesheet

def _render_document(self, report_timestamp):
"""Renders the report document HTML."""

doc, tag, text = yattag.SimpleDoc(stag_end=">").tagtext()
stag = doc.stag
line = doc.line

doc.asis("<!DOCTYPE html>")
with tag("html"):
with tag("head"):
doc.stag("meta", charset="utf-8")
doc.stag("meta", name="viewport", content="width=device-width")
with tag("style", type="text/css"):
text(output_css)
with tag("title"):
text("TaskCat Report")

stag("meta", charset="utf-8")
stag("meta", name="viewport", content="width=device-width")
with tag("style"):
text("\n" + self._get_css())
line("title", "TaskCat Report")
with tag("body"):
tested_on = time.strftime("%A - %b,%d,%Y @ %H:%M:%S")

with tag("table", "class=header-table-fill"):
# Header / Banner
with tag("table", klass="header-table-fill"):
with tag("tbody"):
with tag("th", "colspan=2"):
with tag("tr"):
with tag("td"):
with tag("a", href=repo_link):
text("GitHub Repo: ")
text(repo_link)
doc.stag("br")
with tag("a", href=doc_link):
text("Documentation: ")
text(doc_link)
doc.stag("br")
text("Tested on: ")
text(tested_on)
with tag("td", "class=taskcat-logo"):
with tag("h3"):
text(logo)
doc.stag("p")
with tag("table", "class=table-fill"):
with tag("tbody"):
with tag("thread"):
with tag("tr"):
with tag("th", "class=text-center", "width=25%"):
text("Test Name")
with tag("th", "class=text-left", "width=10%"):
text("Tested Region")
with tag("th", "class=text-left", "width=30%"):
text("Stack Name")
with tag("th", "class=text-left", "width=20%"):
text("Tested Results")
with tag("th", "class=text-left", "width=15%"):
text("Test Logs")

for stack in self._stacks.stacks:
with tag("tr", "class= test-footer"):
with tag("td", "colspan=5"):
text("")

testname = stack.test_name
LOG.info(f"Reporting on {str(stack.id)}")
status, css = get_teststate(stack)

with tag("tr"):
with tag("td", "class=test-info"):
with tag("h3"):
text(testname)
with tag("td", "class=text-left"):
text(stack.region_name)
with tag("td", "class=text-left"):
text(stack.name)
with tag("td", css):
text(str(status))
with tag("td", "class=text-left"):
clog = get_output_file(
stack.region_name, stack.name, "cfnlog"
)
with tag("a", href=clog):
text("View Logs ")
with tag("tr", "class= test-footer"):
with tag("td", "colspan=5"):
vtag = "Generated by taskcat {self._version}"
text(vtag)

doc.stag("p")

html_output = yattag.indent(
doc.getvalue(), indentation=" ", newline="\r\n", indent_text=True
)
with open(str(self._output_file.resolve()), "w", encoding="utf-8") as _f:
_f.write(html_output)
return html_output
with tag("td"):
line("a", f"GitHub Repo: {REPO_LINK}", href=REPO_LINK)
stag("br")
line("a", f"Documentation: {DOC_LINK}", href=DOC_LINK)
stag("br")
text(f"Report Generated: {report_timestamp}")
with tag("td", klass="taskcat-logo"):
line("h3", "taskcat")
with tag("table", klass="table-fill"):
with tag("thead"):
with tag("tr"):
line("th", "Test Name", klass="text-center", width="25%")
line("th", "Tested Region", klass="text-left", width="10%")
line("th", "Stack Name", klass="text-left", width="30%")
line("th", "Tested Results", klass="text-left", width="20%")
line("th", "Test Logs", klass="text-left", width="15%")
# Test Results
with tag("tbody"):
for stack in self._stacks.stacks:
stack: Stack # type hint
LOG.info(f"Reporting on {str(stack.id)}")
with tag("tr"):
with tag("td", klass="test-info"):
line("h3", stack.test_name)
line("td", stack.region_name, klass="text-left")
line("td", stack.name, klass="text-left")
status, status_class = self._get_stack_status_badge(
stack
)
line("td", status, klass=status_class)
with tag("td", klass="text-left"):
log_file = self._get_output_file(
stack.region_name, stack.name, "cfnlog"
)
line("a", "View Logs", href=log_file)
with tag("tr", klass="test-footer"):
line("td", " ", colspan=5)
# Footer
with tag("tfoot"):
with tag("tr", klass="test-footer"):
line(
"td", f"Generated by taskcat {self._version}", colspan=5
)

return doc

def generate_report(
self,
):
"""Generate the report."""

report_timestamp = time.strftime("%A - %b,%d,%Y @ %H:%M:%S")

# Render
doc = self._render_document(report_timestamp)

# Output
html_output = yattag.indent(
doc.getvalue(), indentation=" ", newline="\r\n", indent_text=True
)
with open(str(self._output_file.resolve()), "w", encoding="utf-8") as _f:
_f.write(html_output)
return html_output
7 changes: 6 additions & 1 deletion taskcat/testing/_cfn_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# pylint: disable=line-too-long
import importlib.metadata
import logging
from pathlib import Path
from typing import List as ListType, Union
Expand Down Expand Up @@ -197,10 +198,14 @@ def report(
cfn_logs = _CfnLogTools()
cfn_logs.createcfnlogs(self.test_definition, report_path)
ReportBuilder(
self.test_definition, report_path / "index.html"
self.test_definition, report_path / "index.html", get_installed_version()
).generate_report()


def get_installed_version():
return importlib.metadata.version(__package__ or __name__)
Comment on lines +205 to +206
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took this from _cli.py. I had issues when using that version directly and when attempting to pull it out into _common_utils.py.

I might give this part one more attempt and then ask for help.



def _trim_regions(regions, config):
if regions != "ALL":
for test in config.config.tests.values():
Expand Down
7 changes: 6 additions & 1 deletion tests/testing_module/test_cfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,15 @@ def test_end_keep_failed(self, mock_config: mm):

self.assertTrue("One or more stacks failed to create:" in str(ex.exception))

@patch("taskcat.testing._cfn_test.get_installed_version")
@patch("taskcat.testing._cfn_test.Config")
@patch("taskcat.testing._cfn_test._CfnLogTools")
@patch("taskcat.testing._cfn_test.ReportBuilder")
def test_report(self, mock_report: mm, mock_log: mm, mock_config: mm):
def test_report(
self, mock_report: mm, mock_log: mm, mock_config: mm, mock_version: mm
):
mock_version.return_value = "1.0.0"

cfn_test = CFNTest(mock_config())

td_mock = MagicMock()
Expand Down