def evaluate(self): element, namespaces = self.element, self.namespaces xpath, tags = self.xpath, self.tags # This may raise XPathEvalError for incorrect namespacing results = element.xpath(xpath, namespaces=namespaces) # xpath does not exist in XML if not results: self.message = "xpath: `{}` does not" " exist in the XML.".format( xpath) return False # xpath exists, no tag lookup -> Pass if not tags: self.message = "xpath: `{}` exists in the XML.".format(xpath) return True data = [] # Tag lookup in xpath for idx, tag in enumerate(tags): try: text = results[idx].text if not text: xml_comp = XMLTagComparison( tag=tag, diff=None, error="No value is found," " although the path exists.", extra=None, ) elif isinstance(tag, str) and re.match(tag, text): extra = tag if tag != text else None xml_comp = XMLTagComparison(tag=text, diff=None, error=None, extra=extra) else: passed, error = comparison.basic_compare(first=text, second=tag) if error: xml_comp = XMLTagComparison(tag=text, diff=None, error=error, extra=tag) elif not passed: xml_comp = XMLTagComparison(tag=text, diff=tag, error=None, extra=None) else: xml_comp = XMLTagComparison(tag=text, diff=None, error=None, extra=tag) except IndexError: xml_comp = XMLTagComparison( tag=None, diff=tag, error="No tags found for the index: {}".format(idx), extra=None, ) data.append(xml_comp) self.data = data return all([comp.passed for comp in self.data])
def compare_rows( table, expected_table, comparison_columns, display_columns, strict=True, fail_limit=0, report_fails_only=False, ): """ Apply row by row comparison of two tables, creating a ``RowComparison`` for each row couple. :param table: Original table. :type table: ``list`` of ``dict`` :param expected_table: Comparison table, it can contain custom comparators as column values. :type expected_table: ``list`` of ``dict`` :param comparison_columns: Columns to be used for comparison. :type comparison_columns: ``list`` of ``str`` :param display_columns: Columns to be used for populating ``RowComparison`` data. :type display_columns: ``list`` of ``str`` :param strict: Custom comparator strictness flag, currently will auto-convert non-str values to ``str`` for pattern if ``False``. :type strict: ``bool`` :param fail_limit: Max number of failures before aborting the comparison run. Useful for large tables, when we want to stop after we have N rows that fail the comparison. :type fail_limit: ``int`` :param report_fails_only: If ``True``, only repoty the failures (used for diff typically) :type report_fails_only: ``bool`` :returns: overall passed status and RowComparison data. """ # We always want to display a superset of comparison columns # otherwise we can have a failing comparison but the # resulting data will not include the mismatch context. if not set(comparison_columns).issubset(display_columns): raise ValueError("comparison_columns ({}) must be " "subset of display_columns ({})".format( ", ".join(sorted(comparison_columns)), ", ".join(sorted(display_columns)), )) data = [] num_failures = 0 display_only = [ col for col in display_columns if col not in comparison_columns ] for idx, (row_1, row_2) in enumerate(zip(table, expected_table)): diff, errors, extra = {}, {}, {} for column_name in comparison_columns: if column_name not in row_1 and column_name not in row_2: continue elif (column_name in row_1 and column_name not in row_2 or column_name not in row_1 and column_name in row_2): diff[column_name] = row_2.get(column_name, None) continue first, second = row_1[column_name], row_2[column_name] passed, error = comparison.basic_compare(first=first, second=second, strict=strict) if error: errors[column_name] = error elif not passed: diff[column_name] = second # Populate extra if values differ (we don't check for equality as # that may have raised an error for incompatible types as well) if first is not second and (error or passed): extra[column_name] = second row_data = [row_1.get(col, None) for col in display_columns] # Need to populate `extra` with values from the second table # they are not being used for comparison but for display. extra.update({col: row_2.get(col, None) for col in display_only}) row_comparison = RowComparison(idx, row_data, diff, errors, extra) if not (report_fails_only and row_comparison.passed): data.append(row_comparison) if not row_comparison.passed: num_failures += 1 if fail_limit > 0 and num_failures >= fail_limit: break return num_failures == 0, data
def compare_rows(table, expected_table, comparison_columns, display_columns, strict=True, fail_limit=0): """ Apply row by row comparison of two tables, creating a ``RowComparison`` for each row couple. :param table: Original table. :type table: ``list`` of ``dict`` :param expected_table: Comparison table, it can contain custom comparators as column values. :type expected_table: ``list`` of ``dict`` :param comparison_columns: Columns to be used for comparison. :type comparison_columns: ``list`` of ``str`` :param display_columns: Columns to be used for populating ``RowComparison`` data. :type display_columns: ``list`` of ``str`` :param strict: Custom comparator strictness flag, currently will auto-convert non-str values to ``str`` for pattern if ``False``. :type strict: ``bool`` :param fail_limit: Max number of failures before aborting the comparison run. Useful for large tables, when we want to stop after we have N rows that fail the comparison. The result will contain only failing comparisons if this argument is nonzero. :type fail_limit: ``int`` :returns: overall passed status and RowComparison data. """ # We always want to display a superset of comparison columns # otherwise we can have a failing comparison but the # resulting data will not include the mismatch context. if not set(comparison_columns).issubset(display_columns): raise ValueError('comparison_columns ({}) must be ' 'subset of display_columns ({})'.format( ', '.join(sorted(comparison_columns)), ', '.join(sorted(display_columns)))) data = [] num_failures = 0 display_only = [ col for col in display_columns if col not in comparison_columns ] for idx, (row_1, row_2) in enumerate(zip(table, expected_table)): diff, errors, extra = {}, {}, {} for column_name in comparison_columns: first, second = row_1[column_name], row_2[column_name] passed, error = comparison.basic_compare(first=first, second=second, strict=strict) if error: errors[column_name] = error elif not passed: diff[column_name] = second # Populate extra if values differ (we don't check for equality # as that may have raised an error for incompatible types as well if first is not second and (error or passed): extra[column_name] = second row_data = [row_1[col] for col in display_columns] # Need to populate extra with values from the # second table, if they are not being used # for comparison but have different values. extra.update({ col: row_2[col] for col in display_only if col in row_2 and row_2[col] != row_1[col] }) row_comparison = RowComparison(idx, row_data, diff, errors, extra) if not row_comparison.passed: num_failures += 1 # Include only failing comparisons if there is a limit if fail_limit > 0: if not row_comparison.passed: data.append(row_comparison) if num_failures >= fail_limit: break else: data.append(row_comparison) return num_failures == 0, data