mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat(stitched): Implement a simple REPL for testing stitched ordering
This commit is contained in:
Generated
+41
@@ -935,6 +935,15 @@ version = "0.7.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clipboard-win"
|
||||||
|
version = "5.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
|
||||||
|
dependencies = [
|
||||||
|
"error-code",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
@@ -1820,6 +1829,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "error-code"
|
||||||
|
version = "3.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.3.1"
|
version = "5.3.1"
|
||||||
@@ -4613,6 +4628,25 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustyline"
|
||||||
|
version = "17.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"clipboard-win",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"nix",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width 0.2.2",
|
||||||
|
"utf8parse",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustyline-async"
|
name = "rustyline-async"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -5142,6 +5176,7 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"peg",
|
"peg",
|
||||||
|
"rustyline",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5905,6 +5940,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ itertools.workspace = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
peg = "0.8.5"
|
peg = "0.8.5"
|
||||||
|
rustyline = { version = "17.0.2", default-features = false }
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use rustyline::{DefaultEditor, Result, error::ReadlineError};
|
||||||
|
use stitcher::{Batch, EventEdges, Stitcher, memory_backend::MemoryStitcherBackend};
|
||||||
|
|
||||||
|
const BANNER: &str = "
|
||||||
|
stitched ordering test repl
|
||||||
|
- append an event by typing its name: `A`
|
||||||
|
- to add prev events type an arrow and then space-separated event names: `A --> B C D`
|
||||||
|
- use `/reset` to clear the ordering
|
||||||
|
Ctrl-D to exit, Ctrl-C to clear input
|
||||||
|
"
|
||||||
|
.trim_ascii();
|
||||||
|
|
||||||
|
enum Command<'line> {
|
||||||
|
AppendEvents(EventEdges<'line>),
|
||||||
|
ResetOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
peg::parser! {
|
||||||
|
// partially copied from the test case parser
|
||||||
|
grammar command_parser() for str {
|
||||||
|
/// Parse whitespace.
|
||||||
|
rule _ -> () = quiet! { $([' '])* {} }
|
||||||
|
|
||||||
|
/// Parse empty lines and comments.
|
||||||
|
rule newline() -> () = quiet! { (("#" [^'\n']*)? "\n")+ {} }
|
||||||
|
|
||||||
|
/// Parse an event ID.
|
||||||
|
rule event_id() -> &'input str
|
||||||
|
= quiet! { id:$([char if char.is_ascii_alphanumeric() || ['_', '-'].contains(&char)]+) { id } }
|
||||||
|
/ expected!("event id")
|
||||||
|
|
||||||
|
/// Parse an event and its prev events.
|
||||||
|
rule event() -> (&'input str, HashSet<&'input str>)
|
||||||
|
= id:event_id() prev_events:(_ "-->" _ id:(event_id() ++ _) { id })? {
|
||||||
|
(id, prev_events.into_iter().flatten().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub rule command() -> Command<'input> =
|
||||||
|
"/reset" { Command::ResetOrder }
|
||||||
|
/ events:event() ++ newline() { Command::AppendEvents(events.into_iter().collect()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let mut backend = MemoryStitcherBackend::default();
|
||||||
|
let mut reader = DefaultEditor::new()?;
|
||||||
|
|
||||||
|
println!("{BANNER}");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.readline("> ") {
|
||||||
|
| Ok(line) => match command_parser::command(&line) {
|
||||||
|
| Ok(Command::AppendEvents(events)) => {
|
||||||
|
let batch = Batch::from_edges(&events);
|
||||||
|
let stitcher = Stitcher::new(&backend);
|
||||||
|
let updates = stitcher.stitch(&batch);
|
||||||
|
|
||||||
|
for update in &updates.gap_updates {
|
||||||
|
println!("update to gap {}:", update.key);
|
||||||
|
println!(" new gap contents: {:?}", update.gap);
|
||||||
|
println!(" inserted items: {:?}", update.inserted_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("new items: {:?}", &updates.new_items);
|
||||||
|
println!("events added to gaps: {:?}", &updates.events_added_to_gaps);
|
||||||
|
|
||||||
|
backend.extend(updates);
|
||||||
|
println!("\norder: {backend:?}");
|
||||||
|
},
|
||||||
|
| Ok(Command::ResetOrder) => {
|
||||||
|
backend.clear();
|
||||||
|
println!("order cleared.");
|
||||||
|
},
|
||||||
|
| Err(parse_error) => {
|
||||||
|
println!("parse error!! {parse_error}");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
| Err(ReadlineError::Interrupted) => {
|
||||||
|
println!("interrupt");
|
||||||
|
},
|
||||||
|
| Err(ReadlineError::Eof) => {
|
||||||
|
println!("goodbye :3");
|
||||||
|
break Ok(());
|
||||||
|
},
|
||||||
|
| Err(err) => break Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
sync::atomic::{AtomicU64, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Gap, OrderUpdates, StitchedItem, StitcherBackend};
|
||||||
|
|
||||||
|
/// A version of [`StitchedItem`] which owns event IDs.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum MemoryStitcherItem {
|
||||||
|
Event(String),
|
||||||
|
Gap(Gap),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StitchedItem<'_>> for MemoryStitcherItem {
|
||||||
|
fn from(value: StitchedItem) -> Self {
|
||||||
|
match value {
|
||||||
|
| StitchedItem::Event(id) => MemoryStitcherItem::Event(id.to_string()),
|
||||||
|
| StitchedItem::Gap(gap) => MemoryStitcherItem::Gap(gap),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'id> From<&'id MemoryStitcherItem> for StitchedItem<'id> {
|
||||||
|
fn from(value: &'id MemoryStitcherItem) -> Self {
|
||||||
|
match value {
|
||||||
|
| MemoryStitcherItem::Event(id) => StitchedItem::Event(id),
|
||||||
|
| MemoryStitcherItem::Gap(gap) => StitchedItem::Gap(gap.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A stitcher backend which holds a stitched ordering in RAM.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MemoryStitcherBackend {
|
||||||
|
items: Vec<(u64, MemoryStitcherItem)>,
|
||||||
|
counter: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryStitcherBackend {
|
||||||
|
fn next_id(&self) -> u64 { self.counter.fetch_add(1, Ordering::Relaxed) }
|
||||||
|
|
||||||
|
/// Extend this ordering with new updates.
|
||||||
|
pub fn extend(&mut self, results: OrderUpdates<'_, <Self as StitcherBackend>::Key>) {
|
||||||
|
for update in results.gap_updates {
|
||||||
|
let Some(gap_index) = self.items.iter().position(|(key, _)| *key == update.key)
|
||||||
|
else {
|
||||||
|
panic!("bad update key {}", update.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
let insertion_index = if update.gap.is_empty() {
|
||||||
|
self.items.remove(gap_index);
|
||||||
|
gap_index
|
||||||
|
} else {
|
||||||
|
match self.items.get_mut(gap_index) {
|
||||||
|
| Some((_, MemoryStitcherItem::Gap(gap))) => {
|
||||||
|
*gap = update.gap;
|
||||||
|
},
|
||||||
|
| Some((key, other)) => {
|
||||||
|
panic!("expected item with key {key} to be a gap, it was {other:?}");
|
||||||
|
},
|
||||||
|
| None => unreachable!("we just checked that this index is valid"),
|
||||||
|
}
|
||||||
|
gap_index.checked_add(1).expect(
|
||||||
|
"should never allocate usize::MAX ids. what kind of test are you running",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let to_insert: Vec<_> = update
|
||||||
|
.inserted_items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| (self.next_id(), item.into()))
|
||||||
|
.collect();
|
||||||
|
self.items
|
||||||
|
.splice(insertion_index..insertion_index, to_insert.into_iter())
|
||||||
|
.for_each(drop);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_items: Vec<_> = results
|
||||||
|
.new_items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| (self.next_id(), item.into()))
|
||||||
|
.collect();
|
||||||
|
self.items.extend(new_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over the items in this ordering.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = StitchedItem<'_>> {
|
||||||
|
self.items.iter().map(|(_, item)| item.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear this ordering.
|
||||||
|
pub fn clear(&mut self) { self.items.clear(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StitcherBackend for MemoryStitcherBackend {
|
||||||
|
type Key = u64;
|
||||||
|
|
||||||
|
fn find_matching_gaps<'a>(
|
||||||
|
&'a self,
|
||||||
|
events: impl Iterator<Item = &'a str>,
|
||||||
|
) -> impl Iterator<Item = (Self::Key, Gap)> {
|
||||||
|
// nobody cares about test suite performance right
|
||||||
|
let mut gaps = vec![];
|
||||||
|
|
||||||
|
for event in events {
|
||||||
|
for (key, item) in &self.items {
|
||||||
|
if let MemoryStitcherItem::Gap(gap) = item
|
||||||
|
&& gap.contains(event)
|
||||||
|
{
|
||||||
|
gaps.push((*key, gap.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gaps.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_exists<'a>(&'a self, event: &'a str) -> bool {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.any(|item| matches!(&item.1, MemoryStitcherItem::Event(id) if event == id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MemoryStitcherBackend {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_list().entries(self.iter()).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ use std::{cmp::Ordering, collections::HashSet};
|
|||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
pub mod algorithm;
|
pub mod algorithm;
|
||||||
|
pub mod memory_backend;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
pub use algorithm::*;
|
pub use algorithm::*;
|
||||||
|
|
||||||
/// A gap in the stitched order.
|
/// A gap in the stitched order.
|
||||||
|
|||||||
@@ -1,101 +1,12 @@
|
|||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use super::{algorithm::*, *};
|
use super::{algorithm::*, *};
|
||||||
|
use crate::memory_backend::MemoryStitcherBackend;
|
||||||
|
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
||||||
/// A stitcher backend which holds a stitched ordering in RAM.
|
|
||||||
#[derive(Default)]
|
|
||||||
struct TestStitcherBackend<'id> {
|
|
||||||
items: Vec<(u64, StitchedItem<'id>)>,
|
|
||||||
counter: AtomicU64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'id> TestStitcherBackend<'id> {
|
|
||||||
fn next_id(&self) -> u64 { self.counter.fetch_add(1, Ordering::Relaxed) }
|
|
||||||
|
|
||||||
fn extend(&mut self, results: OrderUpdates<'id, <Self as StitcherBackend>::Key>) {
|
|
||||||
for update in results.gap_updates {
|
|
||||||
let Some(gap_index) = self.items.iter().position(|(key, _)| *key == update.key)
|
|
||||||
else {
|
|
||||||
panic!("bad update key {}", update.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
let insertion_index = if update.gap.is_empty() {
|
|
||||||
self.items.remove(gap_index);
|
|
||||||
gap_index
|
|
||||||
} else {
|
|
||||||
match self.items.get_mut(gap_index) {
|
|
||||||
| Some((_, StitchedItem::Gap(gap))) => {
|
|
||||||
*gap = update.gap;
|
|
||||||
},
|
|
||||||
| Some((key, other)) => {
|
|
||||||
panic!("expected item with key {key} to be a gap, it was {other:?}");
|
|
||||||
},
|
|
||||||
| None => unreachable!("we just checked that this index is valid"),
|
|
||||||
}
|
|
||||||
gap_index.checked_add(1).expect(
|
|
||||||
"should never allocate usize::MAX ids. what kind of test are you running",
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let to_insert: Vec<_> = update
|
|
||||||
.inserted_items
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| (self.next_id(), item))
|
|
||||||
.collect();
|
|
||||||
self.items
|
|
||||||
.splice(insertion_index..insertion_index, to_insert.into_iter())
|
|
||||||
.for_each(drop);
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_items: Vec<_> = results
|
|
||||||
.new_items
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| (self.next_id(), item))
|
|
||||||
.collect();
|
|
||||||
self.items.extend(new_items);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iter(&self) -> impl Iterator<Item = &StitchedItem<'id>> {
|
|
||||||
self.items.iter().map(|(_, item)| item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StitcherBackend for TestStitcherBackend<'_> {
|
|
||||||
type Key = u64;
|
|
||||||
|
|
||||||
fn find_matching_gaps<'a>(
|
|
||||||
&'a self,
|
|
||||||
events: impl Iterator<Item = &'a str>,
|
|
||||||
) -> impl Iterator<Item = (Self::Key, Gap)> {
|
|
||||||
// nobody cares about test suite performance right
|
|
||||||
let mut gaps = vec![];
|
|
||||||
|
|
||||||
for event in events {
|
|
||||||
for (key, item) in &self.items {
|
|
||||||
if let StitchedItem::Gap(gap) = item
|
|
||||||
&& gap.contains(event)
|
|
||||||
{
|
|
||||||
gaps.push((*key, gap.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gaps.into_iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_exists<'a>(&'a self, event: &'a str) -> bool {
|
|
||||||
self.items
|
|
||||||
.iter()
|
|
||||||
.any(|item| matches!(item.1, StitchedItem::Event(id) if event == id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_testcase(testcase: parser::TestCase<'_>) {
|
fn run_testcase(testcase: parser::TestCase<'_>) {
|
||||||
let mut backend = TestStitcherBackend::default();
|
let mut backend = MemoryStitcherBackend::default();
|
||||||
|
|
||||||
for (index, phase) in testcase.into_iter().enumerate() {
|
for (index, phase) in testcase.into_iter().enumerate() {
|
||||||
let stitcher = Stitcher::new(&backend);
|
let stitcher = Stitcher::new(&backend);
|
||||||
@@ -107,7 +18,7 @@ fn run_testcase(testcase: parser::TestCase<'_>) {
|
|||||||
for update in &updates.gap_updates {
|
for update in &updates.gap_updates {
|
||||||
println!("update to gap {}:", update.key);
|
println!("update to gap {}:", update.key);
|
||||||
println!(" new gap contents: {:?}", update.gap);
|
println!(" new gap contents: {:?}", update.gap);
|
||||||
println!(" new items: {:?}", update.inserted_items);
|
println!(" inserted items: {:?}", update.inserted_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("expected new items: {:?}", &phase.order.new_items);
|
println!("expected new items: {:?}", &phase.order.new_items);
|
||||||
@@ -134,9 +45,9 @@ fn run_testcase(testcase: parser::TestCase<'_>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.extend(updates);
|
backend.extend(updates);
|
||||||
println!("extended ordering: {:?}", backend.items);
|
println!("extended ordering: {:?}", backend);
|
||||||
|
|
||||||
for (expected, actual) in phase.order.iter().zip_eq(backend.iter()) {
|
for (expected, ref actual) in phase.order.iter().zip_eq(backend.iter()) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
expected, actual,
|
expected, actual,
|
||||||
"bad item in order, expected {expected:?} but got {actual:?}",
|
"bad item in order, expected {expected:?} but got {actual:?}",
|
||||||
|
|||||||
Reference in New Issue
Block a user