Transformers 文档

测试

Hugging Face's logo
加入 Hugging Face 社区

并获得增强文档体验

开始使用

测试

让我们看看 🤗 Transformers 模型是如何进行测试的,以及如何编写新的测试并改进现有的测试。

存储库中有 2 个测试套件

  1. tests — 通用 API 的测试
  2. examples — 主要用于 API 之外的各种应用程序的测试

Transformers 如何进行测试

  1. 提交 PR 后,它会使用 9 个 CircleCI 任务进行测试。对该 PR 的每个新提交都会重新测试。这些任务在此 配置文件 中定义,因此,如果需要,您可以在您的机器上重现相同的环境。

    这些 CI 任务不会运行 @slow 测试。

  2. 有 3 个任务由 GitHub Actions 运行。

    • Torch Hub 集成:检查 Torch Hub 集成是否正常工作。

    • 自托管 (推送):仅在 main 分支上的提交中,在 GPU 上运行快速测试。它仅在 main 分支上的提交更新了以下文件夹中的代码时才会运行:srctests.github(以防止在添加的模型卡片、笔记本等上运行)。

    • 自托管运行器:在 GPU 上运行 testsexamples 中的普通和慢速测试。

RUN_SLOW=1 pytest tests/
RUN_SLOW=1 pytest examples/

结果可以在 此处 查看。

运行测试

选择要运行的测试

本文档详细介绍了如何运行测试。如果您在阅读完所有内容后仍然需要更多详细信息,可以在 此处 找到。

以下是一些最常用的运行测试方法。

运行所有测试

pytest

或者

make test

请注意,后者定义为

python -m pytest -n auto --dist=loadfile -s -v ./tests/

它告诉 pytest 要

  • 运行与 CPU 内核数量一样多的测试进程(如果您没有大量的 RAM,这可能太多了!)
  • 确保来自同一文件的全部测试都由同一个测试进程运行
  • 不捕获输出
  • 以详细模式运行

获取所有测试的列表

测试套件中的所有测试

pytest --collect-only -q

给定测试文件中的所有测试

pytest tests/test_optimization.py --collect-only -q

运行特定的测试模块

要运行单个测试模块

pytest tests/utils/test_logging.py

运行特定的测试

由于大多数测试内部都使用了 unittest,因此要运行特定的子测试,您需要知道包含这些测试的 unittest 类名称。例如,它可能是

pytest tests/test_optimization.py::OptimizationTest::test_adam_w

这里

  • tests/test_optimization.py - 包含测试的文件
  • OptimizationTest - 类的名称
  • test_adam_w - 特定测试函数的名称

如果文件包含多个类,您可以选择仅运行给定类的测试。例如

pytest tests/test_optimization.py::OptimizationTest

将运行该类中的所有测试。

如前所述,您可以通过运行以下命令查看 OptimizationTest 类中包含哪些测试。

pytest tests/test_optimization.py::OptimizationTest --collect-only -q

您可以根据关键词表达式运行测试。

要仅运行名称包含 adam 的测试

pytest -k adam tests/test_optimization.py

可以使用逻辑 andor 来指示是否应匹配所有关键词或其中任何一个。可以使用 not 进行否定。

要运行除名称包含 adam 的测试之外的所有测试

pytest -k "not adam" tests/test_optimization.py

并且您可以在一个命令中组合这两种模式

pytest -k "ada and not adam" tests/test_optimization.py

例如,要同时运行 test_adafactortest_adam_w,可以使用

pytest -k "test_adafactor or test_adam_w" tests/test_optimization.py

请注意,我们在这里使用 or,因为我们希望其中任何一个关键词匹配以包含两者。

如果只想包含同时包含这两种模式的测试,则应使用 and

pytest -k "test and ada" tests/test_optimization.py

运行加速测试

有时您需要在模型上运行 accelerate 测试。为此,您可以仅将 -m accelerate_tests 添加到您的命令中,例如,如果您想在 OPT 上运行这些测试,则运行

RUN_SLOW=1 pytest -m accelerate_tests tests/models/opt/test_modeling_opt.py

运行文档测试

为了测试文档示例是否正确,您应该检查 doctests 是否通过。例如,让我们使用 WhisperModel.forward 的文档字符串

r"""
Returns:

Example:
    ```python
    >>> import torch
    >>> from transformers import WhisperModel, WhisperFeatureExtractor
    >>> from datasets import load_dataset

    >>> model = WhisperModel.from_pretrained("openai/whisper-base")
    >>> feature_extractor = WhisperFeatureExtractor.from_pretrained("openai/whisper-base")
    >>> ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation")
    >>> inputs = feature_extractor(ds[0]["audio"]["array"], return_tensors="pt")
    >>> input_features = inputs.input_features
    >>> decoder_input_ids = torch.tensor([[1, 1]]) * model.config.decoder_start_token_id
    >>> last_hidden_state = model(input_features, decoder_input_ids=decoder_input_ids).last_hidden_state
    >>> list(last_hidden_state.shape)
    [1, 2, 512]
    ```"""

只需运行以下行即可自动测试所需文件中每个文档字符串示例。

pytest --doctest-modules <path_to_file_or_dir>

如果文件具有 markdown 扩展名,则应添加 --doctest-glob="*.md" 参数。

仅运行修改后的测试

您可以使用 pytest-picked 运行与未暂存文件或当前分支(根据 Git)相关的测试。这是一种快速测试您的更改是否破坏任何内容的好方法,因为它不会运行与您未触及的文件相关的测试。

pip install pytest-picked
pytest --picked

将运行来自已修改但尚未提交的文件和文件夹的所有测试。

源代码修改后自动重新运行失败的测试

pytest-xdist 提供了一个非常有用的功能,可以检测所有失败的测试,然后等待您修改文件并持续重新运行这些失败的测试,直到它们通过,同时您修复它们。这样您就不需要在修复后重新启动 pytest。重复此操作,直到所有测试都通过,之后会再次执行完整运行。

pip install pytest-xdist

进入该模式:pytest -fpytest --looponfail

通过查看looponfailroots根目录及其所有内容(递归)来检测文件更改。如果此值的默认值对您不起作用,则可以通过在setup.cfg中设置配置选项来更改项目中的此值

[tool:pytest]
looponfailroots = transformers tests

pytest.ini/tox.ini文件

[pytest]
looponfailroots = transformers tests

这将导致仅在相对于 ini 文件目录指定的各个目录中查找文件更改。

pytest-watch 是此功能的替代实现。

跳过测试模块

如果要运行所有测试模块,除了几个要排除的模块,可以通过提供要运行的测试的显式列表来排除它们。例如,要运行除test_modeling_*.py测试之外的所有测试

pytest *ls -1 tests/*py | grep -v test_modeling*

清除状态

CI 构建以及在隔离很重要(针对速度)时,应清除缓存。

pytest --cache-clear tests

并行运行测试

如前所述,make test 通过 pytest-xdist 插件(-n X 参数,例如 -n 2 以运行 2 个并行作业)并行运行测试。

pytest-xdist--dist= 选项允许控制测试的组合方式。--dist=loadfile 将一个文件中位于同一进程的测试。

由于执行测试的顺序不同且不可预测,如果使用 pytest-xdist 运行测试套件产生失败(意味着我们有一些未检测到的耦合测试),请使用 pytest-replay 以相同的顺序重放测试,这将有助于以某种方式将失败的序列减少到最少。

测试顺序和重复

最好依次、随机或成组地重复测试几次,以检测任何潜在的互依赖性和与状态相关的错误(拆卸)。直接多次重复对于检测一些由 DL 的随机性揭示的问题也很有用。

重复测试

pip install pytest-flakefinder

然后多次运行每个测试(默认 50 次)

pytest --flake-finder --flake-runs=5 tests/test_failing_test.py

此插件不适用于 pytest-xdist-n 标志。

还有另一个插件 pytest-repeat,但它不适用于 unittest

随机顺序运行测试

pip install pytest-random-order

重要提示:pytest-random-order 的存在会自动随机化测试,无需更改配置或命令行选项。

如前所述,这允许检测耦合测试 - 一个测试的状态影响另一个测试的状态。安装 pytest-random-order 后,它将打印用于该会话的随机种子,例如

pytest tests
[...]
Using --random-order-bucket=module
Using --random-order-seed=573663

因此,如果给定的特定序列失败,可以通过添加该确切的种子来重现它,例如

pytest --random-order-seed=573663
[...]
Using --random-order-bucket=module
Using --random-order-seed=573663

仅当使用完全相同的测试列表(或根本没有列表)时,才会重现完全相同的顺序。一旦开始手动缩小列表范围,就不能再依赖于种子,而必须按它们失败的确切顺序手动列出它们,并告诉 pytest 不要随机化它们,而是使用 --random-order-bucket=none,例如

pytest --random-order-bucket=none tests/test_a.py tests/test_c.py tests/test_b.py

禁用所有测试的洗牌

pytest --random-order-bucket=none

默认情况下,隐含 --random-order-bucket=module,它将在模块级别洗牌文件。它也可以在 classpackageglobalnone 级别洗牌。有关完整详细信息,请参阅其 文档

另一种随机化替代方案是:pytest-randomly。此模块具有非常相似的功能/接口,但它没有 pytest-random-order 中可用的桶模式。它存在安装后强加自身的问题。

外观变化

pytest-sugar

pytest-sugar 是一个插件,它可以改善外观,添加进度条,并立即显示失败的测试和断言。安装后会自动激活。

pip install pytest-sugar

要运行不包含它的测试,请运行

pytest -p no:sugar

或将其卸载。

报告每个子测试名称及其进度

对于单个或一组测试,通过 pytest(在 pip install pytest-pspec 之后)

pytest --pspec tests/test_optimization.py

立即显示失败的测试

pytest-instafail 会立即显示失败和错误,而不是等到测试会话结束。

pip install pytest-instafail
pytest --instafail

是否使用 GPU

在启用 GPU 的设置中,要在仅 CPU 模式下进行测试,请添加 CUDA_VISIBLE_DEVICES=""

CUDA_VISIBLE_DEVICES="" pytest tests/utils/test_logging.py

或者,如果您有多个 GPU,则可以通过 pytest 指定要使用哪个 GPU。例如,如果您有 GPU 01,要仅使用第二个 GPU,可以运行

CUDA_VISIBLE_DEVICES="1" pytest tests/utils/test_logging.py

当您想在不同的 GPU 上运行不同的任务时,这非常方便。

一些测试必须在仅 CPU 模式下运行,另一些则可以在 CPU 或 GPU 或 TPU 上运行,还有一些则可以在多个 GPU 上运行。以下跳过装饰器用于设置测试的 CPU/GPU/TPU 要求

  • require_torch - 此测试仅在 torch 下运行
  • require_torch_gpu - 类似于 require_torch,并且至少需要 1 个 GPU
  • require_torch_multi_gpu - 类似于 require_torch,并且至少需要 2 个 GPU
  • require_torch_non_multi_gpu - 类似于 require_torch,并且需要 0 或 1 个 GPU
  • require_torch_up_to_2_gpus - 类似于 require_torch,并且需要 0 或 1 或 2 个 GPU
  • require_torch_xla - 类似于 require_torch,并且至少需要 1 个 TPU

让我们在下表中描述 GPU 要求

GPU 数量 装饰器
>= 0 @require_torch
>= 1 @require_torch_gpu
>= 2 @require_torch_multi_gpu
< 2 @require_torch_non_multi_gpu
< 3 @require_torch_up_to_2_gpus

例如,以下是一个测试,它必须仅在有 2 个或更多 GPU 可用且安装了 pytorch 时运行

@require_torch_multi_gpu
def test_example_with_multi_gpu():

如果测试需要 tensorflow,请使用 require_tf 装饰器。例如

@require_tf
def test_tf_thing_with_tensorflow():

这些装饰器可以叠加使用。例如,如果一个测试很慢并且在 pytorch 下至少需要一个 GPU,则设置方法如下

@require_torch_gpu
@slow
def test_example_slow_on_gpu():

一些装饰器(如 @parametrized)会重写测试名称,因此 @require_* 跳过装饰器必须放在最后才能正常工作。以下是正确用法的示例

@parameterized.expand(...)
@require_torch_multi_gpu
def test_integration_foo():

此顺序问题在 @pytest.mark.parametrize 中不存在,您可以将其放在开头或结尾,它仍然可以工作。但它仅适用于非单元测试。

测试内部

  • 有多少个 GPU 可用
from transformers.testing_utils import get_gpu_count

n_gpu = get_gpu_count()  # works with torch and tf

使用特定的 PyTorch 后端或设备进行测试

要在特定的 torch 设备上运行测试套件,请添加 TRANSFORMERS_TEST_DEVICE="$device",其中 $device 是目标后端。例如,要在仅 CPU 模式下进行测试

TRANSFORMERS_TEST_DEVICE="cpu" pytest tests/utils/test_logging.py

此变量对于测试自定义或不太常见的 PyTorch 后端(如 mpsxpunpu)很有用。它还可以用于实现与 CUDA_VISIBLE_DEVICES 相同的效果,方法是定位特定的 GPU 或在仅 CPU 模式下进行测试。

某些设备在第一次导入 torch 后需要额外的导入。这可以使用环境变量 TRANSFORMERS_TEST_BACKEND 指定

TRANSFORMERS_TEST_BACKEND="torch_npu" pytest tests/utils/test_logging.py

备用后端可能还需要替换特定于设备的函数。例如,torch.cuda.manual_seed 可能需要替换为特定于设备的种子设置器,如 torch.npu.manual_seedtorch.xpu.manual_seed,以便在设备上正确设置随机种子。要在运行测试套件时指定一个新的后端以及特定于后端的设备函数,请创建一个 Python 设备规范文件 spec.py,其格式如下

import torch
import torch_npu # for xpu, replace it with `import intel_extension_for_pytorch`
# !! Further additional imports can be added here !!

# Specify the device name (eg. 'cuda', 'cpu', 'npu', 'xpu', 'mps')
DEVICE_NAME = 'npu'

# Specify device-specific backends to dispatch to.
# If not specified, will fallback to 'default' in 'testing_utils.py`
MANUAL_SEED_FN = torch.npu.manual_seed
EMPTY_CACHE_FN = torch.npu.empty_cache
DEVICE_COUNT_FN = torch.npu.device_count

此格式还允许指定任何所需的额外导入。要使用此文件替换测试套件中的等效方法,请将环境变量 TRANSFORMERS_TEST_DEVICE_SPEC 设置为规范文件的路径,例如 TRANSFORMERS_TEST_DEVICE_SPEC=spec.py

目前,仅支持 MANUAL_SEED_FNEMPTY_CACHE_FNDEVICE_COUNT_FN 用于特定于设备的调度。

分布式训练

pytest 无法直接处理分布式训练。如果尝试这样做 - 子进程不会执行正确操作,最终会认为自己是 pytest 并开始循环运行测试套件。但是,如果生成一个普通进程,然后生成多个工作进程并管理 IO 管道,则可以正常工作。

以下是一些使用它的测试

要直接跳转到执行点,请在这些测试中搜索 execute_subprocess_async 调用。

您将至少需要 2 个 GPU 才能看到这些测试的实际效果

CUDA_VISIBLE_DEVICES=0,1 RUN_SLOW=1 pytest -sv tests/test_trainer_distributed.py

输出捕获

在测试执行期间,发送到 stdoutstderr 的任何输出都会被捕获。如果测试或设置方法失败,则其相应的捕获输出通常会与失败的回溯一起显示。

要禁用输出捕获并正常获取 stdoutstderr,请使用 -s--capture=no

pytest -s tests/utils/test_logging.py

将测试结果发送到 JUnit 格式输出

pytest tests --junitxml=result.xml

颜色控制

要没有颜色(例如,白色背景上的黄色不可读)

pytest --color=no tests/utils/test_logging.py

将测试报告发送到在线粘贴服务

为每个测试失败创建 URL

pytest --pastebin=failed tests/utils/test_logging.py

这会将测试运行信息提交到远程粘贴服务,并为每个失败提供一个 URL。您可以像往常一样选择测试,或者例如添加 -x,如果您只想发送一个特定的失败。

为整个测试会话日志创建 URL

pytest --pastebin=all tests/utils/test_logging.py

编写测试

🤗 Transformers 测试基于 unittest,但由 pytest 运行,因此大多数情况下可以同时使用这两个系统的特性。

您可以阅读此处,了解支持哪些特性,但需要注意的是,大多数 pytest fixture 都不起作用。参数化也不行,但我们使用了功能类似的 parameterized 模块。

参数化

通常,需要多次运行相同的测试,但使用不同的参数。这可以在测试内部完成,但这样就没有办法只针对一组参数运行该测试。

# test_this1.py
import unittest
from parameterized import parameterized


class TestMathUnitTest(unittest.TestCase):
    @parameterized.expand(
        [
            ("negative", -1.5, -2.0),
            ("integer", 1, 1.0),
            ("large fraction", 1.6, 1),
        ]
    )
    def test_floor(self, name, input, expected):
        assert_equal(math.floor(input), expected)

现在,默认情况下,此测试将运行 3 次,每次将 test_floor 的最后 3 个参数分别分配给参数列表中的对应参数。

并且您可以只运行 negativeinteger 参数集,使用:

pytest -k "negative and integer" tests/test_mytest.py

或者除了 negative 子测试之外的所有子测试,使用:

pytest -k "not negative" tests/test_mytest.py

除了刚刚提到的 -k 过滤器之外,您还可以找出每个子测试的确切名称,并使用它们的精确名称运行任何或所有子测试。

pytest test_this1.py --collect-only -q

它将列出:

test_this1.py::TestMathUnitTest::test_floor_0_negative
test_this1.py::TestMathUnitTest::test_floor_1_integer
test_this1.py::TestMathUnitTest::test_floor_2_large_fraction

所以现在您可以只运行 2 个特定的子测试:

pytest test_this1.py::TestMathUnitTest::test_floor_0_negative  test_this1.py::TestMathUnitTest::test_floor_1_integer

transformers 开发者依赖项中已经包含的 parameterized 模块适用于 unittestspytest 测试。

但是,如果测试不是 unittest,则可以使用 pytest.mark.parametrize(或者您可能会在一些现有的测试中看到它,主要是在 examples 下)。

这是相同的示例,这次使用 pytestparametrize 标记:

# test_this2.py
import pytest


@pytest.mark.parametrize(
    "name, input, expected",
    [
        ("negative", -1.5, -2.0),
        ("integer", 1, 1.0),
        ("large fraction", 1.6, 1),
    ],
)
def test_floor(name, input, expected):
    assert_equal(math.floor(input), expected)

parameterized 一样,使用 pytest.mark.parametrize,您可以精确控制运行哪些子测试,如果 -k 过滤器无法满足需求。除了,此参数化函数为子测试创建了一组略有不同的名称。以下是它们的样子:

pytest test_this2.py --collect-only -q

它将列出:

test_this2.py::test_floor[integer-1-1.0]
test_this2.py::test_floor[negative--1.5--2.0]
test_this2.py::test_floor[large fraction-1.6-1]

所以现在您可以只运行特定的测试:

pytest test_this2.py::test_floor[negative--1.5--2.0] test_this2.py::test_floor[integer-1-1.0]

就像前面的示例一样。

文件和目录

在测试中,我们经常需要知道相对于当前测试文件的位置,这并不简单,因为测试可能从多个目录调用,或者可能位于具有不同深度的子目录中。辅助类 transformers.test_utils.TestCasePlus 通过整理所有基本路径并提供对它们的简单访问器来解决此问题。

  • pathlib 对象(全部已完全解析)

    • test_file_path - 当前测试文件路径,即 __file__
    • test_file_dir - 包含当前测试文件的目录
    • tests_dir - tests 测试套件的目录
    • examples_dir - examples 测试套件的目录
    • repo_root_dir - 存储库的目录
    • src_dir - src 的目录(即 transformers 子目录所在的目录)
  • 字符串化路径---与上面相同,但这些返回路径作为字符串,而不是 pathlib 对象。

    • test_file_path_str
    • test_file_dir_str
    • tests_dir_str
    • examples_dir_str
    • repo_root_dir_str
    • src_dir_str

要开始使用这些,您只需要确保测试位于 transformers.test_utils.TestCasePlus 的子类中。例如:

from transformers.testing_utils import TestCasePlus


class PathExampleTest(TestCasePlus):
    def test_something_involving_local_locations(self):
        data_dir = self.tests_dir / "fixtures/tests_samples/wmt_en_ro"

如果您不需要通过 pathlib 操作路径,或者只需要路径作为字符串,则可以始终在 pathlib 对象上调用 str() 或使用以 _str 结尾的访问器。例如:

from transformers.testing_utils import TestCasePlus


class PathExampleTest(TestCasePlus):
    def test_something_involving_stringified_locations(self):
        examples_dir = self.examples_dir_str

临时文件和目录

使用唯一的临时文件和目录对于并行测试运行至关重要,这样测试就不会覆盖彼此的数据。此外,我们希望在创建它们的每个测试结束时删除临时文件和目录。因此,使用像 tempfile 这样的解决这些需求的包至关重要。

但是,在调试测试时,您需要能够查看写入临时文件或目录的内容,并且您希望知道它的确切路径,而不是在每次测试重新运行时都将其随机化。

辅助类 transformers.test_utils.TestCasePlus 最适合用于此类目的。它是 unittest.TestCase 的子类,因此我们可以在测试模块中轻松地从中继承。

这是一个使用示例:

from transformers.testing_utils import TestCasePlus


class ExamplesTests(TestCasePlus):
    def test_whatever(self):
        tmp_dir = self.get_auto_remove_tmp_dir()

此代码创建一个唯一的临时目录,并将 tmp_dir 设置为其位置。

  • 创建唯一的临时目录
def test_whatever(self):
    tmp_dir = self.get_auto_remove_tmp_dir()

tmp_dir 将包含创建的临时目录的路径。它将在测试结束时自动删除。

  • 创建我选择的临时目录,确保在测试开始之前它是空的,并且在测试之后不将其清空。
def test_whatever(self):
    tmp_dir = self.get_auto_remove_tmp_dir("./xxx")

这在您想监控特定目录并确保以前的测试没有在其中留下任何数据时非常有用。

  • 您可以通过直接覆盖 beforeafter 参数来覆盖默认行为,从而导致以下行为之一:

    • before=True:临时目录将在测试开始时始终被清除。
    • before=False:如果临时目录已存在,则任何现有文件将保留在那里。
    • after=True:临时目录将在测试结束时始终被删除。
    • after=False:临时目录将在测试结束时始终保持不变。

为了安全地运行等效于 rm -r 的操作,如果使用了显式的 tmp_dir,则仅允许项目存储库检出的子目录,这样就不会错误地删除 /tmp 或类似的文件系统重要部分。即请始终传递以 ./ 开头的路径。

每个测试可以注册多个临时目录,并且它们都将自动删除,除非另有要求。

临时 sys.path 覆盖

如果您需要临时覆盖 sys.path 以便例如从另一个测试导入,则可以使用 ExtendSysPath 上下文管理器。示例:

import os
from transformers.testing_utils import ExtendSysPath

bindir = os.path.abspath(os.path.dirname(__file__))
with ExtendSysPath(f"{bindir}/.."):
    from test_trainer import TrainerIntegrationCommon  # noqa

跳过测试

当发现错误并编写了新的测试,但错误尚未修复时,这很有用。为了能够将其提交到主存储库,我们需要确保在 make test 期间跳过它。

方法

  • **跳过**意味着您期望您的测试仅在满足某些条件时才能通过,否则 pytest 应该完全跳过运行测试。常见示例包括在非 Windows 平台上跳过仅 Windows 测试,或跳过依赖于当前不可用的外部资源(例如数据库)的测试。

  • **xfail** 表示您预期测试由于某种原因会失败。一个常见的例子是针对尚未实现的功能或尚未修复的 bug 的测试。当测试尽管预期会失败(用 pytest.mark.xfail 标记)但仍通过时,则为 xpass,将在测试摘要中报告。

两者之间的一个重要区别是 skip 不会运行测试,而 xfail 会运行。因此,如果存在 bug 的代码会导致影响其他测试的不良状态,请不要使用 xfail

实现

  • 以下是如何无条件跳过整个测试
@unittest.skip(reason="this bug needs to be fixed")
def test_feature_x():

或通过 pytest

@pytest.mark.skip(reason="this bug needs to be fixed")

xfail 方法

@pytest.mark.xfail
def test_feature_x():

以下是如何根据测试内部检查跳过测试

def test_feature_x():
    if not has_something():
        pytest.skip("unsupported configuration")

或整个模块

import pytest

if not pytest.config.getoption("--custom-flag"):
    pytest.skip("--custom-flag is missing, skipping tests", allow_module_level=True)

xfail 方法

def test_feature_x():
    pytest.xfail("expected to fail until bug XYZ is fixed")
  • 以下是如何在某些导入缺失时跳过模块中的所有测试
docutils = pytest.importorskip("docutils", minversion="0.3")
  • 基于条件跳过测试
@pytest.mark.skipif(sys.version_info < (3,6), reason="requires python3.6 or higher")
def test_feature_x():

或者

@unittest.skipIf(torch_device == "cpu", "Can't do half precision")
def test_feature_x():

或跳过整个模块

@pytest.mark.skipif(sys.platform == 'win32', reason="does not run on windows")
class TestClass():
    def test_feature_x(self):

更多详细信息、示例和方法请点击此处

慢速测试

测试库不断发展,一些测试需要几分钟才能运行,因此我们无法在 CI 上等待一个小时才能完成测试套件。因此,除了某些基本测试的例外情况外,慢速测试应按如下例标记

from transformers.testing_utils import slow
@slow
def test_integration_foo():

一旦测试被标记为 @slow,要运行此类测试,请设置 RUN_SLOW=1 环境变量,例如

RUN_SLOW=1 pytest tests

某些装饰器(如 @parameterized)会重写测试名称,因此 @slow 和其余跳过装饰器 @require_* 必须最后列出才能正常工作。以下是一个正确用法的示例

@parameterized.expand(...)
@slow
def test_integration_foo():

如本文档开头所述,慢速测试会定期运行,而不是在 PR 的 CI 检查中运行。因此,在 PR 提交期间可能会错过一些问题并被合并。这些问题将在下一次计划的 CI 作业中被捕获。但这也意味着在提交 PR 之前,在您的机器上运行慢速测试非常重要。

以下是对选择哪些测试应标记为慢速的粗略决策机制

如果测试侧重于库的内部组件之一(例如,建模文件、标记化文件、管道),那么我们应该在非慢速测试套件中运行该测试。如果它侧重于库的其他方面,例如文档或示例,那么我们应该在慢速测试套件中运行这些测试。然后,为了改进这种方法,我们应该有一些例外情况

  • 所有需要下载大量权重或大于约 50MB 的数据集(例如,模型或标记器集成测试、管道集成测试)的测试都应设置为慢速。如果您正在添加新模型,则应为集成测试创建并上传其小型版本(使用随机权重)到 Hub。这将在以下段落中讨论。
  • 所有需要进行未专门优化为快速训练的测试都应设置为慢速。
  • 如果其中一些本应是非慢速测试非常慢,我们可以引入例外情况,并将它们设置为 @slow。自动建模测试(将大型文件保存和加载到磁盘)是标记为 @slow 的测试的一个很好的例子。
  • 如果测试在 CI 上(包括任何下载)完成时间少于 1 秒,则无论如何它都应为普通测试。

总的来说,所有非慢速测试都需要完全覆盖不同的内部内容,同时保持快速。例如,可以通过使用专门创建的小型模型(具有随机权重)进行测试来实现显着的覆盖率。此类模型具有最少的层数(例如,2),词汇量大小(例如,1000)等。然后,@slow 测试可以使用大型慢速模型进行定性测试。要查看这些的使用,只需查找带有

grep tiny tests examples

以下是一个创建小型模型的脚本示例 stas/tiny-wmt19-en-de。您可以轻松地将其调整到您特定模型的架构。

如果例如存在下载大型模型的开销,则很容易错误地测量运行时间,但是如果您在本地测试它,下载的文件将被缓存,因此不会测量下载时间。因此,请改用 CI 日志中的执行速度报告(pytest --durations=0 tests 的输出)。

该报告还有助于查找未标记为慢速或需要重写以使其快速运行的慢速异常值。如果您注意到测试套件在 CI 上开始变慢,则此报告的顶部列表将显示最慢的测试。

测试 stdout/stderr 输出

为了测试写入 stdout 和/或 stderr 的函数,测试可以使用 pytestcapsys 系统访问这些流。以下是如何实现的

import sys


def print_to_stdout(s):
    print(s)


def print_to_stderr(s):
    sys.stderr.write(s)


def test_result_and_stdout(capsys):
    msg = "Hello"
    print_to_stdout(msg)
    print_to_stderr(msg)
    out, err = capsys.readouterr()  # consume the captured output streams
    # optional: if you want to replay the consumed streams:
    sys.stdout.write(out)
    sys.stderr.write(err)
    # test:
    assert msg in out
    assert msg in err

当然,大多数情况下,stderr 将作为异常的一部分出现,因此在这种情况下必须使用 try/except

def raise_exception(msg):
    raise ValueError(msg)


def test_something_exception():
    msg = "Not a good value"
    error = ""
    try:
        raise_exception(msg)
    except Exception as e:
        error = str(e)
        assert msg in error, f"{msg} is in the exception:\n{error}"

另一种捕获 stdout 的方法是通过 contextlib.redirect_stdout

from io import StringIO
from contextlib import redirect_stdout


def print_to_stdout(s):
    print(s)


def test_result_and_stdout():
    msg = "Hello"
    buffer = StringIO()
    with redirect_stdout(buffer):
        print_to_stdout(msg)
    out = buffer.getvalue()
    # optional: if you want to replay the consumed streams:
    sys.stdout.write(out)
    # test:
    assert msg in out

捕获 stdout 的一个重要潜在问题是它可能包含 \r 字符,这些字符在正常的 print 中会重置迄今为止打印的所有内容。pytest 没有问题,但在 pytest -s 中,这些字符会被包含在缓冲区中,因此为了能够在有和没有 -s 的情况下运行测试,您必须使用 re.sub(r'~.*\r', '', buf, 0, re.M) 对捕获的输出进行额外的清理。

但是,然后我们有一个辅助上下文管理器包装器来自动处理所有这些,无论它是否包含一些 \r,因此它很简单

from transformers.testing_utils import CaptureStdout

with CaptureStdout() as cs:
    function_that_writes_to_stdout()
print(cs.out)

以下是一个完整的测试示例

from transformers.testing_utils import CaptureStdout

msg = "Secret message\r"
final = "Hello World"
with CaptureStdout() as cs:
    print(msg + final)
assert cs.out == final + "\n", f"captured: {cs.out}, expecting {final}"

如果您想捕获 stderr,请改用 CaptureStderr

from transformers.testing_utils import CaptureStderr

with CaptureStderr() as cs:
    function_that_writes_to_stderr()
print(cs.err)

如果您需要同时捕获两个流,请使用父类 CaptureStd

from transformers.testing_utils import CaptureStd

with CaptureStd() as cs:
    function_that_writes_to_stdout_and_stderr()
print(cs.err, cs.out)

此外,为了帮助调试测试问题,默认情况下,这些上下文管理器会在退出上下文时自动重放捕获的流。

捕获日志记录器流

如果您需要验证日志记录器的输出,可以使用 CaptureLogger

from transformers import logging
from transformers.testing_utils import CaptureLogger

msg = "Testing 1, 2, 3"
logging.set_verbosity_info()
logger = logging.get_logger("transformers.models.bart.tokenization_bart")
with CaptureLogger(logger) as cl:
    logger.info(msg)
assert cl.out, msg + "\n"

使用环境变量进行测试

如果要测试特定测试的环境变量的影响,可以使用辅助装饰器 transformers.testing_utils.mockenv

from transformers.testing_utils import mockenv


class HfArgumentParserTest(unittest.TestCase):
    @mockenv(TRANSFORMERS_VERBOSITY="error")
    def test_env_override(self):
        env_level_str = os.getenv("TRANSFORMERS_VERBOSITY", None)

有时需要调用外部程序,这需要在 os.environ 中设置 PYTHONPATH 以包含多个本地路径。辅助类 transformers.test_utils.TestCasePlus 可以提供帮助

from transformers.testing_utils import TestCasePlus


class EnvExampleTest(TestCasePlus):
    def test_external_prog(self):
        env = self.get_env()
        # now call the external program, passing `env` to it

根据测试文件是在 tests 测试套件下还是 examples 下,它将正确设置 env[PYTHONPATH] 以包含这两个目录之一,以及 src 目录以确保针对当前存储库进行测试,最后使用在调用测试之前已设置为 env[PYTHONPATH] 的任何内容(如果有)。

此辅助方法创建了 os.environ 对象的副本,因此原始对象保持不变。

获取可重复的结果

在某些情况下,您可能希望消除测试的随机性。要获得相同的结果,您需要修复种子

seed = 42

# python RNG
import random

random.seed(seed)

# pytorch RNGs
import torch

torch.manual_seed(seed)
torch.backends.cudnn.deterministic = True
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(seed)

# numpy RNG
import numpy as np

np.random.seed(seed)

# tf RNG
import tensorflow as tf 

tf.random.set_seed(seed)

调试测试

要从警告点开始调试器,请执行以下操作

pytest tests/utils/test_logging.py -W error::UserWarning --pdb

使用 GitHub Actions 工作流

要触发自推送工作流 CI 作业,您必须

  1. transformers 来源(而不是 fork!)上创建一个新分支。
  2. 分支名称必须以 ci_ci- 开头(main 也会触发它,但我们不能在 main 上进行 PR)。它也仅针对特定路径触发 - 如果自编写本文档以来有所更改,您可以在此处找到最新的定义 herepush: 下。
  3. 从此分支创建 PR。
  4. 然后您可以在此处看到作业出现 here。如果存在积压工作,它可能不会立即运行。

测试实验性 CI 功能

测试 CI 功能可能会存在潜在问题,因为它可能会干扰正常的 CI 功能。因此,如果要添加新的 CI 功能,应按以下方式进行。

  1. 创建一个测试所需内容的新专用作业。
  2. 新作业必须始终成功,以便为我们提供绿色 ✓(详细信息如下)。
  3. 让它运行几天,以查看各种不同类型的 PR 在其上运行(用户 fork 分支、非 fork 分支、源自 github.com UI 直接文件编辑的分支、各种强制推送等 - 有很多),同时监控实验性作业的日志(不是整体作业绿色,因为它是有意始终为绿色)
  4. 当确定一切正常时,将新的更改合并到现有作业中。

这样,对 CI 功能本身的实验就不会干扰正常的工作流程。

现在,如何在开发新 CI 功能时使作业始终成功?

一些 CI,如 TravisCI 支持忽略步骤失败,并将报告整个作业成功,但截至撰写本文时,CircleCI 和 Github Actions 不支持该功能。

因此可以使用以下解决方法

  1. 在运行命令的开头使用 set +euo pipefail 来抑制 bash 脚本中大多数潜在的失败。
  2. 最后一个命令必须成功:echo "done" 或仅 true 即可。

这是一个示例

- run:
    name: run CI experiment
    command: |
        set +euo pipefail
        echo "setting run-all-despite-any-errors-mode"
        this_command_will_fail
        echo "but bash continues to run"
        # emulate another failure
        false
        # but the last command must be a success
        echo "during experiment do not remove: reporting success to CI, even if there were failures"

对于简单的命令,您也可以执行以下操作

cmd_that_may_fail || true

当然,一旦对结果满意,将实验性步骤或作业与其余正常作业集成,同时删除 set +euo pipefail 或您可能添加的任何其他内容,以确保实验性作业不会干扰正常的 CI 功能。

如果我们只能为实验性步骤设置类似 allow-failure 的内容,并让它失败而不会影响 PR 的整体状态,那么整个过程将容易得多。但如前所述,CircleCI 和 Github Actions 目前不支持它。

您可以投票支持此功能,并在这些特定于 CI 的线程中查看其状态

DeepSpeed 集成

对于涉及 DeepSpeed 集成的 PR,请记住我们的 CircleCI PR CI 设置没有 GPU。需要 GPU 的测试在不同的 CI 每晚运行。这意味着如果您在 PR 中获得了通过的 CI 报告,并不意味着 DeepSpeed 测试通过。

要运行 DeepSpeed 测试

RUN_SLOW=1 pytest tests/deepspeed/test_deepspeed.py

对建模或 PyTorch 示例代码的任何更改都需要运行模型动物园测试。

RUN_SLOW=1 pytest tests/deepspeed
< > GitHub 更新