Transformers documentation

테스트

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

테스트

먼저 🤗 Transformers 모델이 어떻게 테스트되는지 살펴보고, 새로운 테스트를 작성 및 기존 테스트를 개선하는 방법을 알아봅시다.

이 저장소에는 2개의 테스트 스위트가 있습니다:

  1. tests - 일반 API에 대한 테스트
  2. examples - API의 일부가 아닌 다양한 응용 프로그램에 대한 테스트

Transformers 테스트 방법

  1. PR이 제출되면 9개의 CircleCi 작업으로 테스트가 진행됩니다. 해당 PR에 대해 새로운 커밋이 생성될 때마다 테스트는 다시 진행됩니다. 이 작업들은 이 config 파일에 정의되어 있으므로 필요하다면 사용자의 로컬 환경에서 동일하게 재현해 볼 수 있습니다.

    이 CI 작업은 @slow 테스트를 실행하지 않습니다.

  2. github actions에 의해 실행되는 작업은 3개입니다:

    • torch hub integration: torch hub integration이 작동하는지 확인합니다.

    • self-hosted (push): main 브랜치에서 커밋이 업데이트된 경우에만 GPU를 이용한 빠른 테스트를 실행합니다. 이는 src, tests, .github 폴더 중 하나에 코드가 업데이트된 경우에만 실행됩니다. (model card, notebook, 기타 등등을 추가한 경우 실행되지 않도록 하기 위해서입니다)

    • self-hosted runner: testsexamples에서 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

논리 연산자 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_adam_w 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’s docstring를 사용해 봅시다:

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]
    ```"""

원하는 파일의 모든 docstring 예제를 자동으로 테스트하려면 다음 명령을 실행하면 됩니다:

pytest --doctest-modules <path_to_file_or_dir>

파일의 확장자가 markdown인 경우 --doctest-glob="*.md" 인수를 추가해야 합니다.

수정된 테스트만 실행

수정된 파일 또는 현재 브랜치 (Git 기준)와 관련된 테스트를 실행하려면 pytest-picked을 사용할 수 있습니다. 이는 변경한 내용이 테스트에 영향을 주지 않았는지 빠르게 확인할 수 있는 좋은 방법입니다.

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-file의 디렉터리를 기준으로 상대적으로 지정된 각 디렉터리에서 파일 변경 사항만 찾게 됩니다.

이 기능을 대체할 수 있는 구현 방법인 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를 사용하면 동일한 순서로 테스트를 다시 실행해서 실패하는 시퀀스를 최소화하는 데에 도움이 됩니다.

테스트 순서와 반복

잠재적인 종속성 및 상태 관련 버그(tear down)를 감지하기 위해 테스트를 여러 번, 연속으로, 무작위로 또는 세트로 반복하는 것이 좋습니다. 그리고 직접적인 여러 번의 반복은 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

정확히 동일한 테스트 목록(또는 목록이 없음)을 사용하는 경우에만 정확한 순서를 재현합니다. 목록을 수동으로 좁히기 시작하면 더 이상 시드에 의존할 수 없고 실패했던 정확한 순서로 수동으로 목록을 나열해야합니다. 그리고 --random-order-bucket=none을 사용하여 pytest에게 순서를 임의로 설정하지 않도록 알려야 합니다. 예를 들어 다음과 같습니다:

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, globalnone 수준에서도 섞을 수 있습니다. 자세한 내용은 해당 문서를 참조하세요.

또 다른 무작위화의 대안은 pytest-randomly입니다. 이 모듈은 매우 유사한 기능/인터페이스를 가지고 있지만, pytest-random-order에 있는 버킷 모드를 사용할 수는 없습니다. 설치 후에는 자동으로 적용되는 문제도 동일하게 가집니다.

외관과 느낌을 변경[ look-and-feel-variations

pytest-sugar 사용

pytest-sugar는 테스트가 보여지는 형태를 개선하고, 진행 상황 바를 추가하며, 실패한 테스트와 검증을 즉시 표시하는 플러그인입니다. 설치하면 자동으로 활성화됩니다.

pip install pytest-sugar

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이 있는 경우 다음을 실행할 수 있습니다:

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 요구 사항을 표로 정리하면 아래와 같습니디ㅏ:

| n gpus | decorator | |--------+--------------------------------| | >= 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에는 이러한 순서 문제는 없으므로 처음 혹은 마지막에 위치시킬 수 있고 이러한 경우에도 잘 작동할 것입니다. 하지만 unittest가 아닌 경우에만 작동합니다.

테스트 내부에서 다음을 사용할 수 있습니다:

  • 사용 가능한 GPU 수:
from transformers.testing_utils import get_gpu_count

n_gpu = get_gpu_count()  #torch와 tf와 함께 작동

분산 훈련

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 형식의 출력으로 보내려면 다음을 사용하세요:

py.test tests --junitxml=result.xml

색상 조절

색상이 없게 하려면 다음과 같이 설정하세요(예를 들어 흰색 배경에 노란색 글씨는 가독성이 좋지 않습니다):

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

online pastebin service에 테스트 보고서 전송

각 테스트 실패에 대한 URL을 만듭니다:

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

이렇게 하면 각 실패에 대한 URL을 제공하는 remote Paste service에 테스트 실행 정보를 제출합니다. 일반적인 테스트를 선택할 수도 있고 혹은 특정 실패만 보내려면 -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)

이제 기본적으로 이 테스트는 test_floor의 마지막 3개 인수가 매개변수 목록의 해당 인수에 할당되는 것으로 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 중에 건너뛰도록 해야 합니다.

방법:

  • skip은 테스트가 일부 조건이 충족될 경우에만 통과될 것으로 예상되고, 그렇지 않으면 pytest가 전체 테스트를 건너뛰어야 함을 의미합니다. 일반적인 예로는 Windows가 아닌 플랫폼에서 Windows 전용 테스트를 건너뛰거나 외부 리소스(예를 들어 데이터베이스)에 의존하는 테스트를 건너뛰는 것이 있습니다.

  • xfail은 테스트가 특정한 이유로 인해 실패할 것으로 예상하는 것을 의미합니다. 일반적인 예로는 아직 구현되지 않은 기능이나 아직 수정되지 않은 버그의 테스트가 있습니다. xfail로 표시된 테스트가 예상대로 실패하지 않고 통과된 경우, 이것은 xpass이며 테스트 결과 요약에 기록됩니다.

두 가지 중요한 차이점 중 하나는 skip은 테스트를 실행하지 않지만 xfail은 실행한다는 것입니다. 따라서 오류가 있는 코드가 일부 테스트에 영향을 미칠 수 있는 경우 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")
  • import가 missing된 모듈이 있을 때 그 모듈의 모든 테스트를 건너뛰는 방법:
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보다 큰 데이터셋을 다운로드해야 하는 모든 테스트(예: 모델 통합 테스트, 토크나이저 통합 테스트, 파이프라인 통합 테스트)를 느린 테스트로 설정해야 합니다. 새로운 모델을 추가하는 경우 통합 테스트용으로 무작위 가중치로 작은 버전을 만들어 허브에 업로드해야 합니다. 이 내용은 아래 단락에서 설명됩니다.
  • 특별히 빠르게 실행되도록 최적화되지 않은 학습을 수행해야 하는 테스트는 느린 테스트로 설정해야 합니다.
  • 느리지 않아야 할 테스트 중 일부가 극도로 느린 경우 예외를 도입하고 이를 @slow로 설정할 수 있습니다. 대용량 파일을 디스크에 저장하고 불러오는 자동 모델링 테스트는 @slow으로 표시된 테스트의 좋은 예입니다.
  • CI에서 1초 이내에 테스트가 완료되는 경우(다운로드 포함)에는 느린 테스트가 아니어야 합니다.

느린 테스트가 아닌 경우에는 다양한 내부를 완전히 커버하면서 빠르게 유지되어야 합니다. 예를 들어, 무작위 가중치를 사용하여 특별히 생성된 작은 모델로 테스트하면 상당한 커버리지를 얻을 수 있습니다. 이러한 모델은 최소한의 레이어 수(예: 2), 어휘 크기(예: 1000) 등의 요소만 가집니다. 그런 다음 @slow 테스트는 대형 느린 모델을 사용하여 정성적인 테스트를 수행할 수 있습니다. 이러한 작은 모델을 사용하는 방법을 확인하려면 다음과 같이 tiny 모델을 찾아보세요.

grep tiny tests examples

다음은 작은 모델stas/tiny-wmt19-en-de을 만든 script 예시입니다. 특정 모델의 아키텍처에 맞게 쉽게 조정할 수 있습니다.

예를 들어 대용량 모델을 다운로드하는 경우 런타임을 잘못 측정하기 쉽지만, 로컬에서 테스트하면 다운로드한 파일이 캐시되어 다운로드 시간이 측정되지 않습니다. 대신 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()  # 캡처된 출력 스트림 사용
    # 선택 사항: 캡처된 스트림 재생성
    sys.stdout.write(out)
    sys.stderr.write(err)
    # 테스트:
    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()
    # 선택 사항: 캡처된 스트림 재생성
    sys.stdout.write(out)
    # 테스트:
    assert msg in out

stdout 캡처에 관련된 중요한 문제 중 하나는 보통 print에서 이전에 인쇄된 내용을 재설정하는 \r 문자가 포함될 수 있다는 것입니다. 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()
        # 이제 `env`를 사용하여 외부 프로그램 호출

테스트 파일이 tests 테스트 스위트 또는 examples에 있는지에 따라 env[PYTHONPATH]가 두 디렉터리 중 하나를 포함하도록 설정되며, 현재 저장소에 대해 테스트가 수행되도록 src 디렉터리도 포함됩니다. 테스트 호출 이전에 설정된 경우에는 env[PYTHONPATH]를 그대로 사용합니다.

이 헬퍼 메소드는 os.environ 객체의 사본을 생성하므로 원본은 그대로 유지됩니다.

재현 가능한 결과 얻기

일부 상황에서 테스트에서 임의성을 제거하여 동일하게 재현 가능한 결과를 얻고 싶을 수 있습니다. 이를 위해서는 다음과 같이 시드를 고정해야 합니다.

seed = 42

# 파이썬 RNG
import random

random.seed(seed)

# 파이토치 RNG
import torch

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

# 넘파이 RNG
import numpy as np

np.random.seed(seed)

# 텐서플로 RNG
tf.random.set_seed(seed)

테스트 디버깅

경고가 있는 곳에서 디버거를 시작하려면 다음을 수행하세요.

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

Github Actions 워크플로우 작업 처리

셀프 푸시 워크플로우 CI 작업을 트리거하려면, 다음을 수행해야 합니다.

  1. transformers 원본에서 새 브랜치를 만듭니다(포크가 아닙니다!).
  2. 브랜치 이름은 ci_ 또는 ci-로 시작해야 합니다(main도 트리거하지만 main에서는 PR을 할 수 없습니다). 또한 특정 경로에 대해서만 트리거되므로 이 문서가 작성된 후에 변경된 내용은 여기push:에서 확인할 수 있습니다.
  3. 이 브랜치에서 PR을 생성합니다
  4. 그런 다음 여기에서 작업이 나타나는지 확인할 수 있습니다. 백로그가 있는 경우, 바로 실행되지 않을 수도 있습니다.

실험적인 CI 기능 테스트

CI 기능을 테스트하는 것은 일반 CI 작동에 방해가 될 수 있기 때문에 잠재적으로 문제가 발생할 수 있습니다. 따라서 새로운 CI 기능을 추가하는 경우 다음과 같이 수행해야 합니다.

  1. 테스트해야 할 내용을 테스트하는 새로운 전용 작업을 생성합니다.
  2. 새로운 작업은 항상 성공해야만 녹색 ✓를 받을 수 있습니다(아래에 자세한 내용이 있습니다).
  3. 다양한 PR 유형에 대한 확인을 위해 (사용자 포크 브랜치, 포크되지 않은 브랜치, github.com UI 직접 파일 편집에서 생성된 브랜치, 강제 푸시 등 PR의 유형은 아주 다양합니다.) 며칠 동안 실험 작업의 로그를 모니터링하면서 실행해봅니다. (의도적으로 항상 녹색을 표시하므로 작업 전체가 녹색은 아니라는 점에 유의합니다.)
  4. 모든 것이 안정적인지 확인한 후, 새로운 변경 사항을 기존 작업에 병합합니다.

이렇게 하면 CI 기능 자체에 대한 실험이 일반 작업 흐름에 방해가 되지 않습니다.

그러나 새로운 CI 기능이 개발 중인 동안, 항상 성공하도록 할 수 있는 방법은 무엇일까요?

TravisCI와 같은 일부 CI는 ignore-step-failure를 지원하며 전체 작업을 성공한 것으로 보고하지만, 현재 우리가 사용하는 CircleCI와 Github Actions는 이를 지원하지 않습니다.

따라서 다음과 같은 해결책을 사용할 수 있습니다.

  1. bash 스크립트에서 가능한 많은 오류를 억제하기 위해 실행 명령의 시작 부분에 set +euo pipefail을 추가합니다.
  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 작동에 방해되지 않도록 해야 합니다.

이 전반적인 과정은 실험 단계가 PR의 전반적인 상태에 영향을 주지 않고 실패하도록 allow-failure와 같은 기능을 설정할 수 있다면 훨씬 더 쉬웠을 것입니다. 그러나 앞에서 언급한 바와 같이 CircleCI와 Github Actions는 현재 이러한 기능들 지원하지 않습니다.

이 기능의 지원을 위한 투표에 참여하고 CI 관련 스레드들에서 이러한 상황을 확인할 수도 있습니다.

< > Update on GitHub