generator 教程

本教程使基准测试输入可复现:一个运行种子变为每个数据集相互独立的种子,夹具用它们生成输入,指纹则精确记录被测量的内容。每个示例都是一个完整的测试。

快速上手

moon add Luna-Flow/mare_mark@0.3.0
import {
  "Luna-Flow/mare_mark/model",
  "Luna-Flow/mare_mark/generator",
  "Luna-Flow/mare_mark/fixture",
}
test "one run seed, many dataset seeds" {
  let run_seed = 2026UL
  let seeds = [0, 1, 2].map(id => @generator.derive_seed(run_seed, "dataset", id))
  inspect(seeds[0] != seeds[1] && seeds[1] != seeds[2], content="true")
  inspect(seeds[2] == @generator.derive_seed(2026UL, "dataset", 2), content="true")
}

日常任务

从派生种子生成随机输入

用派生种子驱动任意伪随机生成器。本示例使用一个小型线性同余生成器,因此不需要其他包:

fn random_array(seed : UInt64, length : Int) -> Array[Int] {
  let mut state = seed
  Array::makei(length, _ => {
    state = state * 6364136223846793005UL + 1442695040888963407UL
    (state >> 33).to_int() % 1000
  })
}

test "reproducible random input" {
  let seed = @generator.derive_seed(42UL, "sort-input", 0)
  let first = random_array(seed, 5)
  let again = random_array(seed, 5)
  inspect(first == again, content="true")
  inspect(first.length(), content="5")
}

在夹具中生成

运行器交给夹具一个 GenerationContext,其中包含运行种子、用例 id 和数据集键。在那里派生数据集种子:

test "a seeded fixture" {
  let fixture : @fixture.Fixture[Int, Array[Int], Array[Int]] = @fixture.Fixture::immutable(
    "random-ints",
    "1",
    context => {
      let seed = @generator.derive_seed(context.seed, context.case_id, context.dataset_key.dataset_id)
      random_array(seed, context.dataset_key.scale)
    },
    xs => @generator.stable_fingerprint(xs.map(x => x.to_string()).join(",")),
  )
  let context = @generator.context(42UL, "suite", "sort", @model.DatasetKey::new(8, 0), fixture.id, fixture.version)
  let input = @fixture.materialize(fixture, context)
  inspect(input.length(), content="8")
  inspect(input == @fixture.materialize(fixture, context), content="true")
}

记录被测量的内容

为输入的规范序列化计算指纹,并把它与结果一起保存。运行器会把夹具的指纹放入每个验证事件和每个失败产物中:

test "fingerprints identify serializations" {
  let a = @generator.stable_fingerprint("[1,2,3]")
  let b = @generator.stable_fingerprint("[1, 2, 3]")
  inspect(a.length(), content="71")
  inspect(a == b, content="false")
}

这两个字符串描述的是同一个数组,但它们是不同的序列化,因此请选定一种规范形式。

每次测量的种子

当实验按区组抽取噪声时,请从全部三个 id 派生它:

test "per-measurement seeds" {
  let seeds = [
    @generator.measurement_seed(1UL, 0, 0, 0),
    @generator.measurement_seed(1UL, 0, 0, 1),
    @generator.measurement_seed(1UL, 0, 1, 0),
    @generator.measurement_seed(1UL, 1, 0, 0),
  ]
  let distinct = seeds.all(s => seeds.filter(t => t == s).length() == 1)
  inspect(distinct, content="true")
}

更进一步

  • 把生成函数及其指纹包装在 @generator.Generator 中,并在值发生变化时提升其 version。
  • generator 设计证明了不同的索引和不同的运行种子永远不会产生相同的派生种子。

常见陷阱

  • 对每个数据集直接使用 context.seed。 所有数据集都会抽取同一个流;请按数据集派生。
  • 对非规范文本做哈希。 映射和浮点数在计算指纹前需要固定的顺序和格式。
  • 把指纹当作签名。 它们不带密钥。

后续步骤