Transformers 文档
测试
并获得增强的文档体验
开始使用
测试
让我们来看看 🤗 Transformers 模型是如何进行测试的,以及您如何编写新的测试并改进现有的测试。
存储库中有 2 个测试套件
tests
— 用于通用 API 的测试examples
— 主要用于各种非 API 组成部分的应用的测试
transformers 是如何测试的
一旦 PR 被提交,它将通过 9 个 CircleCi 作业进行测试。每次对该 PR 的新提交都会重新测试。这些作业在此配置文件中定义,以便在需要时您可以在您的机器上重现相同的环境。
这些 CI 作业不运行
@slow
测试。有 3 个由 github actions 运行的作业
torch hub 集成:检查 torch hub 集成是否工作。
自托管 (push):仅在
main
上的提交上在 GPU 上运行快速测试。它仅在main
上的提交更新了以下文件夹之一中的代码时运行:src
、tests
、.github
(以防止在添加模型卡、笔记本电脑等时运行)。自托管 runner:在
tests
和examples
中在 GPU 上运行正常和慢速测试
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
逻辑 and
和 or
可用于指示所有关键字是否应匹配或任一关键字是否应匹配。not
可用于否定。
要运行除名称中包含 adam
的测试之外的所有测试
pytest -k "not adam" tests/test_optimization.py
您可以将这两种模式组合在一起
pytest -k "ada and not adam" tests/test_optimization.py
例如,要同时运行 test_adafactor
和 test_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 测试
有时您需要在您的模型上运行 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 -f
或 pytest --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
,它将在模块级别对文件进行洗牌。它还可以在 class
、package
、global
和 none
级别进行洗牌。有关完整详细信息,请参阅其文档。
另一种随机化替代方案是: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 GPU 添加 CUDA_VISIBLE_DEVICES=""
CUDA_VISIBLE_DEVICES="" pytest tests/utils/test_logging.py
或者,如果你有多张 GPU,你可以指定哪张 GPU 被 pytest
使用。例如,如果你有 GPU 0
和 1
,并且只想使用第二张 GPU,你可以运行
CUDA_VISIBLE_DEVICES="1" pytest tests/utils/test_logging.py
对于 Intel GPU,在上面的例子中使用 ZE_AFFINITY_MASK
代替 CUDA_VISIBLE_DEVICES
。
当你想要在不同的 GPU 上运行不同的任务时,这非常方便。
有些测试必须在仅 CPU 环境下运行,有些可以在 CPU、GPU 或 TPU 上运行,还有一些需要在多 GPU 环境下运行。以下 skip 装饰器用于设置测试对 CPU/GPU/XPU/TPU 的要求:
require_torch
- 此测试仅在 torch 环境下运行require_torch_gpu
- 与require_torch
相同,但额外要求至少 1 张 GPUrequire_torch_multi_gpu
- 与require_torch
相同,但额外要求至少 2 张 GPUrequire_torch_non_multi_gpu
- 与require_torch
相同,但额外要求 0 或 1 张 GPUrequire_torch_up_to_2_gpus
- 与require_torch
相同,但额外要求 0 或 1 或 2 张 GPUrequire_torch_xla
- 与require_torch
相同,但额外要求至少 1 个 TPU
让我们在下表中描述 GPU 的要求
n gpus | 装饰器 |
---|---|
>= 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_*
skip 装饰器必须放在最后才能正确工作。这是一个正确用法的例子
@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 后端(如 mps
、xpu
或 npu
)非常有用。它也可以用于实现与 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_seed
或 torch.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
设置为 spec 文件的路径,例如 TRANSFORMERS_TEST_DEVICE_SPEC=spec.py
。
目前,设备特定的调度仅支持 MANUAL_SEED_FN
、EMPTY_CACHE_FN
和 DEVICE_COUNT_FN
。
分布式训练
pytest
无法直接处理分布式训练。如果尝试这样做 - 子进程不会做正确的事情,最终会认为它们是 pytest
并开始循环运行测试套件。但是,如果生成一个普通进程,然后该进程又衍生出多个 worker 并管理 IO 管道,则它可以工作。
以下是一些使用它的测试
要直接跳转到执行点,请在这些测试中搜索 execute_subprocess_async
调用。
你至少需要 2 张 GPU 才能看到这些测试的实际效果
CUDA_VISIBLE_DEVICES=0,1 RUN_SLOW=1 pytest -sv tests/test_trainer_distributed.py
输出捕获
在测试执行期间,发送到 stdout
和 stderr
的任何输出都会被捕获。如果测试或 setup 方法失败,其相应的捕获输出通常会与失败回溯一起显示。
要禁用输出捕获并正常获取 stdout
和 stderr
,请使用 -s
或 --capture=no
pytest -s tests/utils/test_logging.py
要将测试结果发送到 JUnit 格式的输出
pytest tests --junitxml=result.xml
颜色控制
为了不显示颜色(例如,白色背景上的黄色不可读)
pytest --color=no tests/utils/test_logging.py
将测试报告发送到在线 Pastebin 服务
为每个测试失败创建一个 URL
pytest --pastebin=failed tests/utils/test_logging.py
这会将测试运行信息提交到远程 Paste 服务,并为每个失败提供一个 URL。你可以像往常一样选择测试,或者添加例如 -x,如果你只想发送一个特定的失败。
为整个测试会话日志创建一个 URL
pytest --pastebin=all tests/utils/test_logging.py
编写测试
🤗 transformers 的测试基于 unittest
,但由 pytest
运行,因此大多数时候可以使用这两个系统的功能。
你可以在这里阅读支持哪些功能,但要记住的重要一点是,大多数 pytest
fixtures 都不起作用。参数化也不行,但我们使用模块 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 个参数分配给参数列表中的相应参数。
你可以仅使用 negative
和 integer
参数集运行,使用
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
模块 parameterized 已经在 transformers
的开发者依赖项中,它适用于 unittests
和 pytest
测试。
但是,如果测试不是 unittest
,你可以使用 pytest.mark.parametrize
(或者你可能会在一些现有测试中看到它被使用,主要在 examples
下)。
这是相同的示例,这次使用 pytest
的 parametrize
标记
# 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")
当你想要监视特定目录并想确保之前的测试没有在该目录中留下任何数据时,这对于调试很有用。
你可以通过直接覆盖
before
和after
参数来覆盖默认行为,从而导致以下行为之一before=True
:临时目录始终在测试开始时清除。before=False
:如果临时目录已存在,则任何现有文件都将保留在那里。after=True
:临时目录始终在测试结束时删除。after=False
:临时目录始终在测试结束时保持不变。
为了安全地运行等效于 rm -r
的操作,如果使用了显式的 tmp_dir
,则仅允许项目仓库检出的子目录,这样就不会错误地 nuked 掉 /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
跳过测试
当发现 bug 并且编写了新测试,但 bug 尚未修复时,这很有用。 为了能够将其提交到主仓库,我们需要确保在 make test
期间跳过它。
方法
skip 意味着你期望你的测试仅在满足某些条件时才通过,否则 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
和其余的 skip 装饰器 @require_*
必须放在最后才能正确工作。 这是一个正确用法的例子
@parameterized.expand(...)
@slow
def test_integration_foo():
正如本文档开头所解释的那样,慢速测试会在计划的时间运行,而不是在 PR CI 检查中运行。 因此,在 PR 提交期间可能会遗漏一些问题并被合并。 这些问题将在下一个计划的 CI 作业期间被捕获。 但这也意味着在提交 PR 之前在你的机器上运行慢速测试非常重要。
以下是用于选择哪些测试应标记为慢速的粗略决策机制
如果测试侧重于库的内部组件之一(例如,建模文件、分词文件、pipelines),那么我们应该在非慢速测试套件中运行该测试。 如果它侧重于库的其他方面,例如文档或示例,那么我们应该在慢速测试套件中运行这些测试。 然后,为了改进这种方法,我们应该有例外情况
- 所有需要下载大量权重集或大于约 50MB 的数据集的测试(例如,模型或分词器集成测试、pipeline 集成测试)都应设置为慢速。 如果你要添加新模型,则应创建并上传到 hub 其微型版本(带有随机权重)以进行集成测试。 这将在以下段落中讨论。
- 所有需要进行非专门优化以求快速的训练的测试都应设置为慢速。
- 如果其中一些本应是非慢速测试的测试速度极其缓慢,我们可以引入例外情况,并将它们设置为
@slow
。 自动建模测试(将大型文件保存并加载到磁盘)是标记为@slow
的测试的一个很好的例子。 - 如果测试在 CI 上在 1 秒内完成(包括任何下载),那么它应该是一个正常测试,无论如何。
总而言之,所有非慢速测试都需要完全覆盖不同的内部组件,同时保持快速。 例如,可以通过使用专门创建的带有随机权重的小型模型来实现显着的覆盖率。 这种模型具有非常少的层数(例如,2 层)、词汇表大小(例如,1000)等。 然后,@slow
测试可以使用大型慢速模型进行定性测试。 要查看这些的用法,只需查找带有 tiny 模型的
grep tiny tests examples
这是一个 脚本 的示例,该脚本创建了微型模型 stas/tiny-wmt19-en-de。 你可以轻松地将其调整为你的特定模型的架构。
如果例如下载大型模型存在开销,则很容易错误地测量运行时,但如果你在本地测试,则下载的文件将被缓存,因此不会测量下载时间。 因此,请改为检查 CI 日志中的执行速度报告(pytest --durations=0 tests
的输出)。
该报告还有助于查找未标记为慢速或需要重写以求快速的慢速异常值。 如果你注意到测试套件在 CI 上开始变慢,则此报告的顶部列表将显示最慢的测试。
测试 stdout/stderr 输出
为了测试写入 stdout
和/或 stderr
的函数,测试可以使用 pytest
的 capsys 系统 访问这些流。 以下是如何实现此目的
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)
此外,为了帮助调试测试问题,默认情况下,这些上下文管理器会在从上下文退出时自动重放捕获的流。
捕获 logger 流
如果你需要验证 logger 的输出,你可以使用 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 任务,你必须
- 在
transformers
原始仓库(非 fork)上创建一个新分支。 - 分支名称必须以
ci_
或ci-
开头(main
分支也会触发,但我们不能在main
分支上创建 PR)。它也仅针对特定路径触发 - 如果自本文档编写以来定义已更改,你可以在 此处 的 *push:* 下找到最新的定义。 - 从这个分支创建一个 PR。
- 然后你可以在 此处 看到任务出现。如果存在积压,它可能不会立即运行。
测试实验性 CI 功能
测试 CI 功能可能存在潜在问题,因为它可能会干扰正常的 CI 功能。因此,如果要添加新的 CI 功能,应按以下步骤进行。
- 创建一个新的专用任务,用于测试需要测试的内容
- 新任务必须始终成功,以便为我们提供绿色的 ✓ (详情如下)。
- 让它运行几天,以观察各种不同的 PR 类型在其上运行(用户 fork 分支、非 fork 分支、源自 github.com UI 直接文件编辑的分支、各种强制推送等 - 很多),同时监控实验性任务的日志(不是整体任务的绿色,因为它有意始终为绿色)
- 当明确一切稳定后,将新更改合并到现有任务中。
这样,对 CI 功能本身的实验就不会干扰正常的工作流程。
现在,在开发新的 CI 功能时,我们如何使任务始终成功?
一些 CI,例如 TravisCI 支持忽略步骤失败并将报告整体任务为成功,但截至本文撰写时,CircleCI 和 Github Actions 不支持这一点。
因此,可以使用以下解决方法
- 在运行命令的开头使用
set +euo pipefail
来抑制 bash 脚本中大多数潜在的失败。 - 最后一个命令必须成功:
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
对 modeling 或 PyTorch 示例代码的任何更改都需要运行模型动物园测试。
RUN_SLOW=1 pytest tests/deepspeed