用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/mratsim/tattletale --skill testing命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Repository documentation contract for the Tattletale monorepo: the house style for doc comments, module headers, inline comments, and any committed prose (what-over-how, contracts over narration, banned-vocabulary blocklist, format rules, seven canonical reference files). Use when writing or updating doc comments, module headers, inline comments, or any prose in this repo, or when de-sloping existing comments.
Nim bindings to libtorch for tensor operations with high-level sugar
Nim type system patterns and pitfalls
正在显示 SKILL.md
| name | testing |
| description | Nim testing conventions, unittest framework, and C++ compatibility patterns |
| license | MIT |
| compatibility | opencode |
| metadata | {"audience":"developers","workflow":"testing"} |
I provide guidance for writing tests in Nim that:
std/unittest frameworkTorchTensorUse this skill when:
Nim's standard library provides a simple testing framework:
import std/unittest
suite "my module tests":
test "addition works":
check 1 + 1 == 2
test "string handling":
let result = "hello".toUpperAscii()
check result == "HELLO"
Key procs:
suite(name, body) - Group related teststest(name, body) - Define a single testcheck(expr) - Assert expression is true, prints failed value on failuredoAssert(expr) - Like check but raises on failure (use for invariants)submitTest(result) - Submit test result from a procedureWhen you declare variables at module scope (top-level) in Nim tests, the generated C++ code uses = {} initialization:
TorchTensor expectedTensor = {}; // This fails!
expectedTensor = myFunction(a, b);
The C++ torch::Tensor type (and other FFI types with cppNonPod) does not accept brace initialization. This causes:
error: ambiguous overload for 'operator=' (operand types are 'at::Tensor' and '<brace-enclosed initializer list>')
Always wrap test code in a proc main():
import std/unittest, workspace/libtorch
proc generateTensor(): TorchTensor =
# This works - Nim generates:
# auto result = myFunction(a, b);
arange(10, kFloat32)
proc runTests*() =
suite "tensor tests":
test "generate tensor":
let tensor = generateTensor()
check tensor.numel() == 10
when isMainModule:
runTests()
This generates proper C++:
auto tensor = generateTensor(); // No {} initialization
Tests that load files should follow this pattern:
import std/unittest, std/os, workspace/safetensors, workspace/libtorch
const FIXTURES_DIR = currentSourcePath().parentDir() / "fixtures"
proc main() =
suite "safetensors loading":
test "load fixture":
let fixturePath = FIXTURES_DIR / "model.safetensors"
check fileExists(fixturePath)
var mf = memfiles.open(fixturePath, mode = fmRead)
defer: mf.close()
let (st, offset) = safetensors.load(mf)
check st.tensors.len > 0
when isMainModule:
main()
Key points:
currentSourcePath().parentDir() / "fixtures" for fixture pathsmemfiles.open with defer: mf.close()continue for missing fixturesDefine test parameters as const at module level:
const Patterns = ["gradient", "alternating", "repeating"]
const Shapes: array[4, seq[int64]] = [
@[int64 8],
@[int64 4, 4],
@[int64 2, 3, 4],
@[int64 3, 2, 2, 2]
]
const TestedDtypes = [F64, F32, F16, I64, I32, I16, I8, U64, U32, U16, U8]
Extract reusable logic into proc with * export:
proc generateExpectedTensor*(pattern: string, shape: seq[int64], dtype: ScalarKind): TorchTensor =
let shapeRef = shape.asTorchView()
let numel = shape.product()
case pattern
of "gradient":
arange(numel, dtype).reshape(shapeRef).to(dtype)
of "alternating":
let flat = arange(numel, kInt64)
let modVal = (flat % 2).to(kFloat64)
modVal.reshape(shapeRef).to(dtype)
else:
raise newException(ValueError, "Unknown pattern: " & pattern)
Note: Each branch of a case must assign to result.
Each module has a task defined in config.nims for running its tests:
# Test toktoktok
nim test_toktoktok
# Test libtorch
nim test_libtorch
# Test safetensors
nim test_safetensors
The command nim test_toktoktok compiles and runs all test files in workspace/toktoktok/tests/ that start with test_ or t_.
The project uses:
--path:. - Makes workspace/module imports worknim cpp -r plus flags for output and cache directoriesFor this project, fixtures are in:
workspace/toktoktok/tests/tokenizers/
Reference fixtures using:
const FIXTURES_DIR = currentSourcePath().parentDir() / "tokenizers"
If you have a parameter named shape and access a field info.shape:
proc generateExpectedTensor*(pattern: string, shape: seq[int64], ...): TorchTensor =
for info in tensors: # error: 'shape' shadows info.shape
check info.shape == shape
Fix: Rename parameter to avoid shadowing:
proc generateExpectedTensor*(pattern: string, shapeSeq: seq[int64], ...): TorchTensor =
for info in tensors:
check info.shape == shapeSeq # Now works
Each branch of a case must explicitly assign to result:
proc foo(x: int): int =
case x
of 1: result = 10 # Must use 'result ='
of 2: 20 # ERROR: doesn't assign!
Follow the naming convention: test_*.nim or t_*.nim in the module's tests/ directory.
# workspace/my_module/tests/test_myfeature.nim
import std/unittest, std/os
import workspace/my_module
proc runMyFeatureTests*() =
suite "my feature tests":
test "basic functionality":
let result = myModule.function()
check result == expectedValue
when isMainModule:
runMyFeatureTests()
The test will be discovered automatically by the test command:
# If it's in my_module:
nim c -r --task:test_my_module
Or run all tests for the module:
nim test_my_module
Create a fixtures/ directory and add test data:
workspace/my_module/tests/fixtures/
Reference in test code:
const FIXTURES_DIR = currentSourcePath().parentDir() / "fixtures"
let fixturePath = FIXTURES_DIR / "test_data.bin"
test_ or t_workspace/module/tests/proc runTests*()when isMainModule: runTests() at the enddefer for resource cleanup (files, etc.)*constFor AI/ML modules, test vectors are generated via Python scripts using torch and safetensors.
workspace/module/
├── tests/
│ ├── test_module.nim # Nim tests
│ ├── fixtures/ # Generated fixture files
│ │ ├── model.safetensors
│ │ └── tokenizer.json
│ └── testgen/ # Python test vector generators
│ └── generate_vectors.py
pyproject.toml with [dependency-groups] for shared dependencies:
[dependency-groups]
test-vectors = [
"torch>=2.0.0",
"safetensors>=0.7.0",
"transformers>=4.40.0",
"numpy>=2.4.2",
]
uv run --group test-vectors python workspace/module/tests/testgen/generate_vectors.pyimport torch
import numpy as np
from safetensors.numpy import save_file
import os
FIXTURES_DIR = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"fixtures",
)
def generate_vandermonde():
x = torch.arange(1, 6, dtype=torch.float32)
vandermonde = torch.vander(x, increasing=True).T
return vandermonde.to(torch.bfloat16).view(torch.uint16).numpy()
def main():
fixtures = {
"BF16_vandermonde_5x5": generate_vandermonde(),
}
save_file(fixtures, os.path.join(FIXTURES_DIR, "vandermonde.safetensors"))
print("Fixtures generated")
if __name__ == "__main__":
main()
When adding new test vectors, regenerate the fixture files:
uv run --group test-vectors python workspace/module/tests/testgen/generate_vectors.py
Tests involving TorchTensor and other libtorch FFI types should use the shared test utilities:
import workspace/libtorch_testutils
Wrap test code that may throw C++ exceptions:
proc testTensorOps(): bool =
let a = ones(@[2, 3], kFloat32)
let b = zeros(@[2, 3], kFloat32)
let c = a + b
result = c.isDefined()
when isMainModule:
runCppTest("tensor operations", testTensorOps) # Handles exceptions automatically
Or use the template directly:
check catchCppExceptions(testTensorOps())
assertDefined - Check tensor is initialized:
let tensor = ones(@[2, 3], kFloat32)
assertDefined(tensor) # Raises if not defined
assertDefined(tensor, "weight") # Custom name in error
assertShape - Verify tensor dimensions:
let tensor = randn(@[2, 3, 4])
assertShape(tensor, 2, 3, 4)
assertDtype - Verify tensor dtype:
let tensor = ones(@[2, 3], kFloat32)
assertDtype(tensor, kFloat32)
assertAllClose / assertClose - Compare tensor values:
let actual = computeSomething()
let expected = ones(@[2, 3], kFloat32) * 2.0
assertAllClose(actual, expected) # Default rtol=2e-2, abstol=2e-2
assertClose(actual, expected, rtol=1e-5, abstol=1e-5) # Custom tolerance
printTensor - Print tensor with label:
printTensor(myTensor, "Weight matrix")
printTensorShape - Print shape and dtype:
printTensorShape(myTensor, "Input")
# Output: Input:
# Shape: [2, 3, 4], Dtype: kFloat32
ptrHex - Convert pointer to hex string for aliasing detection:
let tensor = ones(@[2, 3], kFloat32)
echo "data_ptr = 0x", tensor.data_ptr().ptrHex()
echo "shape.data() = 0x", tensor.shape.data().ptrHex()
# Useful for detecting memory aliasing issues
dataPtrHex / shapePtrHex - Convenience wrappers:
let tensor = ones(@[2, 3], kFloat32)
echo "data_ptr = 0x", tensor.dataPtrHex()
echo "shape_ptr = 0x", tensor.shapePtrHex()
# Equivalent to above but more convenient
printTensorShape(myTensor, "Input")
# Output: Input:
# Shape: [2, 3, 4], Dtype: kFloat32
traceExec - Debug macro to trace execution:
traceExec:
let a = ones(@[2, 3])
let b = zeros(@[2, 3])
let c = a + b
# Prints each statement before executing
Complete example:
# workspace/my_module/tests/test_feature.nim
import
std/unittest,
workspace/libtorch,
workspace/libtorch_testutils,
workspace/my_module
proc testBasicFunctionality(): bool =
let input = ones(@[2, 3], kFloat32)
let result = myModule.process(input)
assertDefined(result)
assertShape(result, 2, 3)
result = true
proc testEdgeCase(): bool =
let input = zeros(@[1], kFloat32)
let output = myModule.process(input)
assertAllClose(output, input)
result = true
when isMainModule:
runCppTest("basic functionality", testBasicFunctionality)
runCppTest("edge case", testEdgeCase)
workspace/libtorch_testutils for tests with TorchTensorrunCppTest for formatted output with automatic exception handling
(captures the C++ stacktrace; do NOT confuse it with crucible's private
proc runTest() scope wrapper — see workspace/crucible/AGENTS.md)catchCppExceptions when integrating with std/unittest checkassertDefined, assertShape, etc.) for clear error messagesprintTensor and printTensorShape for debugging failuresworkspace/module/tests/ directorytest_ or t_A kernel test that compares against a libtorch reference is three clearly separated parts, so a reader always sees which variable comes from the kernel, which from the reference, and where the comparison happens:
check*() proc that builds the shared inputs once, calls both, and
asserts.proc kernelUnderTest(xf: seq[float32], M, N: int): F.Tensor =
## Runs the kernel on the fp16-rounded inputs; returns the fp16
## output converted to fp32.
var engine = bkMetal.init()
engine.ingest(kernelMsl)
... # build fp16 buffers from xf, engine.run, read back
result = toTensor(outF).reshape(M, N)
proc reference(xf: seq[float32], M, N: int): F.Tensor =
## The reference: torch op over the same fp16-rounded inputs.
let xh = toTensor(xf).reshape(M, N).to(kFloat16)
result = F.some_op(xh.to(kFloat32))
proc checkFeature(): bool =
## Kernel output vs the torch reference on one random batch.
Torch.manual_seed(0x5EED'u64)
let xf = scaledRand(M, N, 2.0'f32)
let actual = kernelUnderTest(xf, M, N)
let expected = reference(xf, M, N)
echo &" worst |Δ| = {worstAbsDiff(actual, expected)} (tolerance 5e-3)"
assertAllClose(actual, expected, rtol = 0.0'f64, abstol = 5e-3'f64)
result = true
when isMainModule:
runCppTest("feature vs the torch reference", checkFeature)
Rules:
actual and expected (or kernel/ref), so the
kernel-vs-reference split is visible at the assert.check*() proc stays readable.worstAbsDiff(a, b: F.Tensor): float32 helper prints the worst
deviation before the assert; the assert carries the tolerance.A kernel test's reference must be libtorch math over the same fp16-rounded inputs — never a Nim reimplementation of the kernel's algorithm, and never a reference that shares the kernel's own code path.
Rebuilding the kernel's arithmetic in Nim (matmul loops, norms, rope rotations, ...) instead of calling libtorch is banned:
manual_silu_and_mul_fp16.nim); the reference call
must be a torch op — F.linear, F.rmsNorm,
F.scaled_dot_product_attention, ... from the transformers workspace
(workspace/transformers/src/layers/linear.nim pattern).Data-preparation helpers that mirror a storage format (e.g. a dequant decode table that rebuilds an fp16 weight matrix from its packed bits) are allowed — they are fixture-style reconstruction, not the op's arithmetic — but they must live once in a shared helper, not be copied per test.
A reference that cannot diverge from the kernel proves nothing:
The reference must be computed independently of the kernel: same random inputs, fp16 rounding applied identically, torch arithmetic.