manim/tests/utils/logging_tester.py
Oll-iver 8a5267a9ee
Enable strict=True for zip() where safe (#4547)
* sub_alphas is derived directly from to_update so they're guaranteed to be of the same length.

* self.shapes is initialised as a direct copy of the mobject, guaranteed to be of same length.

* linspace in this case guarantees both arrays are of equal size (self.n_segments).

* Any transformation already requires that each datapoint in the first tuple has a corresponding datapoint in the second (ie same length)

* Replaced arange with linspace, eliminates risk of floating point errors and forces rgbas and offset to be the same size for strict=True

* all_arc_configs is either defined specifically by length of point_pairs or strictly forced to be the same length (n). In any case they'll always be the same length so strict=True works.

* There should always be an equal amount of start and end anchors; radius_list is defined directly from the length of vertex_group; both outer_vertices and inner_vertices posess n vertices.

* boundary_times always contains has an even length so both 'slices' in the zup function are the same length.

* colors_in_gradient is defined to be the same length as p_list_complete; labels and parts are seemingly user inputs with no guarantee of equal length; val_range is defined to be same lenght as self.bar_names; however there's no authentication that self.values has a fixed length after it's been defined ie user can append to the list creating a mismatch between len(self.values) and len(self.bars)

* In most cases here, the tuples are either defined to be of same length or manipulated to be by the align_data function. In the match_points function there is currently no validation to ensure both mobjects are the same length.

* Reverting _add_x_axis_labels() zip() function back
to strict=False due to failing test cases

* Reverted strict zip usage

* color_gradient is defined to be same length as p_list_complete & within _add_x_axis_labels we define val_range to be the same length as self.bar_names

* align_data and lock_matching_data have no validation to ensure tuples in the zip() function are of the same length. Every other time zip() is used here it is generally immediately manipulating or explicitly defining the tuples to be of same length

* All tuples in zip() functions here are either clearly the same size or manipulated to be the same size using the make_even function.

* The tuples in the zip() function will clearly be of equal length, the second tuple is simply a cyclic shift of the first.

* In the ingest_submobjects function arrays is a one to one mapping of attrs so they are guaranteed to have equal lengths.

* Every usage of zip() consists of tuples that are either manipulated to be equal size or defined to be equal size.

* the zip() function in bezier_remap will always consist of equal length tuples as current_number_of_curves is read directly from the shape of bezier_tuples and is used to dictate the size of split_factors.

* The zip() function color_gradient() will always consist of equal length tuples as floors is defined directly from alphas (which also defines alphas_mod1)

* The tuples in the zip() function in adjacent_n_tuples will always be the same length so strict=True.

* The find_intersection() contains a zip() function that has been set to strict=True. While it is technically possible to pass tuples to this function that are *not* the same length, this would result in generally unexpected behaviour anyway.

* Changed zip() function to have strict=True in __init__() as custom_labels is dependent on tick_range so guaranteed to have the same length.

* Several instances of zip() set to strict=True. In add_coordinates we have axis manipulated to be the same length as tick_range. In get_riemann_rectangles() we have colors dependent on of x_range_Array forcing them to be the same length. Finally in plot_line_graph()  it is clearly intended that all inputs used in the zip() function are of the same length (except possibly z which may not exist and will be made equal length to x); while it is not guaranteed they will be the same length this would cause unintended behaviour.

* zip() function bool changed to strict=True in all these test cases. Most test cases either a) hardcode two things to be the same length, b) verify things are the same length before the function or c) explicitly exist to check whether two things are the same length.
2026-02-11 10:39:25 +01:00

100 lines
4 KiB
Python

from __future__ import annotations
import itertools
import json
import os
from functools import wraps
from pathlib import Path
import pytest
def _check_logs(reference_logfile_path: Path, generated_logfile_path: Path) -> None:
with reference_logfile_path.open() as reference_logfile:
reference_logs = reference_logfile.readlines()
with generated_logfile_path.open() as generated_logfile:
generated_logs = generated_logfile.readlines()
diff = abs(len(reference_logs) - len(generated_logs))
if len(reference_logs) != len(generated_logs):
msg_assert = ""
if len(reference_logs) > len(generated_logs):
msg_assert += f"Logs generated are SHORTER than the expected logs. There are {diff} extra logs.\n"
msg_assert += "Last log of the generated log is : \n"
msg_assert += generated_logs[-1]
else:
msg_assert += f"Logs generated are LONGER than the expected logs.\n There are {diff} extra logs :\n"
for log in generated_logs[len(reference_logs) :]:
msg_assert += log
msg_assert += f"\nPath of reference log: {reference_logfile}\nPath of generated logs: {generated_logfile}"
pytest.fail(msg_assert)
for index, ref, gen in zip(
itertools.count(), reference_logs, generated_logs, strict=False
):
# As they are string, we only need to check if they are equal. If they are not, we then compute a more precise difference, to debug.
if ref == gen:
continue
ref_log = json.loads(ref)
gen_log = json.loads(gen)
diff_keys = [
d1[0]
for d1, d2 in zip(ref_log.items(), gen_log.items(), strict=False)
if d1[1] != d2[1]
]
# \n and \t don't not work in f-strings.
newline = "\n"
tab = "\t"
assert len(diff_keys) == 0, (
f"Logs don't match at {index} log. : \n{newline.join([f'In {key} field, got -> {newline}{tab}{repr(gen_log[key])}. {newline}Expected : -> {newline}{tab}{repr(ref_log[key])}.' for key in diff_keys])}"
+ f"\nPath of reference log: {reference_logfile}\nPath of generated logs: {generated_logfile}"
)
def logs_comparison(
control_data_file: str | os.PathLike, log_path_from_media_dir: str | os.PathLike
):
"""Decorator used for any test that needs to check logs.
Parameters
----------
control_data_file
Name of the control data file, i.e. .log that will be compared to the outputted logs.
.. warning:: You don't have to pass the path here.
.. example:: "SquareToCircleWithLFlag.log"
log_path_from_media_dir
The path of the .log generated, from the media dir. Example: /logs/Square.log.
Returns
-------
Callable[[Any], Any]
The test wrapped with which we are going to make the comparison.
"""
control_data_file = Path(control_data_file)
log_path_from_media_dir = Path(log_path_from_media_dir)
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
# NOTE : Every args goes seemingly in kwargs instead of args; this is perhaps Pytest.
result = f(*args, **kwargs)
tmp_path = kwargs["tmp_path"]
tests_directory = Path(__file__).absolute().parent.parent
control_data_path = (
tests_directory / "control_data" / "logs_data" / control_data_file
)
path_log_generated = tmp_path / log_path_from_media_dir
# The following will say precisely which subdir does not exist.
if not path_log_generated.exists():
for parent in reversed(path_log_generated.parents):
if not parent.exists():
pytest.fail(
f"'{parent.name}' does not exist in '{parent.parent}' (which exists). ",
)
break
_check_logs(control_data_path, path_log_generated)
return result
return wrapper
return decorator