添加测试

如果你正在为manim添加新功能,你应该为它们添加适当的测试。测试通过检查没有其他功能被破坏和/或被无意中修改,防止manim在每次更改时出现问题。

警告

完整的测试套件需要Cairo 1.18才能运行所有测试。 然而,Cairo 1.18可能无法从您的包管理器(如apt)中获得, 而且您很可能安装的是较旧的版本,例如1.16。如果您使用1.18之前的版本运行测试, 许多测试将被跳过。这些测试在在线CI中不会被跳过。

如果你想在本地运行所有测试,你需要安装Cairo 1.18或更高版本。 你可以通过从源代码编译Cairo来实现:

  1. 这里下载cairo-1.18.0.tar.xz,并解压缩它;

  2. 打开INSTALL文件并按照说明操作(你可能需要安装mesonninja);

  3. 运行测试套件并验证Cairo版本是否正确。

Manim 测试方法

Manim 使用 pytest 作为其测试框架。 要开始测试过程,请转到项目的根目录并在终端中运行 pytest。 测试期间发生的任何错误都会显示在终端中。

一些有用的 pytest 标志:

  • -x 将使 pytest 在遇到第一个失败时停止

  • -s 将使 pytest 显示所有的打印信息(包括场景生成期间的打印信息,如 DEBUG 信息)

  • --skip_slow 将跳过(任意)慢速测试

  • --show_diff 将在单元测试失败时显示视觉比较。

它是如何工作的

目前有三种类型的测试:

  1. 单元测试:

    测试manim的大部分基本功能。例如,有一个测试用于Mobject,检查它是否可以添加到场景中等。

  2. 图形单元测试: 因为 manim 是一个图形库,我们测试帧。为此,我们创建测试场景来渲染特定功能。 当 pytest 运行时,它会将测试结果与对照数据进行比较,无论是 6 fps 还是仅最后一帧。如果匹配,则测试通过。如果测试和对照数据不同,则测试失败。你可以使用 --show_diff 标志与 pytest 一起使用,以直观地查看差异。extract_frames.py 脚本允许你查看测试的所有帧。

  3. 视频格式测试:

    由于Manim是一个视频库,我们也需要测试视频。不幸的是,我们无法直接测试视频内容,因为渲染的视频可能会因系统不同而略有差异(这与ffmpeg有关)。因此,我们只比较以.json格式导出的视频配置值。

架构

manim/tests 目录看起来像这样:

.
├── conftest.py
├── control_data
│   ├── graphical_units_data
│   │   ├── creation
│   │   │   ├── DrawBorderThenFillTest.npy
│   │   │   ├── FadeInFromDownTest.npy
│   │   │   ├── FadeInFromLargeTest.npy
│   │   │   ├── FadeInFromTest.npy
│   │   │   ├── FadeInTest.npy
│   │   │   ├── ...
│   │   ├── geometry
│   │   │   ├── AnnularSectorTest.npy
│   │   │   ├── AnnulusTest.npy
│   │   │   ├── ArcBetweenPointsTest.npy
│   │   │   ├── ArcTest.npy
│   │   │   ├── CircleTest.npy
│   │   │   ├── CoordinatesTest.npy
│   │   │   ├── ...
│   │   ├── graph
│   │   │   ├── ...
|   |   |   | ...
│   └── videos_data
│       ├── SquareToCircleWithDefaultValues.json
│       └── SquareToCircleWithlFlag.json
├── helpers
│   ├── graphical_units.py
│   ├── __init__.py
│   └── video_utils.py
├── __init__.py
├── test_camera.py
├── test_config.py
├── test_copy.py
├── test_vectorized_mobject.py
├── test_graphical_units
│   ├── conftest.py
│   ├── __init__.py
│   ├── test_creation.py
│   ├── test_geometry.py
│   ├── test_graph.py
│   ├── test_indication.py
│   ├── test_movements.py
│   ├── test_threed.py
│   ├── test_transform.py
│   └── test_updaters.py
├── test_logging
│   ├── basic_scenes.py
│   ├── expected.txt
│   ├── testloggingconfig.cfg
│   └── test_logging.py
├── test_scene_rendering
│   ├── conftest.py
│   ├── __init__.py
│   ├── simple_scenes.py
│   ├── standard_config.cfg
│   └── test_cli_flags.py
└── utils
    ├── commands.py
    ├── __init__.py
    ├── testing_utils.py
    └── video_tester.py
   ...

主要目录

  • control_data/:

    包含控制数据的目录。control_data/graphical_units_data/ 包含图形测试的预期和正确的帧数据,而 control_data/videos_data/ 包含用于检查视频的 .json 文件。

  • test_graphical_units/:

    包含图形测试。

  • test_scene_rendering/:

    对于需要以某种方式渲染场景的测试,例如CLI标志的测试(端到端测试)。

  • utils/:

    pytest 使用的有用内部函数。

    注意

    fixtures 不包含在这里,它们在 conftest.py 中。

  • helpers/:

    为开发者设置图形/视频测试的辅助函数。

添加新测试

单元测试

Pytest通过搜索文件名以“test_”开头的文件来确定哪些函数是测试,然后在这些文件中查找以“test”开头的函数和以“Test”开头的类。这些类型的测试必须放在tests/目录下(例如tests/test_container.py)。

图形单元测试

测试必须写在正确的文件中(即与功能所属的适当类别相对应的文件),并遵循单元测试的结构。

例如,要测试位于manim/mobject/geometry.py中的Circle VMobject,请将CircleTest添加到test/test_geometry.py中。

模块的名称由变量 __module_test__ 表示,该变量必须在任何图形测试文件中声明。模块名称用于存储图形控制数据。

重要

你需要使用frames_comparison装饰器来创建一个测试。测试函数必须接受一个名为scene的参数,该参数将像标准construct方法中的self一样使用。

以下是一个在 test_geometry.py 中的示例:

from manim import *
from manim.utils.testing.frames_comparison import frames_comparison

__module_test__ = "geometry"


@frames_comparison
def test_circle(scene):
    circle = Circle()
    scene.play(Animation(circle))

装饰器可以带或不带括号使用。默认情况下,测试仅测试最后一帧。要启用多帧测试,您必须在参数中设置``last_frame=False``。

@frames_comparison(last_frame=False)
def test_circle(scene):
    circle = Circle()
    scene.play(Animation(circle))

您还可以在需要时指定所需的基础场景(例如,ThreeDScene):

@frames_comparison(last_frame=False, base_scene=ThreeDScene)
def test_circle(scene):
    circle = Circle()
    scene.play(Animation(circle))

请随意查看@frames_comparison的文档以获取更多信息。

请注意,测试名称必须遵循语法 test_,否则 pytest 不会将其识别为测试。

警告

如果你现在运行 pytest,你会得到一个 FileNotFound 错误。这是因为你还没有为你的测试创建控制数据。

要为您的测试创建控制数据,您必须使用标志--set_test与pytest一起使用。 对于上面的例子,它将是

pytest test_geometry.py::test_circle --set_test -s

(-s 在这里查看manim日志,以便你可以看到发生了什么).

如果你想查看所有的控制数据帧(例如,确保你的测试正在做你想要的事情),请使用 extract_frames.py脚本。第一个参数是.npz文件的路径,第二个参数是你想要创建帧的 目录。帧将被命名为frame0.png, frame1.png等。

python scripts/extract_frames.py tests/test_graphical_units/control_data/plot/axes.npz output

请确保在生成控制数据后立即将其添加到git中,使用git add

视频测试

为了测试生成的视频,我们使用装饰器 tests.utils.videos_tester.video_comparison:

@video_comparison(
    "SquareToCircleWithlFlag.json", "videos/simple_scenes/480p15/SquareToCircle.mp4"
)
def test_basic_scene_l_flag(tmp_path, manim_cfg_file, simple_scenes_path):
    scene_name = "SquareToCircle"
    command = [
        "python",
        "-m",
        "manim",
        simple_scenes_path,
        scene_name,
        "-l",
        "--media_dir",
        str(tmp_path),
    ]
    out, err, exit_code = capture(command)
    assert exit_code == 0, err

注意

assert exit*\ code == 0, err 用于在命令运行失败时使用。装饰器接受两个参数:json名称和视频应生成的路径,从media/目录开始。

注意这里的夹具:

  • tmp_path 是一个 pytest 夹具,用于获取临时路径。根据标志 --media_dir,Manim 将在此处输出。

  • manim_cfg_file 夹具返回指向 test_scene_rendering/standard_config.cfg 的路径。这只是为了缩短代码,在多个测试需要使用此配置文件的情况下。

  • simple_scenes_path 同上,除了 test_scene_rendering/simple_scene.py

你必须先生成一个.json文件才能测试你的视频。为此,请使用helpers.save_control_data_from_video

例如,一个测试将检查l标志是否正常工作,首先需要使用-l标志从场景中渲染视频。然后我们将测试(在这种情况下,SquareToCircle),它位于test_scene_rendering/simple_scene.py。更改目录到tests/,创建一个文件(例如create\_data.py),完成后立即删除。然后运行:

save_control_data_from_video("<path-to-video>", "SquareToCircleWithlFlag.json")

运行此操作将保存 control_data/videos_data/SquareToCircleWithlFlag.json,它将 看起来像这样:

{
    "name": "SquareToCircleWithlFlag",
    "config": {
        "codec_name": "h264",
        "width": 854,
        "height": 480,
        "avg_frame_rate": "15/1",
        "duration": "1.000000",
        "nb_frames": "15"
    }
}

如果您有任何问题,请随时在Discord、您的拉取请求或问题中提问。