添加测试

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

警告

完整的测试套件需要Cairo 1.18才能运行所有测试。然而,您的包管理器(例如 apt)可能不提供Cairo 1.18,并且您很可能安装了旧版本,例如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.pngframe1.png 等。

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

请务必在生成控制数据后立即使用 git add <您的控制数据.npz> 将其添加到git。

视频测试

为了测试生成的视频,我们使用装饰器 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/ 目录开始。

注意这里的fixtures

  • tmp_path 是一个pytest fixture,用于获取临时路径。Manim将根据 --media_dir 标志在这里输出。

  • manim_cfg_file fixture返回指向 test_scene_rendering/standard_config.cfg 的路径。这只是为了缩短代码,以防多个测试需要使用此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、您的拉取请求中或在issue中提出。