feat: conformance tests pass for first time

Change implementation to exponentially increase search space at each
level.
This commit is contained in:
nobody 2025-12-12 21:05:29 -08:00
commit 546d6deb69
Signed by: GrocerPublishAgent
GPG key ID: D460CD54A9E3AB86
13 changed files with 1852 additions and 102 deletions

View file

@ -0,0 +1,28 @@
# Conformance Tests
I'm using the Rust implementation as the source of truth. It was pretty easy to write unit tests and measure performance in Rust. I'm most familiar with testing tools in Rust so I did all the validation work there. Now I just want to check that all the other language implementations behave exactly the same as the Rust version, which I poured all this energy into validating for correctness.
## Running the tests
| Language | Directory | Command |
|----------|-----------|---------|
| TypeScript | `runners/typescript` | `npm run test` |
## Generating test data
To generate the test data, run:
```
cd generator
cargo run
```
This produces `.scenario.json` files in `genfiles/`. Each file contains a sequence of random numbers and the expected outputs from the Rust implementation.
The idea is that we replay the same sequence of random numbers for each implementation. All of the different language libraries allow you to inject the source of randomness, so the tests just inject a mock random generator that reads from the recorded sequence. In theory, every implementation makes the exact same calls to random in the same order, and they all produce identical output.
## Scenario format
You can test the entire public API by assuming you have a sorted list of sort keys. A sort key is just an LSEQ identifier. Each scenario has an `initialState` array of sort keys in sorted order. Then there's a list of operations: pick an array index to insert before or after, which tells you which two boundary keys to use for the `alloc` function from the paper. You call alloc, insert the result into the list, and repeat.
This setup can test everything you'd want to test, even if it's a bit indirect for very basic cases. You could still write basic unit tests in each language for the simple stuff.

204
conformance-tests/generator/Cargo.lock generated Normal file
View file

@ -0,0 +1,204 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "conformance-test-generator"
version = "0.1.0"
dependencies = [
"peoplesgrocers-lseq",
"rand",
"serde",
"serde_json",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "peoplesgrocers-lseq"
version = "1.0.0"
dependencies = [
"rand",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "zerocopy"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -0,0 +1,10 @@
[package]
name = "conformance-test-generator"
version = "0.1.0"
edition = "2024"
[dependencies]
peoplesgrocers-lseq = { path = "../../rust" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.8"

View file

@ -0,0 +1,254 @@
mod model;
use model::{Insert, Scenario};
use peoplesgrocers_lseq::{LseqRng, SortKey, LSEQ};
use rand::{rngs::StdRng, Rng, SeedableRng};
use std::fs;
/// A recording RNG wrapper that records random values as floats
/// compatible with TypeScript's Math.random() consumption pattern.
struct RecordingRng {
inner: StdRng,
recorded: Vec<f64>,
}
impl RecordingRng {
fn new(seed: u64) -> Self {
RecordingRng {
inner: StdRng::seed_from_u64(seed),
recorded: Vec::new(),
}
}
fn take_recorded(&mut self) -> Vec<f64> {
std::mem::take(&mut self.recorded)
}
}
impl LseqRng for RecordingRng {
fn gen_bool(&mut self, p: f64) -> bool {
// Use fully qualified syntax to call rand::Rng method
let result = Rng::gen_bool(&mut self.inner, p);
// Record a float that TypeScript's `f < p` would produce the same result
// If result is true, we need f < p, so use p/2
// If result is false, we need f >= p, so use (1 + p) / 2
let recorded_value = if result { p / 2.0 } else { (1.0 + p) / 2.0 };
self.recorded.push(recorded_value);
result
}
fn gen_range(&mut self, range: std::ops::Range<u64>) -> u64 {
// Use fully qualified syntax to call rand::Rng method
let result: u64 = Rng::gen_range(&mut self.inner, range.clone());
// Record a float that TypeScript's `Math.floor(f * range_size)` would produce the same result
// For result r in [0, range_size), we need floor(f * range_size) = r
// So f should be in [r/range_size, (r+1)/range_size)
// Use midpoint: (r + 0.5) / range_size
let range_size = (range.end - range.start) as f64;
let relative_result = (result - range.start) as f64;
let recorded_value = (relative_result + 0.5) / range_size;
self.recorded.push(recorded_value);
result
}
}
fn generate_scenario(
name: &str,
description: &str,
seed: u64,
init: Vec<&str>,
ops: impl FnOnce(&mut LSEQ<RecordingRng>, &mut Vec<String>) -> Vec<Insert>,
) -> Scenario {
let mut lseq = LSEQ::new(RecordingRng::new(seed));
let mut state: Vec<String> = init.iter().map(|s| s.to_string()).collect();
let operations = ops(&mut lseq, &mut state);
Scenario {
name: name.to_string(),
description: description.to_string(),
seed,
init: init.iter().map(|s| s.to_string()).collect(),
rng: lseq.take_rng().take_recorded(),
operations,
}
}
fn main() {
let scenarios = vec![
// 01 - Sequential append
generate_scenario(
"sequential-append",
"20 inserts, each after the last",
42,
vec![],
|lseq, state| {
let mut ops = Vec::new();
for _ in 0..20 {
let before_key = state.last().map(|s| s.parse::<SortKey>().unwrap());
let result = lseq.alloc(before_key.as_ref(), None);
let result_str = result.to_string();
ops.push(Insert::After {
index: -1, // after last element (or beginning if empty)
outcome: result_str.clone(),
});
state.push(result_str);
}
ops
},
),
// 02 - Sequential prepend
generate_scenario(
"sequential-prepend",
"20 inserts, each before the first",
43,
vec![],
|lseq, state| {
let mut ops = Vec::new();
for _ in 0..20 {
let after_key = state.first().map(|s| s.parse::<SortKey>().unwrap());
let result = lseq.alloc(None, after_key.as_ref());
let result_str = result.to_string();
ops.push(Insert::Before {
index: 0, // before first element (prepend)
outcome: result_str.clone(),
});
state.insert(0, result_str);
}
ops
},
),
// 03 - Random insert 100 items
generate_scenario(
"random-insert-100-items",
"100 inserts at random positions",
44,
vec![],
|lseq, state| {
let mut ops = Vec::new();
let mut position_rng = StdRng::seed_from_u64(100); // Separate RNG for positions
for _ in 0..100 {
// Pick a random insertion point (0..=state.len())
let idx = if state.is_empty() {
0
} else {
Rng::gen_range(&mut position_rng, 0..=state.len())
};
// Derive beforeKey and afterKey from the insertion index
let before_key = if idx > 0 {
Some(state[idx - 1].parse::<SortKey>().unwrap())
} else {
None
};
let after_key = if idx < state.len() {
Some(state[idx].parse::<SortKey>().unwrap())
} else {
None
};
let result = lseq.alloc(before_key.as_ref(), after_key.as_ref());
let result_str = result.to_string();
ops.push(Insert::Before {
index: idx as i32,
outcome: result_str.clone(),
});
state.insert(idx, result_str);
}
ops
},
),
// 04 - Dense packing
generate_scenario(
"dense-packing",
"20 inserts between adjacent keys '0' and '1'",
45,
vec!["0", "1"],
|lseq, state| {
let mut ops = Vec::new();
// Always insert between first and second element
for _ in 0..20 {
let before_key = state[0].parse::<SortKey>().unwrap();
let after_key = state[1].parse::<SortKey>().unwrap();
let result = lseq.alloc(Some(&before_key), Some(&after_key));
let result_str = result.to_string();
ops.push(Insert::Before {
index: 1, // between state[0] and state[1]
outcome: result_str.clone(),
});
state.insert(1, result_str);
}
ops
},
),
// 05 - Deep nesting
generate_scenario(
"deep-nesting",
"Force 5+ level deep keys by inserting between adjacent keys",
46,
vec!["M", "N"],
|lseq, state| {
let mut ops = Vec::new();
// Keep inserting between first two to force depth
for _ in 0..30 {
let before_key = state[0].parse::<SortKey>().unwrap();
let after_key = state[1].parse::<SortKey>().unwrap();
let result = lseq.alloc(Some(&before_key), Some(&after_key));
let result_str = result.to_string();
ops.push(Insert::Before {
index: 1, // between state[0] and state[1]
outcome: result_str.clone(),
});
state.insert(1, result_str);
}
ops
},
),
// 06 - Edge min interval
generate_scenario(
"edge-min-interval",
"Insert between adjacent keys (A, B) which have interval=1",
47,
vec!["A", "B"],
|lseq, state| {
let mut ops = Vec::new();
for _ in 0..10 {
let before_key = state[0].parse::<SortKey>().unwrap();
let after_key = state[1].parse::<SortKey>().unwrap();
let result = lseq.alloc(Some(&before_key), Some(&after_key));
let result_str = result.to_string();
ops.push(Insert::Before {
index: 1, // between state[0] and state[1]
outcome: result_str.clone(),
});
state.insert(1, result_str);
}
ops
},
),
];
// Write each scenario to a file
let output_dir = "../genfiles";
fs::create_dir_all(output_dir).expect("Failed to create genfiles directory");
for (i, scenario) in scenarios.iter().enumerate() {
let filename = format!("{}/{:02}-{}.scenario.json", output_dir, i + 1, scenario.name);
let json = serde_json::to_string_pretty(scenario).expect("Failed to serialize scenario");
fs::write(&filename, json).expect("Failed to write scenario file");
println!("Generated: {}", filename);
}
}

View file

@ -0,0 +1,46 @@
use serde::ser::{SerializeMap, Serializer};
use serde::Serialize;
#[derive(Serialize)]
pub struct Scenario {
pub name: String,
pub description: String,
pub seed: u64,
pub init: Vec<String>,
pub rng: Vec<f64>,
pub operations: Vec<Insert>,
}
/// Specifies an insertion point in the sequence.
///
/// - `Insert::Before { index: 3, .. }` → insert before state[3]
/// - `Insert::Before { index: 0, .. }` → insert at start
/// - `Insert::After { index: 2, .. }` → insert after state[2]
/// - `Insert::After { index: -1, .. }` → insert at end (Python-style negative index)
///
/// Serializes to `{ "before": n, "expected": "..." }` or `{ "after": n, "expected": "..." }`.
#[derive(Debug)]
pub enum Insert {
Before { index: i32, outcome: String },
After { index: i32, outcome: String },
}
impl Serialize for Insert {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(2))?;
match self {
Insert::Before { index, outcome } => {
map.serialize_entry("before", index)?;
map.serialize_entry("expected", outcome)?;
}
Insert::After { index, outcome } => {
map.serialize_entry("after", index)?;
map.serialize_entry("expected", outcome)?;
}
}
map.end()
}
}

View file

View file

@ -0,0 +1,674 @@
{
"name": "lseq-conformance-runner",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lseq-conformance-runner",
"version": "1.0.0",
"dependencies": {
"@peoplesgrocers/lseq": "file:../../../typescript",
"uvu": "^0.5.6"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.7.0",
"typescript": "^5.0.0"
}
},
"../../../typescript": {
"name": "@peoplesgrocers/lseq",
"version": "1.0.0",
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"tsx": "^4.7.0",
"typescript": "^5.0.0",
"uvu": "^0.5.6"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
"integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
"integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
"integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
"integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
"integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
"integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
"integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
"integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
"integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
"integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
"integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
"integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
"integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
"integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
"integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
"integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
"integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
"integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
"integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
"integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
"integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
"integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
"integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
"integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
"integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
"integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@peoplesgrocers/lseq": {
"resolved": "../../../typescript",
"link": true
},
"node_modules/@types/node": {
"version": "20.19.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
"integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/esbuild": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
"integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.1",
"@esbuild/android-arm": "0.27.1",
"@esbuild/android-arm64": "0.27.1",
"@esbuild/android-x64": "0.27.1",
"@esbuild/darwin-arm64": "0.27.1",
"@esbuild/darwin-x64": "0.27.1",
"@esbuild/freebsd-arm64": "0.27.1",
"@esbuild/freebsd-x64": "0.27.1",
"@esbuild/linux-arm": "0.27.1",
"@esbuild/linux-arm64": "0.27.1",
"@esbuild/linux-ia32": "0.27.1",
"@esbuild/linux-loong64": "0.27.1",
"@esbuild/linux-mips64el": "0.27.1",
"@esbuild/linux-ppc64": "0.27.1",
"@esbuild/linux-riscv64": "0.27.1",
"@esbuild/linux-s390x": "0.27.1",
"@esbuild/linux-x64": "0.27.1",
"@esbuild/netbsd-arm64": "0.27.1",
"@esbuild/netbsd-x64": "0.27.1",
"@esbuild/openbsd-arm64": "0.27.1",
"@esbuild/openbsd-x64": "0.27.1",
"@esbuild/openharmony-arm64": "0.27.1",
"@esbuild/sunos-x64": "0.27.1",
"@esbuild/win32-arm64": "0.27.1",
"@esbuild/win32-ia32": "0.27.1",
"@esbuild/win32-x64": "0.27.1"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/uvu": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
"integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
"kleur": "^4.0.3",
"sade": "^1.7.3"
},
"bin": {
"uvu": "bin.js"
},
"engines": {
"node": ">=8"
}
}
}
}

View file

@ -0,0 +1,18 @@
{
"name": "lseq-conformance-runner",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "tsx src/runner.ts"
},
"dependencies": {
"@peoplesgrocers/lseq": "file:../../../typescript",
"uvu": "^0.5.6"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.7.0",
"typescript": "^5.0.0"
}
}

View file

@ -0,0 +1,89 @@
import { LSEQ } from "@peoplesgrocers/lseq";
import { test } from "uvu";
import * as assert from "uvu/assert";
import * as fs from "fs";
import * as path from "path";
interface Operation {
before?: number;
after?: number;
expected: string;
}
interface Scenario {
name: string;
description: string;
seed: number;
init: string[];
rng: number[];
operations: Operation[];
}
function createMockRandom(values: number[]): () => number {
let index = 0;
return () => {
if (index >= values.length) {
throw new Error(`Ran out of random values at index ${index}`);
}
return values[index++];
};
}
const testDataDir = path.join(import.meta.dirname!, "../../../genfiles");
const files = fs
.readdirSync(testDataDir)
.filter((f) => f.endsWith(".scenario.json"))
.sort();
for (const file of files) {
const filePath = path.join(testDataDir, file);
const scenario: Scenario = JSON.parse(fs.readFileSync(filePath, "utf-8"));
test(`${scenario.name}`, () => {
const mockRandom = createMockRandom(scenario.rng);
const lseq = new LSEQ(mockRandom);
const state: string[] = [...scenario.init];
for (let i = 0; i < scenario.operations.length; i++) {
const op = scenario.operations[i];
// Derive beforeKey and afterKey from the insertion point
// - `before: X` means insert before index X → beforeKey=state[X-1], afterKey=state[X]
// - `after: X` means insert after index X → beforeKey=state[X], afterKey=state[X+1]
let beforeKey: string | null = null;
let afterKey: string | null = null;
let insertIdx: number;
if (op.before !== undefined) {
// Insert before index X
const idx = op.before < 0 ? state.length + op.before : op.before;
beforeKey = idx > 0 ? state[idx - 1] : null;
afterKey = idx < state.length ? state[idx] : null;
insertIdx = idx;
} else if (op.after !== undefined) {
// Insert after index X
const idx = op.after < 0 ? state.length + op.after : op.after;
beforeKey = idx >= 0 && idx < state.length ? state[idx] : null;
afterKey = idx + 1 < state.length ? state[idx + 1] : null;
insertIdx = idx + 1;
} else {
// Neither specified - insert at end
beforeKey = state.length > 0 ? state[state.length - 1] : null;
afterKey = null;
insertIdx = state.length;
}
const result = lseq.alloc(beforeKey, afterKey);
assert.is(
result,
op.expected,
`op ${i}: alloc(${beforeKey}, ${afterKey})`
);
state.splice(insertIdx, 0, result);
}
});
}
test.run();

View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"]
}