Select Git revision
test_apiutils.py
-
Henrik tom Wörden authoredHenrik tom Wörden authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
utils.py 7.80 KiB
# encoding: utf-8
#
# This file is a part of the LinkAhead Project.
#
# Copyright (C) 2024 IndiScale GmbH <info@indiscale.com>
# Copyright (C) 2024 Daniel Hornung <d.hornung@indiscale.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Utilities for the tests.
"""
from datetime import datetime
from typing import Iterable, Optional, Union
from openpyxl import Workbook
def assert_equal_jsons(json1, json2, allow_none: bool = True, allow_empty: bool = True,
ignore_datetime: bool = False, ignore_id_value: bool = False,
allow_name_dict: bool = False,
path: Optional[list] = None) -> None:
"""Compare two json objects for near equality.
Raise an assertion exception if they are not equal.
Parameters
----------
allow_name_dict: bool, default=False
If True, a string and a dict ``{"name": "string's value"}`` are considered equal.
"""
if path is None:
path = []
assert isinstance(json1, dict) == isinstance(json2, dict), f"Type mismatch, path: {path}"
if isinstance(json1, dict):
keys = set(json1.keys()).union(json2.keys())
for key in keys:
this_path = path + [key]
# Case 1: exists in both collections
if key in json1 and key in json2:
el1 = json1[key]
el2 = json2[key]
if allow_none and (el1 is None and (el2 == [] or el2 == {})
or el2 is None and (el1 == [] or el1 == {})):
# shortcut in case of equivalent empty content
continue
if allow_name_dict: # Special exception
my_str = None
if isinstance(el1, str) and isinstance(el2, dict):
my_str = el1
my_dict = el2
elif isinstance(el2, str) and isinstance(el1, dict):
my_str = el2
my_dict = el1
if my_str is not None:
if len(my_dict) == 1 and my_dict.get("name") == my_str:
continue
assert isinstance(el1, type(el2)), f"Type mismatch, path: {this_path}"
if isinstance(el1, (dict, list)):
# Iterables: Recursion
assert_equal_jsons(
el1, el2, allow_none=allow_none, allow_empty=allow_empty,
ignore_datetime=ignore_datetime, ignore_id_value=ignore_id_value,
allow_name_dict=allow_name_dict,
path=this_path)
continue
if not (ignore_id_value and key == "id"):
assert equals_with_casting(el1, el2, ignore_datetime=ignore_datetime), (
f"Values at path {this_path} are not equal:\n{el1},\n{el2}")
continue
# Case 2: exists only in one collection
existing = json1.get(key, json2.get(key))
assert ((allow_none and _is_recursively_none(existing))
or (allow_empty and existing == [])), (
f"Element at path {this_path} is None or empty in one json and does not exist in "
"the other.")
return
assert isinstance(json1, list) and isinstance(json2, list), f"Is not a list, path: {path}"
assert len(json1) == len(json2), (f"Lists must have equal length, path: {path}\n"
f"{json1}\n ---\n{json2}")
for idx, (el1, el2) in enumerate(zip(json1, json2)):
this_path = path + [idx]
if isinstance(el1, dict):
assert_equal_jsons(el1, el2, allow_none=allow_none, allow_empty=allow_empty,
ignore_datetime=ignore_datetime, ignore_id_value=ignore_id_value,
allow_name_dict=allow_name_dict,
path=this_path)
else:
assert equals_with_casting(el1, el2), (
f"Values at path {this_path} are not equal:\n{el1},\n{el2}")
def equals_with_casting(value1, value2, ignore_datetime: bool = False) -> bool:
"""Compare two values, return True if equal, False otherwise. Try to cast to clever datatypes.
"""
try:
dt1 = datetime.fromisoformat(value1)
dt2 = datetime.fromisoformat(value2)
if ignore_datetime:
return True
return dt1 == dt2
except (ValueError, TypeError):
pass
return value1 == value2
def purge_from_json(data: Union[dict, list], remove_keys: list[str]) -> Union[dict, list]:
"""Remove matching entries from json data.
Parameters
----------
data : Union[dict, list]
The json data to clean.
remove_keys : list[str]
Remove all keys that are in this list
Returns
-------
out : Union[dict, list]
The cleaned result.
"""
# Remove only from dicts
if isinstance(data, dict):
keys = set(data.keys())
for removable in remove_keys:
if removable in keys:
data.pop(removable)
elements = list(data.values())
else:
if not isinstance(data, list):
raise ValueError("Data must be a dict or list.")
elements = data
# Recurse for all elements
for element in elements:
if isinstance(element, dict) or isinstance(element, list):
purge_from_json(element, remove_keys=remove_keys)
return data
def compare_workbooks(wb1: Workbook, wb2: Workbook, hidden: bool = True):
"""Compare two workbooks for equal content.
Raises an error if differences are found.
Parameters
----------
hidden: bool, optional
Test if the "hidden" status of rows and columns is the same.
"""
assert wb1.sheetnames == wb2.sheetnames, (
f"Sheet names are different: \n{wb1.sheetnames}\n !=\n{wb2.sheetnames}"
)
for sheetname in wb2.sheetnames:
sheet_1 = wb1[sheetname]
sheet_2 = wb2[sheetname]
for irow, (row1, row2) in enumerate(zip(sheet_1.iter_rows(), sheet_2.iter_rows())):
if hidden:
assert (sheet_1.row_dimensions[irow].hidden
== sheet_2.row_dimensions[irow].hidden), f"hidden row: {sheetname}, {irow}"
for icol, (cell1, cell2) in enumerate(zip(row1, row2)):
if hidden:
assert (sheet_1.column_dimensions[cell1.column_letter].hidden
== sheet_2.column_dimensions[cell2.column_letter].hidden), (
f"hidden col: {sheetname}, {icol}")
assert cell1.value == cell2.value, (
f"Sheet: {sheetname}, cell: {cell1.coordinate}, Values: \n"
f"{cell1.value}\n{cell2.value}"
)
def _is_recursively_none(obj: Union[list, dict, None] = None):
"""Test if ``obj`` is None or recursively consists only of None-like objects."""
if obj is None:
return True
if isinstance(obj, (list, dict)):
if isinstance(obj, list):
mylist: Iterable = obj
else:
mylist = obj.values()
for element in mylist:
if not _is_recursively_none(element):
return False
return True
return False