mirror of
https://github.com/loro-dev/loro.git
synced 2025-02-02 11:06:14 +00:00
test: fuzzing test init
This commit is contained in:
parent
37b35cf42f
commit
4da32c7d0e
9 changed files with 430 additions and 17 deletions
24
crates/loro-core/fuzz/Cargo.lock
generated
24
crates/loro-core/fuzz/Cargo.lock
generated
|
@ -200,6 +200,12 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.25"
|
||||
|
@ -278,6 +284,7 @@ dependencies = [
|
|||
"ring",
|
||||
"rle",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"string_cache",
|
||||
|
@ -608,6 +615,12 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
|
@ -634,6 +647,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.10"
|
||||
|
|
|
@ -47,3 +47,9 @@ name = "text"
|
|||
path = "fuzz_targets/text.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "recursive"
|
||||
path = "fuzz_targets/recursive.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
|
5
crates/loro-core/fuzz/fuzz_targets/recursive.rs
Normal file
5
crates/loro-core/fuzz/fuzz_targets/recursive.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use loro_core::fuzz::recursive::{test_multi_sites, Action};
|
||||
|
||||
fuzz_target!(|actions: Vec<Action>| { test_multi_sites(8, actions) });
|
|
@ -5,14 +5,16 @@ use serde::{Deserialize, Serialize};
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Slot {}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
|
||||
pub enum ContainerType {
|
||||
/// See [`crate::text::TextContent`]
|
||||
Text,
|
||||
Map,
|
||||
List,
|
||||
/// Users can define their own container types.
|
||||
Custom(u16),
|
||||
// TODO: Users can define their own container types.
|
||||
// Custom(u16),
|
||||
}
|
||||
|
||||
/// Container is a special kind of op content. Each container has its own CRDT implementation.
|
||||
|
|
|
@ -380,6 +380,7 @@ impl Text {
|
|||
self.with_container(|text| text.delete(ctx, pos, len))
|
||||
}
|
||||
|
||||
// TODO: can be len?
|
||||
pub fn text_len(&self) -> usize {
|
||||
self.with_container(|text| text.text_len())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use enum_as_inner::EnumAsInner;
|
||||
use tabled::{TableIteratorExt, Tabled};
|
||||
pub mod recursive;
|
||||
|
||||
use crate::{
|
||||
array_mut_ref, container::registry::ContainerWrapper, debug_log, id::ClientID, LoroCore,
|
||||
|
@ -186,10 +187,7 @@ impl Actionable for Vec<LoroCore> {
|
|||
let text = self[*site as usize].get_text("text");
|
||||
let value = text.get_value();
|
||||
let value = value.as_string().unwrap();
|
||||
*pos %= value.len() + 1;
|
||||
while !value.is_char_boundary(*pos) {
|
||||
*pos = (*pos + 1) % (value.len() + 1)
|
||||
}
|
||||
change_pos_to_char_boundary(pos, value);
|
||||
}
|
||||
Action::Del { pos, len, site } => {
|
||||
*site %= self.len() as u8;
|
||||
|
@ -202,15 +200,7 @@ impl Actionable for Vec<LoroCore> {
|
|||
|
||||
let text = text.get_value();
|
||||
let str = text.as_string().unwrap();
|
||||
*pos %= str.len() + 1;
|
||||
while !str.is_char_boundary(*pos) {
|
||||
*pos = (*pos + 1) % str.len();
|
||||
}
|
||||
|
||||
*len = (*len).min(str.len() - (*pos));
|
||||
while !str.is_char_boundary(*pos + *len) {
|
||||
*len += 1;
|
||||
}
|
||||
change_delete_to_char_boundary(pos, len, str);
|
||||
}
|
||||
Action::Sync { from, to } => {
|
||||
*from %= self.len() as u8;
|
||||
|
@ -221,6 +211,24 @@ impl Actionable for Vec<LoroCore> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn change_delete_to_char_boundary(pos: &mut usize, len: &mut usize, str: &str) {
|
||||
*pos %= str.len() + 1;
|
||||
while !str.is_char_boundary(*pos) {
|
||||
*pos = (*pos + 1) % str.len();
|
||||
}
|
||||
*len = (*len).min(str.len() - (*pos));
|
||||
while !str.is_char_boundary(*pos + *len) {
|
||||
*len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_pos_to_char_boundary(pos: &mut usize, value: &str) {
|
||||
*pos %= value.len() + 1;
|
||||
while !value.is_char_boundary(*pos) {
|
||||
*pos = (*pos + 1) % (value.len() + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_eq(site_a: &mut LoroCore, site_b: &mut LoroCore) {
|
||||
let a = site_a.get_text("text");
|
||||
let b = site_b.get_text("text");
|
||||
|
|
367
crates/loro-core/src/fuzz/recursive.rs
Normal file
367
crates/loro-core/src/fuzz/recursive.rs
Normal file
|
@ -0,0 +1,367 @@
|
|||
use arbitrary::Arbitrary;
|
||||
use enum_as_inner::EnumAsInner;
|
||||
use tabled::{TableIteratorExt, Tabled};
|
||||
|
||||
use crate::{
|
||||
array_mut_ref,
|
||||
container::{registry::ContainerWrapper, ContainerID},
|
||||
debug_log,
|
||||
id::ClientID,
|
||||
ContainerType, List, LoroCore, LoroValue, Map, Text,
|
||||
};
|
||||
|
||||
#[derive(Arbitrary, EnumAsInner, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum Action {
|
||||
Map {
|
||||
site: u8,
|
||||
container_idx: u8,
|
||||
key: u8,
|
||||
value: FuzzValue,
|
||||
},
|
||||
List {
|
||||
site: u8,
|
||||
container_idx: u8,
|
||||
key: u8,
|
||||
value: FuzzValue,
|
||||
},
|
||||
Text {
|
||||
site: u8,
|
||||
container_idx: u8,
|
||||
pos: u8,
|
||||
value: u32,
|
||||
is_del: bool,
|
||||
},
|
||||
Sync {
|
||||
from: u8,
|
||||
to: u8,
|
||||
},
|
||||
SyncAll,
|
||||
}
|
||||
|
||||
struct Actor {
|
||||
site: ClientID,
|
||||
loro: LoroCore,
|
||||
// TODO: use set and merge
|
||||
map_containers: Vec<Map>,
|
||||
// TODO: use set and merge
|
||||
list_containers: Vec<List>,
|
||||
// TODO: use set and merge
|
||||
text_containers: Vec<Text>,
|
||||
}
|
||||
|
||||
#[derive(Arbitrary, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FuzzValue {
|
||||
Null,
|
||||
I32(i32),
|
||||
Container(ContainerType),
|
||||
}
|
||||
|
||||
impl From<FuzzValue> for LoroValue {
|
||||
fn from(v: FuzzValue) -> Self {
|
||||
match v {
|
||||
FuzzValue::Null => LoroValue::Null,
|
||||
FuzzValue::I32(i) => LoroValue::I32(i),
|
||||
FuzzValue::Container(c) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tabled for Action {
|
||||
const LENGTH: usize = 4;
|
||||
|
||||
fn fields(&self) -> Vec<std::borrow::Cow<'_, str>> {
|
||||
match self {
|
||||
Action::Sync { from, to } => vec![
|
||||
"sync".into(),
|
||||
format!("{} to {}", from, to).into(),
|
||||
"".into(),
|
||||
"".into(),
|
||||
],
|
||||
Action::SyncAll => vec!["sync all".into(), "".into(), "".into(), "".into()],
|
||||
Action::Map {
|
||||
site,
|
||||
container_idx,
|
||||
key,
|
||||
value,
|
||||
} => vec![
|
||||
"map".into(),
|
||||
format!("site {} container {}", site, container_idx).into(),
|
||||
format!("key {}", key).into(),
|
||||
format!("value {:?}", value).into(),
|
||||
],
|
||||
Action::List {
|
||||
site,
|
||||
container_idx,
|
||||
key,
|
||||
value,
|
||||
} => vec![
|
||||
"list".into(),
|
||||
format!("site {} container {}", site, container_idx).into(),
|
||||
format!("key {}", key).into(),
|
||||
format!("value {:?}", value).into(),
|
||||
],
|
||||
Action::Text {
|
||||
site,
|
||||
container_idx,
|
||||
pos,
|
||||
value,
|
||||
is_del,
|
||||
} => vec![
|
||||
"text".into(),
|
||||
format!("site {} container {}", site, container_idx).into(),
|
||||
format!("pos {}", pos).into(),
|
||||
format!("{}{}", if *is_del { "D" } else { "" }, value).into(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn headers() -> Vec<std::borrow::Cow<'static, str>> {
|
||||
vec!["type".into(), "site".into(), "prop".into(), "value".into()]
|
||||
}
|
||||
}
|
||||
|
||||
trait Actionable {
|
||||
fn apply_action(&mut self, action: &Action);
|
||||
fn preprocess(&mut self, action: &mut Action);
|
||||
}
|
||||
|
||||
impl Actor {
|
||||
fn add_new_container(&mut self, new: ContainerID) {
|
||||
match new.container_type() {
|
||||
ContainerType::Text => self.text_containers.push(self.loro.get_text(new)),
|
||||
ContainerType::Map => self.map_containers.push(self.loro.get_map(new)),
|
||||
ContainerType::List => self.list_containers.push(self.loro.get_list(new)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actionable for Vec<Actor> {
|
||||
fn preprocess(&mut self, action: &mut Action) {
|
||||
let max_users = self.len() as u8;
|
||||
match action {
|
||||
Action::Sync { from, to } => {
|
||||
*from %= max_users;
|
||||
*to %= max_users;
|
||||
}
|
||||
Action::SyncAll => {}
|
||||
Action::Map {
|
||||
site,
|
||||
container_idx,
|
||||
..
|
||||
} => {
|
||||
*site %= max_users;
|
||||
*container_idx %= self[*site as usize].map_containers.len().max(1) as u8;
|
||||
}
|
||||
Action::List {
|
||||
site,
|
||||
container_idx,
|
||||
key,
|
||||
value,
|
||||
} => {
|
||||
*site %= max_users;
|
||||
*container_idx %= self[*site as usize].list_containers.len().max(1) as u8;
|
||||
if let Some(list) = self[*site as usize]
|
||||
.list_containers
|
||||
.get(*container_idx as usize)
|
||||
{
|
||||
*key %= list.values_len().max(1) as u8;
|
||||
} else {
|
||||
*value = FuzzValue::I32(1);
|
||||
*key = 0;
|
||||
}
|
||||
}
|
||||
Action::Text {
|
||||
site,
|
||||
container_idx,
|
||||
pos,
|
||||
value,
|
||||
is_del,
|
||||
} => {
|
||||
*site %= max_users;
|
||||
*container_idx %= self[*site as usize].text_containers.len().max(1) as u8;
|
||||
if let Some(text) = self[*site as usize]
|
||||
.text_containers
|
||||
.get(*container_idx as usize)
|
||||
{
|
||||
*pos %= text.text_len().max(1) as u8;
|
||||
if *is_del {
|
||||
*value &= 0x1f;
|
||||
*value = (*value).min(text.text_len() as u32 - (*pos) as u32);
|
||||
}
|
||||
} else {
|
||||
*is_del = false;
|
||||
*pos = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_action(&mut self, action: &Action) {
|
||||
match action {
|
||||
Action::Sync { from, to } => {
|
||||
let (a, b) = array_mut_ref!(self, [*from as usize, *to as usize]);
|
||||
let a = &mut a.loro;
|
||||
let b = &mut b.loro;
|
||||
a.import(b.export(a.vv()));
|
||||
b.import(a.export(b.vv()));
|
||||
}
|
||||
Action::SyncAll => {
|
||||
for i in 1..self.len() {
|
||||
let (a, b) = array_mut_ref!(self, [0, i]);
|
||||
a.loro.import(b.loro.export(a.loro.vv()));
|
||||
}
|
||||
|
||||
for i in 1..self.len() {
|
||||
let (a, b) = array_mut_ref!(self, [0, i]);
|
||||
b.loro.import(a.loro.export(b.loro.vv()));
|
||||
}
|
||||
}
|
||||
Action::Map {
|
||||
site,
|
||||
container_idx,
|
||||
key,
|
||||
value,
|
||||
} => {
|
||||
let actor = &mut self[*site as usize];
|
||||
let container = actor.map_containers.get_mut(*container_idx as usize);
|
||||
let container = if container.is_none() {
|
||||
let map = actor.loro.get_map("map");
|
||||
actor.map_containers.push(map);
|
||||
&mut actor.map_containers[0]
|
||||
} else {
|
||||
container.unwrap()
|
||||
};
|
||||
|
||||
match value {
|
||||
FuzzValue::Null => {
|
||||
container.delete(&actor.loro, &key.to_string());
|
||||
}
|
||||
FuzzValue::I32(i) => {
|
||||
container.insert(&actor.loro, &key.to_string(), *i);
|
||||
}
|
||||
FuzzValue::Container(c) => {
|
||||
let new = container.insert_obj(&actor.loro, &key.to_string(), c.clone());
|
||||
actor.add_new_container(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::List {
|
||||
site,
|
||||
container_idx,
|
||||
key,
|
||||
value,
|
||||
} => {
|
||||
let actor = &mut self[*site as usize];
|
||||
let container = actor.list_containers.get_mut(*container_idx as usize);
|
||||
let container = if container.is_none() {
|
||||
let list = actor.loro.get_list("list");
|
||||
actor.list_containers.push(list);
|
||||
&mut actor.list_containers[0]
|
||||
} else {
|
||||
container.unwrap()
|
||||
};
|
||||
|
||||
match value {
|
||||
FuzzValue::Null => {
|
||||
container.delete(&actor.loro, *key as usize, 1);
|
||||
}
|
||||
FuzzValue::I32(i) => {
|
||||
container.insert(&actor.loro, *key as usize, *i);
|
||||
}
|
||||
FuzzValue::Container(c) => {
|
||||
let new = container.insert_obj(&actor.loro, *key as usize, c.clone());
|
||||
actor.add_new_container(new)
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Text {
|
||||
site,
|
||||
container_idx,
|
||||
pos,
|
||||
value,
|
||||
is_del,
|
||||
} => {
|
||||
let actor = &mut self[*site as usize];
|
||||
let container = actor
|
||||
.text_containers
|
||||
.get_mut(*container_idx as usize)
|
||||
.unwrap();
|
||||
if *is_del {
|
||||
container.delete(&actor.loro, *pos as usize, *value as usize);
|
||||
} else {
|
||||
container.insert(&actor.loro, *pos as usize, &value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_eq(site_a: &mut LoroCore, site_b: &mut LoroCore) {
|
||||
let a = site_a.get_text("text");
|
||||
let b = site_b.get_text("text");
|
||||
let value_a = a.get_value();
|
||||
let value_b = b.get_value();
|
||||
assert_eq!(value_a.as_string().unwrap(), value_b.as_string().unwrap());
|
||||
}
|
||||
|
||||
fn check_synced(sites: &mut [Actor]) {
|
||||
for i in 0..sites.len() - 1 {
|
||||
for j in i + 1..sites.len() {
|
||||
debug_log!("-------------------------------");
|
||||
debug_log!("checking {} with {}", i, j);
|
||||
debug_log!("-------------------------------");
|
||||
|
||||
let (a, b) = array_mut_ref!(sites, [i, j]);
|
||||
let a = &mut a.loro;
|
||||
let b = &mut b.loro;
|
||||
a.import(b.export(a.vv()));
|
||||
b.import(a.export(b.vv()));
|
||||
check_eq(a, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_multi_sites(site_num: u8, mut actions: Vec<Action>) {
|
||||
let mut sites = Vec::new();
|
||||
for i in 0..site_num {
|
||||
sites.push(Actor {
|
||||
site: i as u64,
|
||||
loro: LoroCore::new(Default::default(), Some(i as u64)),
|
||||
map_containers: Default::default(),
|
||||
list_containers: Default::default(),
|
||||
text_containers: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut applied = Vec::new();
|
||||
for action in actions.iter_mut() {
|
||||
sites.preprocess(action);
|
||||
applied.push(action.clone());
|
||||
debug_log!("\n{}", (&applied).table());
|
||||
sites.apply_action(action);
|
||||
}
|
||||
|
||||
debug_log!("=================================");
|
||||
// println!("{}", actions.table());
|
||||
check_synced(&mut sites);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod failed_tests {
|
||||
use super::test_multi_sites;
|
||||
use super::Action::*;
|
||||
use super::FuzzValue::*;
|
||||
|
||||
#[test]
|
||||
fn case_0() {
|
||||
test_multi_sites(
|
||||
8,
|
||||
vec![List {
|
||||
site: 73,
|
||||
container_idx: 73,
|
||||
key: 73,
|
||||
value: I32(1229539657),
|
||||
}],
|
||||
)
|
||||
}
|
||||
}
|
|
@ -443,7 +443,7 @@ fn check_import_change_valid(change: &Change<RemoteOp>) {
|
|||
.and_then(|x| x.as_list())
|
||||
.and_then(|x| x.as_insert())
|
||||
{
|
||||
assert!(slice.as_raw_str().is_some())
|
||||
assert!(slice.as_raw_str().is_some() || slice.as_raw_data().is_some())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use enum_as_inner::EnumAsInner;
|
|||
use fxhash::FxHashMap;
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
|
||||
use crate::{container::ContainerID, context::Context, Container, id::ClientID};
|
||||
use crate::{container::ContainerID, context::Context, Container};
|
||||
|
||||
/// [LoroValue] is used to represents the state of CRDT at a given version
|
||||
#[derive(Debug, PartialEq, Clone, EnumAsInner)]
|
||||
|
|
Loading…
Reference in a new issue