بنقرة واحدة
بنقرة واحدة
| 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:
runTest("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:
runTest("basic functionality", testBasicFunctionality)
runTest("edge case", testEdgeCase)
workspace/libtorch_testutils for tests with TorchTensorrunTest for formatted output with automatic exception handlingcatchCppExceptions when integrating with std/unittest checkassertDefined, assertShape, etc.) for clear error messagesprintTensor and printTensorShape for debugging failuresworkspace/module/tests/ directorytest_ or t_Nim bindings to libtorch for tensor operations with high-level sugar
Nim type system patterns and pitfalls
Common import patterns and pitfalls for the Tattletale Nim project
Regex functionality in Nim including std/re, std/nre wrappers around PCRE, and the pure Nim nim-regex alternative with linear-time matching guarantees
Nim to Python interoperability including nimpy for calling Python from Nim and exporting Nim to Python, nimporter for packaging Nim modules as Python packages, and cffi/ctypes for calling Nim from Python
Nim's hash table module for key-value storage