CoolFace
Apppublic

declare-lab/tango2

sourceHugging Faceupdated 2y agoView on Hugging Face
92likes
testing_utils.py499 linesDownload Raw Back to utils
1import inspect2import logging3import os4import random5import re6import tempfile7import unittest8import urllib.parse9from distutils.util import strtobool10from io import BytesIO, StringIO11from pathlib import Path12from typing import List, Optional, Union13 14import numpy as np15import PIL.Image16import PIL.ImageOps17import requests18from packaging import version19 20from .import_utils import (21    BACKENDS_MAPPING,22    is_compel_available,23    is_flax_available,24    is_note_seq_available,25    is_onnx_available,26    is_opencv_available,27    is_torch_available,28    is_torch_version,29)30from .logging import get_logger31 32 33global_rng = random.Random()34 35logger = get_logger(__name__)36 37if is_torch_available():38    import torch39 40    if "DIFFUSERS_TEST_DEVICE" in os.environ:41        torch_device = os.environ["DIFFUSERS_TEST_DEVICE"]42 43        available_backends = ["cuda", "cpu", "mps"]44        if torch_device not in available_backends:45            raise ValueError(46                f"unknown torch backend for diffusers tests: {torch_device}. Available backends are:"47                f" {available_backends}"48            )49        logger.info(f"torch_device overrode to {torch_device}")50    else:51        torch_device = "cuda" if torch.cuda.is_available() else "cpu"52        is_torch_higher_equal_than_1_12 = version.parse(53            version.parse(torch.__version__).base_version54        ) >= version.parse("1.12")55 56        if is_torch_higher_equal_than_1_12:57            # Some builds of torch 1.12 don't have the mps backend registered. See #892 for more details58            mps_backend_registered = hasattr(torch.backends, "mps")59            torch_device = "mps" if (mps_backend_registered and torch.backends.mps.is_available()) else torch_device60 61 62def torch_all_close(a, b, *args, **kwargs):63    if not is_torch_available():64        raise ValueError("PyTorch needs to be installed to use this function.")65    if not torch.allclose(a, b, *args, **kwargs):66        assert False, f"Max diff is absolute {(a - b).abs().max()}. Diff tensor is {(a - b).abs()}."67    return True68 69 70def print_tensor_test(tensor, filename="test_corrections.txt", expected_tensor_name="expected_slice"):71    test_name = os.environ.get("PYTEST_CURRENT_TEST")72    if not torch.is_tensor(tensor):73        tensor = torch.from_numpy(tensor)74 75    tensor_str = str(tensor.detach().cpu().flatten().to(torch.float32)).replace("\n", "")76    # format is usually:77    # expected_slice = np.array([-0.5713, -0.3018, -0.9814, 0.04663, -0.879, 0.76, -1.734, 0.1044, 1.161])78    output_str = tensor_str.replace("tensor", f"{expected_tensor_name} = np.array")79    test_file, test_class, test_fn = test_name.split("::")80    test_fn = test_fn.split()[0]81    with open(filename, "a") as f:82        print(";".join([test_file, test_class, test_fn, output_str]), file=f)83 84 85def get_tests_dir(append_path=None):86    """87    Args:88        append_path: optional path to append to the tests dir path89    Return:90        The full path to the `tests` dir, so that the tests can be invoked from anywhere. Optionally `append_path` is91        joined after the `tests` dir the former is provided.92    """93    # this function caller's __file__94    caller__file__ = inspect.stack()[1][1]95    tests_dir = os.path.abspath(os.path.dirname(caller__file__))96 97    while not tests_dir.endswith("tests"):98        tests_dir = os.path.dirname(tests_dir)99 100    if append_path:101        return os.path.join(tests_dir, append_path)102    else:103        return tests_dir104 105 106def parse_flag_from_env(key, default=False):107    try:108        value = os.environ[key]109    except KeyError:110        # KEY isn't set, default to `default`.111        _value = default112    else:113        # KEY is set, convert it to True or False.114        try:115            _value = strtobool(value)116        except ValueError:117            # More values are supported, but let's keep the message simple.118            raise ValueError(f"If set, {key} must be yes or no.")119    return _value120 121 122_run_slow_tests = parse_flag_from_env("RUN_SLOW", default=False)123_run_nightly_tests = parse_flag_from_env("RUN_NIGHTLY", default=False)124 125 126def floats_tensor(shape, scale=1.0, rng=None, name=None):127    """Creates a random float32 tensor"""128    if rng is None:129        rng = global_rng130 131    total_dims = 1132    for dim in shape:133        total_dims *= dim134 135    values = []136    for _ in range(total_dims):137        values.append(rng.random() * scale)138 139    return torch.tensor(data=values, dtype=torch.float).view(shape).contiguous()140 141 142def slow(test_case):143    """144    Decorator marking a test as slow.145 146    Slow tests are skipped by default. Set the RUN_SLOW environment variable to a truthy value to run them.147 148    """149    return unittest.skipUnless(_run_slow_tests, "test is slow")(test_case)150 151 152def nightly(test_case):153    """154    Decorator marking a test that runs nightly in the diffusers CI.155 156    Slow tests are skipped by default. Set the RUN_NIGHTLY environment variable to a truthy value to run them.157 158    """159    return unittest.skipUnless(_run_nightly_tests, "test is nightly")(test_case)160 161 162def require_torch(test_case):163    """164    Decorator marking a test that requires PyTorch. These tests are skipped when PyTorch isn't installed.165    """166    return unittest.skipUnless(is_torch_available(), "test requires PyTorch")(test_case)167 168 169def require_torch_2(test_case):170    """171    Decorator marking a test that requires PyTorch 2. These tests are skipped when it isn't installed.172    """173    return unittest.skipUnless(is_torch_available() and is_torch_version(">=", "2.0.0"), "test requires PyTorch 2")(174        test_case175    )176 177 178def require_torch_gpu(test_case):179    """Decorator marking a test that requires CUDA and PyTorch."""180    return unittest.skipUnless(is_torch_available() and torch_device == "cuda", "test requires PyTorch+CUDA")(181        test_case182    )183 184 185def skip_mps(test_case):186    """Decorator marking a test to skip if torch_device is 'mps'"""187    return unittest.skipUnless(torch_device != "mps", "test requires non 'mps' device")(test_case)188 189 190def require_flax(test_case):191    """192    Decorator marking a test that requires JAX & Flax. These tests are skipped when one / both are not installed193    """194    return unittest.skipUnless(is_flax_available(), "test requires JAX & Flax")(test_case)195 196 197def require_compel(test_case):198    """199    Decorator marking a test that requires compel: https://github.com/damian0815/compel. These tests are skipped when200    the library is not installed.201    """202    return unittest.skipUnless(is_compel_available(), "test requires compel")(test_case)203 204 205def require_onnxruntime(test_case):206    """207    Decorator marking a test that requires onnxruntime. These tests are skipped when onnxruntime isn't installed.208    """209    return unittest.skipUnless(is_onnx_available(), "test requires onnxruntime")(test_case)210 211 212def require_note_seq(test_case):213    """214    Decorator marking a test that requires note_seq. These tests are skipped when note_seq isn't installed.215    """216    return unittest.skipUnless(is_note_seq_available(), "test requires note_seq")(test_case)217 218 219def load_numpy(arry: Union[str, np.ndarray], local_path: Optional[str] = None) -> np.ndarray:220    if isinstance(arry, str):221        # local_path = "/home/patrick_huggingface_co/"222        if local_path is not None:223            # local_path can be passed to correct images of tests224            return os.path.join(local_path, "/".join([arry.split("/")[-5], arry.split("/")[-2], arry.split("/")[-1]]))225        elif arry.startswith("http://") or arry.startswith("https://"):226            response = requests.get(arry)227            response.raise_for_status()228            arry = np.load(BytesIO(response.content))229        elif os.path.isfile(arry):230            arry = np.load(arry)231        else:232            raise ValueError(233                f"Incorrect path or url, URLs must start with `http://` or `https://`, and {arry} is not a valid path"234            )235    elif isinstance(arry, np.ndarray):236        pass237    else:238        raise ValueError(239            "Incorrect format used for numpy ndarray. Should be an url linking to an image, a local path, or a"240            " ndarray."241        )242 243    return arry244 245 246def load_pt(url: str):247    response = requests.get(url)248    response.raise_for_status()249    arry = torch.load(BytesIO(response.content))250    return arry251 252 253def load_image(image: Union[str, PIL.Image.Image]) -> PIL.Image.Image:254    """255    Args:256    Loads `image` to a PIL Image.257        image (`str` or `PIL.Image.Image`):258            The image to convert to the PIL Image format.259    Returns:260        `PIL.Image.Image`: A PIL Image.261    """262    if isinstance(image, str):263        if image.startswith("http://") or image.startswith("https://"):264            image = PIL.Image.open(requests.get(image, stream=True).raw)265        elif os.path.isfile(image):266            image = PIL.Image.open(image)267        else:268            raise ValueError(269                f"Incorrect path or url, URLs must start with `http://` or `https://`, and {image} is not a valid path"270            )271    elif isinstance(image, PIL.Image.Image):272        image = image273    else:274        raise ValueError(275            "Incorrect format used for image. Should be an url linking to an image, a local path, or a PIL image."276        )277    image = PIL.ImageOps.exif_transpose(image)278    image = image.convert("RGB")279    return image280 281 282def export_to_video(video_frames: List[np.ndarray], output_video_path: str = None) -> str:283    if is_opencv_available():284        import cv2285    else:286        raise ImportError(BACKENDS_MAPPING["opencv"][1].format("export_to_video"))287    if output_video_path is None:288        output_video_path = tempfile.NamedTemporaryFile(suffix=".mp4").name289 290    fourcc = cv2.VideoWriter_fourcc(*"mp4v")291    h, w, c = video_frames[0].shape292    video_writer = cv2.VideoWriter(output_video_path, fourcc, fps=8, frameSize=(w, h))293    for i in range(len(video_frames)):294        img = cv2.cvtColor(video_frames[i], cv2.COLOR_RGB2BGR)295        video_writer.write(img)296    return output_video_path297 298 299def load_hf_numpy(path) -> np.ndarray:300    if not path.startswith("http://") or path.startswith("https://"):301        path = os.path.join(302            "https://huggingface.co/datasets/fusing/diffusers-testing/resolve/main", urllib.parse.quote(path)303        )304 305    return load_numpy(path)306 307 308# --- pytest conf functions --- #309 310# to avoid multiple invocation from tests/conftest.py and examples/conftest.py - make sure it's called only once311pytest_opt_registered = {}312 313 314def pytest_addoption_shared(parser):315    """316    This function is to be called from `conftest.py` via `pytest_addoption` wrapper that has to be defined there.317 318    It allows loading both `conftest.py` files at once without causing a failure due to adding the same `pytest`319    option.320 321    """322    option = "--make-reports"323    if option not in pytest_opt_registered:324        parser.addoption(325            option,326            action="store",327            default=False,328            help="generate report files. The value of this option is used as a prefix to report names",329        )330        pytest_opt_registered[option] = 1331 332 333def pytest_terminal_summary_main(tr, id):334    """335    Generate multiple reports at the end of test suite run - each report goes into a dedicated file in the current336    directory. The report files are prefixed with the test suite name.337 338    This function emulates --duration and -rA pytest arguments.339 340    This function is to be called from `conftest.py` via `pytest_terminal_summary` wrapper that has to be defined341    there.342 343    Args:344    - tr: `terminalreporter` passed from `conftest.py`345    - id: unique id like `tests` or `examples` that will be incorporated into the final reports filenames - this is346      needed as some jobs have multiple runs of pytest, so we can't have them overwrite each other.347 348    NB: this functions taps into a private _pytest API and while unlikely, it could break should349    pytest do internal changes - also it calls default internal methods of terminalreporter which350    can be hijacked by various `pytest-` plugins and interfere.351 352    """353    from _pytest.config import create_terminal_writer354 355    if not len(id):356        id = "tests"357 358    config = tr.config359    orig_writer = config.get_terminal_writer()360    orig_tbstyle = config.option.tbstyle361    orig_reportchars = tr.reportchars362 363    dir = "reports"364    Path(dir).mkdir(parents=True, exist_ok=True)365    report_files = {366        k: f"{dir}/{id}_{k}.txt"367        for k in [368            "durations",369            "errors",370            "failures_long",371            "failures_short",372            "failures_line",373            "passes",374            "stats",375            "summary_short",376            "warnings",377        ]378    }379 380    # custom durations report381    # note: there is no need to call pytest --durations=XX to get this separate report382    # adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/runner.py#L66383    dlist = []384    for replist in tr.stats.values():385        for rep in replist:386            if hasattr(rep, "duration"):387                dlist.append(rep)388    if dlist:389        dlist.sort(key=lambda x: x.duration, reverse=True)390        with open(report_files["durations"], "w") as f:391            durations_min = 0.05  # sec392            f.write("slowest durations\n")393            for i, rep in enumerate(dlist):394                if rep.duration < durations_min:395                    f.write(f"{len(dlist)-i} durations < {durations_min} secs were omitted")396                    break397                f.write(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}\n")398 399    def summary_failures_short(tr):400        # expecting that the reports were --tb=long (default) so we chop them off here to the last frame401        reports = tr.getreports("failed")402        if not reports:403            return404        tr.write_sep("=", "FAILURES SHORT STACK")405        for rep in reports:406            msg = tr._getfailureheadline(rep)407            tr.write_sep("_", msg, red=True, bold=True)408            # chop off the optional leading extra frames, leaving only the last one409            longrepr = re.sub(r".*_ _ _ (_ ){10,}_ _ ", "", rep.longreprtext, 0, re.M | re.S)410            tr._tw.line(longrepr)411            # note: not printing out any rep.sections to keep the report short412 413    # use ready-made report funcs, we are just hijacking the filehandle to log to a dedicated file each414    # adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/terminal.py#L814415    # note: some pytest plugins may interfere by hijacking the default `terminalreporter` (e.g.416    # pytest-instafail does that)417 418    # report failures with line/short/long styles419    config.option.tbstyle = "auto"  # full tb420    with open(report_files["failures_long"], "w") as f:421        tr._tw = create_terminal_writer(config, f)422        tr.summary_failures()423 424    # config.option.tbstyle = "short" # short tb425    with open(report_files["failures_short"], "w") as f:426        tr._tw = create_terminal_writer(config, f)427        summary_failures_short(tr)428 429    config.option.tbstyle = "line"  # one line per error430    with open(report_files["failures_line"], "w") as f:431        tr._tw = create_terminal_writer(config, f)432        tr.summary_failures()433 434    with open(report_files["errors"], "w") as f:435        tr._tw = create_terminal_writer(config, f)436        tr.summary_errors()437 438    with open(report_files["warnings"], "w") as f:439        tr._tw = create_terminal_writer(config, f)440        tr.summary_warnings()  # normal warnings441        tr.summary_warnings()  # final warnings442 443    tr.reportchars = "wPpsxXEf"  # emulate -rA (used in summary_passes() and short_test_summary())444    with open(report_files["passes"], "w") as f:445        tr._tw = create_terminal_writer(config, f)446        tr.summary_passes()447 448    with open(report_files["summary_short"], "w") as f:449        tr._tw = create_terminal_writer(config, f)450        tr.short_test_summary()451 452    with open(report_files["stats"], "w") as f:453        tr._tw = create_terminal_writer(config, f)454        tr.summary_stats()455 456    # restore:457    tr._tw = orig_writer458    tr.reportchars = orig_reportchars459    config.option.tbstyle = orig_tbstyle460 461 462class CaptureLogger:463    """464    Args:465    Context manager to capture `logging` streams466        logger: 'logging` logger object467    Returns:468        The captured output is available via `self.out`469    Example:470    ```python471    >>> from diffusers import logging472    >>> from diffusers.testing_utils import CaptureLogger473 474    >>> msg = "Testing 1, 2, 3"475    >>> logging.set_verbosity_info()476    >>> logger = logging.get_logger("diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.py")477    >>> with CaptureLogger(logger) as cl:478    ...     logger.info(msg)479    >>> assert cl.out, msg + "\n"480    ```481    """482 483    def __init__(self, logger):484        self.logger = logger485        self.io = StringIO()486        self.sh = logging.StreamHandler(self.io)487        self.out = ""488 489    def __enter__(self):490        self.logger.addHandler(self.sh)491        return self492 493    def __exit__(self, *exc):494        self.logger.removeHandler(self.sh)495        self.out = self.io.getvalue()496 497    def __repr__(self):498        return f"captured: {self.out}\n"499