moon_mutest

Mutation testing toolkit for MoonBit projects

mutation-testing
testing
quality
cli
moon add Magic486/moon_mutest@0.1.7
Download zip
Author
Version
0.1.7
License
Apache-2.0
Last updated
last month
Downloads
34

Dependencies

README

#moon_mutest

moon_mutest 是 MoonBit 项目的变异测试工具。它把小型代码改动应用到临时 workspace,再运行 moon checkmoon test,用结果判断测试是否真的能发现 代码被改坏。

它关注的不是“测试有没有运行”,而是“断言是否足够强”。例如把 == 改成 != + 改成 - 后测试仍通过,说明这个 mutant 逃逸,对应代码可能缺少精确断言或 边界用例。

#安装

库 API 可以从 mooncakes.io 安装:

moon add Magic486/moon_mutest@0.1.7

CLI 目前随源码仓库提供,需要在本仓库根目录执行:

moon run --target js cmd/main -- scan "a == b && true"

需要 Node.js 和可用的 moon 命令。CLI 会在临时目录运行,默认不会修改目标项目。

发布包的下游使用示例位于 examples/consumer_workspace,可在发布后验证:

moon -C examples/consumer_workspace test

#最快用法

先从一个很小的范围开始:

moon run --target js cmd/main -- run path/to/workspace --max-mutants 10 --first 10

对全部生产源码运行时,工具默认:

  1. 跳过 *_test.mbt、生成文件和 _build
  2. 复制目标 workspace 到临时目录。
  3. 先执行基线 moon checkmoon test
  4. 逐个应用 mutation 并汇总 killedsurvivedcompile-errortimeoutskippedequivalent

默认命令可以用 --check-command--test-command 覆盖。

#增量变异测试

对日常开发和 PR,推荐只测相对 Git 参考点发生变化的生产源码:

moon run --target js cmd/main -- run . \ --changed-since origin/master \ --max-mutants 30 \ --first 10

--changed-since REF 先以 git merge-base REF HEAD 找到共同基线,再使用 git diff --relative --name-only --diff-filter=ACMR BASE -- 获取 新增、复制、修改和重命名后的文件,并包含当前未提交修改。删除文件会被忽略。 报告中的 changed-sincegit-changed-filesincremental-files 会显示实际范围。 使用该参数时,目标 workspace 必须位于可读取参考点的 Git 仓库中。

本地快速检查上一提交以来的改动:

moon run --target js cmd/main -- run . --changed-since HEAD~1 --max-mutants 20

在 CI 中,先确保 Git 历史包含参考分支;GitHub Actions 可以使用 actions/checkoutfetch-depth: 0,然后传入 origin/master。命令会以目标 workspace 作为相对路径根,因此嵌套的 MoonBit workspace 也能正确匹配 Git 文件。

也可以写入 workspace 根目录的 moon_mutest.json

{ "changed_since": "origin/master", "max_mutants": 30, "first": 10, "fail_under": 80, "max_survived": 0 }

命令行参数优先于配置文件。未指定 --config 时,会自动读取 workspace/moon_mutest.json

已人工确认的等价 mutant 可以在配置中注明 id 和原因。它会出现在报告中,但不影响 mutation score:

{ "equivalent": [ { "id": 12, "reason": "该分支在当前域模型中与原逻辑等价" } ] }

对于整行都不应生成 mutation 的生成代码或兼容代码,可在源码行末写 // mutest:ignore。这是一项显式抑制,不应被用来掩盖 escaped mutant。

#质量门禁

将变异测试接入 CI 时,使用质量门禁让不达标的 run 返回非零退出码:

moon run --target js cmd/main -- run . \ --changed-since origin/master \ --fail-under 80 \ --max-survived 0 \ --max-compile-error 0 \ --max-timeout 0 \ --max-skipped 0

--strict-gate 是一组保守默认值:score 至少 90,且不允许 survived、 compile-error、timeout 或 skipped。

仓库内有两个可复现示例:

# 强断言:预期 killed=1、score=100%、质量门禁通过 moon run --target js cmd/main -- run examples/quality_gate_workspace \ --max-mutants 1 --first 1 --fail-under 100 --max-survived 0 --max-skipped 0 # 弱断言:预期 survived=1、risk=high,并给出补测建议 moon run --target js cmd/main -- run examples/weak_test_workspace \ --max-mutants 1 --first 1

#报告

默认文本报告适合终端与 CI 日志。还支持:

# 供 CI 机器消费 moon run --target js cmd/main -- run . --format json --max-mutants 20 # 供代码评审或归档阅读 moon run --target js cmd/main -- run . --format markdown --max-mutants 20 > mutest-report.md # 可离线打开的总览、文件风险排序与 survived 诊断 moon run --target js cmd/main -- run . --format html --max-mutants 20 > mutest-report.html

报告会按文件排序风险,并针对 survived mutant 给出建议。例如数值变异逃逸时会提示 补充精确数值断言和边界值测试。

#常用参数

参数用途
--profile basic\|boundary\|experimental选择变异规则集。
--changed-since REF只测相对 Git 参考点发生变化的生产文件。
--max-mutants N / --first N限制规划或实际执行数量。
--id-start A --id-end B执行半开区间 [A, B) 的 mutant id。
--include-tests也把测试文件作为 mutation 目标。
--include-generated包含生成的 MoonBit 文件。
--keep-temp / --temp-dir PATH保留或指定临时 workspace,便于排障。
--no-fail-fast一个 mutant 执行全部命令,而不是在首次有效信号后停止。
--format text\|markdown\|json\|html选择报告格式。
--fail-under--max-*启用质量门禁。

#作为库使用

///|
test {
let manifest = @moon_mutest.manifest("a == b && true", file="demo.mbt")
inspect(manifest.summary.candidate_count, content="3")
}

根包还提供扫描、规则过滤、项目计划、批次/分片选择、报告和质量门禁 API;详情可查看 生成的 API 文档或 repository layout

#开发与 CI

提交前运行:

moon fmt moon info moon check --target all moon test --target all git diff --exit-code

GitHub Actions 位于 .github/workflows/ci.yml,覆盖 moon checkmoon test moon fmtmoon info、CLI 示例、质量门禁和 HTML 报告。Gitlink 代码流水线可使用 仓库根目录的 Jenkinsfile

当前 MoonBit 工具链若不支持 moon fmt --deny-warnmoon info --deny-warn,CI 会 自动使用对应的最新可用命令,再通过 git diff --exit-code 验证格式与接口文件没有未提交改动。

#边界与许可证

  • 当前 CLI 使用 JS/Node 后端执行真实 workspace;扫描与规划库支持 MoonBit 的常规后端。
  • 变异测试会增加 CI 时间,建议 PR 使用 --changed-since--first,全量扫描放到 nightly。
  • Apache-2.0,见 LICENSE

#
BaselineOutcome

#
BaselineReport

#
BatchStrategy

#
BatchValidation

#
CommandPhase

#
CommandResult

#
ExecutionBatch

#
ExecutionPlan

#
FileMutationPlan

#
FileRunReport

#
MutantOutcome

#
MutantResult

#
MutationCandidate

#
MutationExecution

#
MutationFilter

#
MutationManifest

#
MutationRunReport

#
MutationSummary

#
MutestConfig

#
PatchPreview

#
ProjectMutantResult

#
ProjectMutation

#
ProjectMutationPlan

#
ProjectRunReport

#
QualityGate

#
QualityGateReport

#
QualityGateStatus

#
QualityGateViolation

#
ReportFormat

#
RuleCatalogEntry

#
RuleSetValidation

#
RunnerCommand

#
RunnerScript

#
SelectionMode

#
SelectionReport

#
SelectionValidation

#
ShellDialect

#
WorkspaceFileCategory

#
WorkspaceFileDecision

#
WorkspaceFileSpec

#
WorkspaceSelectionReport

#
apply_edit

fn apply_edit(source : String, edit :
TextEdit
) -> String?

#
apply_mutation

fn apply_mutation(source : String, candidate :
MutationCandidate
) -> String

#
baseline_can_run

fn baseline_can_run(report :
BaselineReport
) -> Bool

#
baseline_outcome_label

fn baseline_outcome_label(outcome :
BaselineOutcome
) -> String

#
boundary_literal_rules

#
candidate_to_json_string

fn candidate_to_json_string(candidate :
MutationCandidate
, indent? : Int) -> String

#
command_failure

fn command_failure(phase :
CommandPhase
, code : Int, stdout? : String, stderr? : String) ->
CommandResult

#
command_phase_label

fn command_phase_label(phase :
CommandPhase
) -> String

#
config_to_json_string

fn config_to_json_string(config :
MutestConfig
, indent? : Int) -> String

#
default_commands

#
diagnose_mutant_result

fn diagnose_mutant_result(result :
ProjectMutantResult
) -> String

#
discover

fn discover(source : String, file? : String) -> Array[
MutationCandidate
]

#
discover_filtered

#
discover_with_profile

#
discover_with_rules

#
edit_for_project_mutation

#
estimate_execution_timeout_ms

fn estimate_execution_timeout_ms(execution :
MutationExecution
) -> Int

#
execution_by_mutation_id

#
experimental_rules

Strong operator replacements intended for a small, opt-in experimental run.

#
file_risk_level

fn file_risk_level(file :
FileRunReport
) -> String

#
format_baseline_report

fn format_baseline_report(report :
BaselineReport
) -> String

#
format_batch_plan

fn format_batch_plan(plan :
BatchPlan
) -> String

#
format_batch_strategy

fn format_batch_strategy(strategy :
BatchStrategy
) -> String

#
format_candidate

fn format_candidate(candidate :
MutationCandidate
) -> String

#
format_candidate_list

fn format_candidate_list(candidates : ArrayView[
MutationCandidate
]) -> String

#
format_config

fn format_config(config :
MutestConfig
) -> String

#
format_execution_plan

fn format_execution_plan(plan :
ExecutionPlan
) -> String

#
format_html_run_report

fn format_html_run_report(report :
ProjectRunReport
) -> String

#
format_manifest

fn format_manifest(source : String, file? : String) -> String

#
format_manifest_json

fn format_manifest_json(source : String, file? : String) -> String

#
format_manifest_json_with_profile

fn format_manifest_json_with_profile(source : String, profile :
RuleProfile
, file? : String) -> String

#
format_manifest_with_profile

fn format_manifest_with_profile(source : String, profile :
RuleProfile
, file? : String) -> String

#
format_markdown_project_plan

fn format_markdown_project_plan(plan :
ProjectMutationPlan
) -> String

#
format_markdown_run_report

fn format_markdown_run_report(report :
ProjectRunReport
) -> String

#
format_mutant_patch

fn format_mutant_patch(mutant :
Mutant
) -> String

#
format_project_mutation

fn format_project_mutation(mutation :
ProjectMutation
) -> String

#
format_project_plan

#
format_project_plan_summary

fn format_project_plan_summary(plan :
ProjectMutationPlan
) -> String

#
format_quality_gate

fn format_quality_gate(gate :
QualityGate
) -> String

#
format_quality_gate_report

fn format_quality_gate_report(report :
QualityGateReport
) -> String

#
format_result

fn format_result(result :
MutantResult
) -> String

#
format_rule_catalog

fn format_rule_catalog(profile? :
RuleProfile
) -> String

#
format_run_report

fn format_run_report(report :
MutationRunReport
) -> String

#
format_runner_command

fn format_runner_command(command :
RunnerCommand
) -> String

#
format_runner_script

fn format_runner_script(script :
RunnerScript
) -> String

#
format_selection_mode

fn format_selection_mode(mode :
SelectionMode
) -> String

#
format_selection_report

fn format_selection_report(report :
SelectionReport
) -> String

#
format_summary

fn format_summary(summary :
MutationSummary
) -> String

#
format_survived_diagnostics

fn format_survived_diagnostics(report :
ProjectRunReport
) -> String

#
format_unified_diff

fn format_unified_diff(file : String, line : Int, before : String, after : String) -> String

#
format_workspace_file_category

fn format_workspace_file_category(category :
WorkspaceFileCategory
) -> String

#
format_workspace_selection_report

fn format_workspace_selection_report(report :
WorkspaceSelectionReport
) -> String

#
generate_mutants

fn generate_mutants(source : String, file? : String) -> Array[
Mutant
]

#
manifest

fn manifest(source : String, file? : String) ->
MutationManifest

#
manifest_to_json_string

fn manifest_to_json_string(manifest :
MutationManifest
, indent? : Int) -> String

#
manifest_with_profile

fn manifest_with_profile(source : String, profile :
RuleProfile
, file? : String) ->
MutationManifest

#
mutant_by_id

fn mutant_by_id(source : String, id : Int, file? : String) ->
Mutant
?

#
mutation_kind_label

fn mutation_kind_label(kind :
MutationKind
) -> String

#
mutation_risk_level

fn mutation_risk_level(report :
ProjectRunReport
) -> String

#
mutation_risk_summary

fn mutation_risk_summary(report :
ProjectRunReport
) -> String

#
outcome_label

fn outcome_label(outcome :
MutantOutcome
) -> String

#
parse_config_json

fn parse_config_json(text : String) ->
MutestConfig
raise

#
parse_rule_profile

fn parse_rule_profile(text : String) ->
RuleProfile
?

#
preview_candidate_patch

#
preview_project_mutation_patch

#
profile_label

fn profile_label(profile :
RuleProfile
) -> String

#
quality_gate_status_label

fn quality_gate_status_label(status :
QualityGateStatus
) -> String

#
report_format_label

fn report_format_label(format :
ReportFormat
) -> String

#
select_changed_workspace_files

Keep only already-selected workspace files whose root-relative paths are present in a Git changed-file list.

#
shell_dialect_label

fn shell_dialect_label(dialect :
ShellDialect
) -> String

#
source_file

fn source_file(path : String, content : String) ->
SourceFile

#
summarize

fn summarize(source : String, file? : String) ->
MutationSummary

#
validate_edit

fn validate_edit(source : String, edit :
TextEdit
) -> Bool

Source Files