Skip to content

Commit cae4daa

Browse files
feature: add error_table in log file (#1)
Add filterable error table to log output.
1 parent 601e9d0 commit cae4daa

2 files changed

Lines changed: 113 additions & 2 deletions

File tree

src/xmlvalidator/XmlValidator.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ def __init__(
402402
xsd_path: str | Path | None = None,
403403
base_url: str | None = None,
404404
error_facets: List[str] | None = None,
405-
fail_on_errors: bool = True
405+
fail_on_errors: bool = True,
406406
) -> None:
407407
"""
408408
**Library Scope**
@@ -1460,7 +1460,8 @@ def validate_xml_files( # pylint: disable=R0913:too-many-arguments disable=R0914
14601460
write_to_csv: Optional[bool] = True,
14611461
timestamped: Optional[bool] = True,
14621462
reset_errors: bool = True,
1463-
fail_on_errors: Optional[bool] = None
1463+
fail_on_errors: Optional[bool] = None,
1464+
error_table: Optional[bool] = True
14641465
) -> Tuple[
14651466
List[ Dict[str, Any] ],
14661467
str | None
@@ -1591,6 +1592,11 @@ def validate_xml_files( # pylint: disable=R0913:too-many-arguments disable=R0914
15911592
XML files, one or more errors have been reported. Error
15921593
reporting and exporting will not change.
15931594
1595+
``error_table``
1596+
1597+
If True, writes all collected errors to a filterable table in
1598+
the log file. Defaults to True.
1599+
15941600
**Returns**
15951601
15961602
A tuple, holding:
@@ -1670,6 +1676,11 @@ def validate_xml_files( # pylint: disable=R0913:too-many-arguments disable=R0914
16701676
)
16711677
else:
16721678
csv_path = None
1679+
# Write errors to the log file as a table if requested.
1680+
if error_table and self.validator_results.errors_by_file:
1681+
self.validator_results.write_error_table_to_log(
1682+
self.validator_results.errors_by_file,
1683+
)
16731684
# Log a summary of the test run.
16741685
self.validator_results.log_summary()
16751686
if fail_on_errors and self.validator_results.errors_by_file:

src/xmlvalidator/xml_validator_results.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,60 @@ class ValidatorResultRecorder:
8181
validation_summary: Dict[str, List[str]] = field(
8282
default_factory=lambda: {"valid": [], "invalid": []}
8383
)
84+
# Used to create unique error_tables in the log file if multiple tables are present.
85+
error_table_id = 0
86+
# Used styling to use the same theme as Robot Framework uses.
87+
style_and_filter_script = """
88+
<style>
89+
#table_block_0 {
90+
margin-bottom: -5em;
91+
}
92+
.dataframe th {
93+
background-color: var(--primary-color);
94+
padding: 0.2em 0.3em;
95+
}
96+
.dataframe th, .dataframe td {
97+
border-width: 1px;
98+
border-style: solid;
99+
border-color: var(--secondary-color);
100+
padding: 0.1em 0.3em;
101+
font-family: Helvetica, sans-serif;
102+
}
103+
input.filter {
104+
width: 25em;
105+
background-color: var(--background-color);
106+
border-color: var(--secondary-color);
107+
border-width: 2px;
108+
border-style: solid;
109+
border-radius: 2px;
110+
color: var(--text-color);
111+
margin-left: 0;
112+
font-family: Helvetica, sans-serif;
113+
}
114+
</style>
115+
<script>
116+
function filterTable(blockId) {
117+
const container = document.getElementById("table_block_" + blockId);
118+
const input = container.querySelector("input");
119+
const table = container.querySelector("table");
120+
const filter = input.value.toLowerCase();
121+
const trs = table.getElementsByTagName("tr");
122+
123+
for (let i = 1; i < trs.length; i++) {
124+
const tds = trs[i].getElementsByTagName("td");
125+
let rowVisible = false;
126+
for (let j = 0; j < tds.length; j++) {
127+
const td = tds[j];
128+
if (td && td.textContent.toLowerCase().indexOf(filter) > -1) {
129+
rowVisible = true;
130+
break;
131+
}
132+
}
133+
trs[i].style.display = rowVisible ? "" : "none";
134+
}
135+
}
136+
</script>
137+
"""
84138

85139
def _get_summary(self) -> Dict[str, int]:
86140
"""
@@ -340,6 +394,52 @@ def write_errors_to_csv(self,
340394
) from e
341395
return str( output_csv_path.resolve() )
342396

397+
def write_error_table_to_log(self, errors: List[ Dict[str, Any] ]):
398+
"""
399+
Writes a table of validation errors to the log file.
400+
401+
This method takes a list of error dictionaries and writes them
402+
to the log file in a table format. It also adds an input that
403+
can be used to filter through the errors and updates in real
404+
time.
405+
406+
Args:
407+
408+
- errors (List[Dict[str, Any]]):
409+
A list of dictionaries, where each dictionary contains details
410+
of a validation error. Each key in the dictionaries
411+
corresponds to a column in the output CSV.
412+
413+
Notes:
414+
415+
- If `errors` is an empty list, the method exits early and logs
416+
an informational message without creating a file.
417+
the error dictionaries.
418+
- The method uses `pandas` for CSV generation.
419+
"""
420+
# Return if no errors were passed.
421+
if not errors:
422+
logger.info("No errors to write to log file.")
423+
return
424+
# Convert the errors list to a DataFrame.
425+
df = pd.DataFrame(errors)
426+
# Convert the dataframe to HTML.
427+
df_table = df.to_html(index=False, border=0)
428+
# Get the table id and increment for the next one.
429+
error_table_id = self.error_table_id
430+
self.error_table_id += 1
431+
# Add the filter input to the df_table (includes the function call)
432+
full_html = f"""<div id="table_block_{error_table_id}">
433+
<input class="filter" type="text" onkeyup="filterTable('{error_table_id}')" placeholder="Search validation errors...">
434+
{df_table}
435+
</div>"""
436+
# Add the style and filter script if it is the first table
437+
if error_table_id == 0:
438+
full_html = f"{full_html}{self.style_and_filter_script}"
439+
# Actually print the table to the log file
440+
logger.info(full_html, html=True)
441+
442+
343443
class ValidatorResult: # pylint: disable=R0903:too-few-public-methods
344444
"""
345445
Encapsulates the result of an operation in a success-or-failure format.

0 commit comments

Comments
 (0)