Eijebong/Archipelago-fuzzer

GitHub: Eijebong/Archipelago-fuzzer

为 Archipelago 游戏模块提供并行化的配置级模糊测试,通过随机 YAML 生成自动发现选项处理与生成逻辑中的缺陷。

Stars: 29 | Forks: 6

# Archipelago 模糊测试器 (Fuzzer) 这是一个相当简单的模糊测试器,它会生成包含 N 个随机 YAML 的 multiworlds 并记录失败情况。 ## 如何运行? 你需要从源码运行 archipelago。如果你不知道如何操作,可以查阅 archipelago 项目的文档 [这里](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/running%20from%20source.md) 将 `fuzz.py` 文件复制到 archipelago 项目的根目录下,然后你就可以像运行其他 archipelago 入口点一样运行这个模糊测试器: ``` python fuzz.py -r 100 -j 16 -g alttp -n 1 ``` 这将对 alttp 世界运行 100 次测试,每次生成使用 1 个 YAML,并使用 16 个作业。 输出结果将位于 `./fuzz_output`。 ## 标志位 (Flags) - `-g` 选择要进行模糊测试的 apworld。如果省略,每次运行将随机选择一个已加载的世界 - `-j` 指定并行运行的作业数量。默认为 10,建议值为你 CPU 的核心数。 - `-r` 指定要进行的生成次数。这是一个必填设置。 - `-n` 指定每次生成使用多少个 YAML。默认为 1。你还可以 指定范围,例如 `1-10`,使每次生成在 1 到 10 个 YAML 之间随机选取。 - `-t` 指定每次生成的最长时间(秒)。默认为 15 秒。 - `-m` 指定一个覆盖特定值的元文件 (meta file)。 - `--skip-output` 指定跳过生成的输出步骤。 - `--dump-ignored` 使得选项错误也被转储到结果中。 - `--with-static-worlds` 接受一个包含 YAML 的目录路径,这些 YAML 将包含在每次生成中。非递归。 - `--hook` 接受一个指向 hook 的 `module:class` 字符串,并且可以指定多次。更多信息见下文。 ## 元文件 (Meta files) 你可以通过 `-m` 标志提供一个元文件,以强制某些选项始终保持相同的值。 其语法与 archipelago 的 meta.yaml 语法非常相似: ``` null: progression_balancing: 50 Pokemon FireRed and LeafGreen: ability_blacklist: [] move_blacklist: [] ``` 请注意,与 archipelago 的元文件不同,这将覆盖生成的 YAML 中的值,在生成时没有隐式的选项应用,因此你不需要在报告错误时提供该元文件。 ### 触发器 (Triggers) 你也可以在元文件中定义 [triggers](https://archipelago.gg/tutorial/Archipelago/triggers/en)。 它们可以是针对特定游戏的,也可以是全局的。 ``` triggers: - option_category: Pokemon FireRed and LeafGreen option_name: some_option option_result: trigger_value options: Pokemon FireRed and LeafGreen: target_option: forced_value Pokemon FireRed and LeafGreen: triggers: - option_name: source_option option_result: trigger_value options: Pokemon FireRed and LeafGreen: target_option: forced_value ``` **注意:** Archipelago 的触发器仅在值与 YAML 中的内容完全匹配时才会触发。当选项键与你预期的值不匹配时,这可能会导致一些混淆。例如,开关 (toggles) 需要匹配 `'true'`/`'false'` 而不是 `true`/`false`,因为 archipelago 在将触发器值与生成的值进行比较之前不会解释触发器值。 ### 模糊测试约束 (Fuzz Constraints) 约束用于防止模糊测试器生成无效的选项组合。 它们定义在游戏元文件的 `fuzz_constraints` 键下,可以在 archipelago 触发器不够用时使用。 #### `if_selected` + `must_include` / `must_exclude` 当选择某个值时,要求或禁止同一选项中的其他值。 ``` - option: included_levels if_selected: "Hard Mode" must_include: "Tutorial" must_exclude: "Easy Skip" ``` #### `if_value` + `then` / `then_exclude` / `then_include` 当某个选项具有特定值时,设置其他选项或修改其内容。 ``` - option: difficulty if_value: expert then: hints: false then_exclude: levels: ["Tutorial", "Practice"] then_include: levels: ["Boss Rush"] ``` #### `if_any_selected` + `requires_any` 当选择任意触发值时,确保至少存在一个必需值。 ``` - option: sanities if_any_selected: ["KeySanity", "CheckpointSanity"] requires_any: ["Act A", "Act B"] ``` #### `mutually_exclusive` 不能共存的值。当两者同时存在时,会随机保留一个。 ``` - option: modes mutually_exclusive: ["Hard Mode", "Easy Mode"] ``` #### `max_count_of` 将数字选项的上限限制为另一个选项的大小。 ``` - option: num_gates max_count_of: allowed_bosses ``` #### `max_remaining_from` 限制数字选项,使得此选项与另一个选项的大小之和不超过固定的最大容量。 ``` - option: num_required_levels max_remaining_from: excluded_levels max_capacity: 20 ``` #### `sum_cap` 限制一组数字,使它们的总和不超过固定的最大容量。 ``` # base_items 与 extra_items 的总和不能超过 20 - sum_cap: - base_items - extra_items max_capacity: 20 ``` #### `ensure_any` 必须存在这些值中的至少一个。 ``` - option: included_levels ensure_any: ["World 1", "World 2", "World 3"] ``` ## 钩子 (Hooks) 为了将模糊测试器重新用于某些特定的错误测试,在生成之前对 archipelago 进行 monkeypatch(打补丁)和/或重新分类某些失败情况可能会很有用。 这就是 hook 的用处所在。 你可以在你的 archipelago 安装目录中 `fuzz.py` 旁边的文件中声明一个类似这样的类: ``` from fuzz import BaseHook, GenOutcome class Hook(BaseHook): def setup_main(self, args): """ The args parameter is the `Namespace` containing the parsed arguments from the CLI. setup is classed as early as possible after argument parsing in the main process. It is guaranteed to be only ever called once. It will always be called before any worker process is started """ pass def setup_worker(self, args): """ The args parameter is the `Namespace` containing the parsed arguments from the CLI. setup is classed as early as possible after starting a worker process. It is guaranteed to only ever be called once per worker process, before any generation attempt. """ pass def reclassify_outcome(self, outcome, exception): """ The outcome is a `GenOutcome` from generation. The exception is the exception raised during generation if one happened, None otherwise. This function is called in the worker process just after the result is first decided. The one exception is for timeouts where the outcome has to be processed on the main process. As such, this function must do very minimal work and not make assumptions as whether it's running in worker or in the main process. """ return GenOutcome.Success, exception def before_generate(self, args): """ This method will be called once per generation, just before we actually call into archipelago. The `args` argument contains the `Namespace` object passed to archipelago for generation. It can be modified since this happens before generation. """ pass def after_generate(self, multiworld, output_dir): """ This method will be called once per generation except if the generation timed out. If you need to inspect the failure, use `reclassify_outcome` instead. If the generation succeeds, multiworld is the object returned by archipelago on success, otherwise it's None """ pass def finalize(self): """ This method will be called once just before the main process exits. It will only be called on the main process """ pass ``` 然后你可以传递以下参数:`--hook your_file:Hook`,注意这应该是你的文件名,不带扩展名。 此仓库中的 `hooks` 文件夹包含了一些我个人使用 hooks 的示例。 ### 性能分析器钩子 (Profiler hook) 你可以使用提供的 `profile` hook 获取 callgrind 格式的性能分析文件。 示例: ``` python -O fuzz.py -r 1000 -n 1 -g pokemon_crystal -j24 --hook hooks.profile:Hook ``` 输出文件 (`fuzz_output/full.prof`) 可以使用 `qcachegrind` 等工具读取。 ### 确定性钩子 (Determinism hook) 你可以使用提供的 `determinism` hook 检查生成的确定性。 示例 ``` python -O fuzz.py -r 1000 -n 1 -g pokemon_crystal -j12 --hook hooks.determinism:Hook ``` 任何不属于确定性问题的失败都将被视为已忽略 (ignored)。
标签:APWorld, Archipelago, Multiworld, Python, QA, YAML配置, 并发测试, 无后门, 测试工具, 游戏测试, 逆向工具, 随机生成