test: fuzzing test init

This commit is contained in:
Zixuan Chen 2022-11-12 17:51:34 +08:00
parent 37b35cf42f
commit 4da32c7d0e
9 changed files with 430 additions and 17 deletions

View file

@ -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"

View file

@ -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

View 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) });

View file

@ -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.

View file

@ -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())
}

View file

@ -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");

View 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),
}],
)
}
}

View file

@ -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())
}
}
}

View file

@ -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)]