use std::collections::{HashMap, HashSet};
use crate::bso_record::EncryptedBso;
use crate::client::{SetupStorageClient, Sync15ClientResponse};
use crate::collection_keys::CollectionKeys;
use crate::error::{self, ErrorKind, ErrorResponse};
use crate::key_bundle::KeyBundle;
use crate::record_types::{MetaGlobalEngine, MetaGlobalRecord};
use crate::request::{InfoCollections, InfoConfiguration};
use crate::util::ServerTimestamp;
use interrupt_support::Interruptee;
use serde_derive::*;
use sync_guid::Guid;
use self::SetupState::*;
const STORAGE_VERSION: usize = 5;
const DEFAULT_ENGINES: &[(&str, usize)] = &[
("passwords", 1),
("clients", 1),
("addons", 1),
("addresses", 1),
("bookmarks", 2),
("creditcards", 1),
("forms", 1),
("history", 1),
("prefs", 2),
("tabs", 1),
];
const DEFAULT_DECLINED: &[&str] = &[];
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "schema_version")]
pub enum PersistedGlobalState {
V2 { declined: Option<Vec<String>> },
}
impl Default for PersistedGlobalState {
#[inline]
fn default() -> PersistedGlobalState {
PersistedGlobalState::V2 { declined: None }
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub(crate) struct EngineChangesNeeded {
pub local_resets: HashSet<String>,
pub remote_wipes: HashSet<String>,
}
#[derive(Debug, Default, Clone, PartialEq)]
struct RemoteEngineState {
info_collections: HashSet<String>,
declined: HashSet<String>,
}
#[derive(Debug, Default, Clone, PartialEq)]
struct EngineStateInput {
local_declined: HashSet<String>,
remote: Option<RemoteEngineState>,
user_changes: HashMap<String, bool>,
}
#[derive(Debug, Default, Clone, PartialEq)]
struct EngineStateOutput {
declined: HashSet<String>,
changes_needed: EngineChangesNeeded,
}
fn compute_engine_states(input: EngineStateInput) -> EngineStateOutput {
use crate::util::*;
log::debug!("compute_engine_states: input {:?}", input);
let (must_enable, must_disable) = partition_by_value(&input.user_changes);
let have_remote = input.remote.is_some();
let RemoteEngineState {
info_collections,
declined: remote_declined,
} = input.remote.clone().unwrap_or_default();
let both_declined_and_remote = set_intersection(&info_collections, &remote_declined);
if !both_declined_and_remote.is_empty() {
log::warn!(
"Remote state contains engines which are in both info/collections and meta/global's declined: {:?}",
both_declined_and_remote,
);
}
let most_recent_declined_list = if have_remote {
&remote_declined
} else {
&input.local_declined
};
let result_declined = set_difference(
&set_union(most_recent_declined_list, &must_disable),
&must_enable,
);
let output = EngineStateOutput {
changes_needed: EngineChangesNeeded {
local_resets: set_difference(&result_declined, &input.local_declined),
remote_wipes: set_intersection(&info_collections, &must_disable),
},
declined: result_declined,
};
log::debug!("compute_engine_states: output {:?}", output);
output
}
impl PersistedGlobalState {
fn set_declined(&mut self, new_declined: Vec<String>) {
match self {
Self::V2 { ref mut declined } => *declined = Some(new_declined),
}
}
pub(crate) fn get_declined(&self) -> &[String] {
match self {
Self::V2 { declined: Some(d) } => &d,
Self::V2 { declined: None } => &[],
}
}
}
#[derive(Debug, Clone)]
pub struct GlobalState {
pub config: InfoConfiguration,
pub collections: InfoCollections,
pub global: MetaGlobalRecord,
pub global_timestamp: ServerTimestamp,
pub keys: EncryptedBso,
}
fn new_global(pgs: &PersistedGlobalState) -> error::Result<MetaGlobalRecord> {
let sync_id = Guid::random();
let mut engines: HashMap<String, _> = HashMap::new();
for (name, version) in DEFAULT_ENGINES.iter() {
let sync_id = Guid::random();
engines.insert(
(*name).to_string(),
MetaGlobalEngine {
version: *version,
sync_id,
},
);
}
let declined = match pgs {
PersistedGlobalState::V2 { declined: Some(d) } => d.clone(),
_ => DEFAULT_DECLINED.iter().map(ToString::to_string).collect(),
};
Ok(MetaGlobalRecord {
sync_id,
storage_version: STORAGE_VERSION,
engines,
declined,
})
}
fn fixup_meta_global(global: &mut MetaGlobalRecord) -> bool {
let mut changed_any = false;
for &(name, version) in DEFAULT_ENGINES.iter() {
let had_engine = global.engines.contains_key(name);
let should_have_engine = !global.declined.iter().any(|c| c == name);
if had_engine != should_have_engine {
if should_have_engine {
log::debug!("SyncID for engine {:?} was missing", name);
global.engines.insert(
name.to_string(),
MetaGlobalEngine {
version,
sync_id: Guid::random(),
},
);
} else {
log::debug!("SyncID for engine {:?} was present, but shouldn't be", name);
global.engines.remove(name);
}
changed_any = true;
}
}
changed_any
}
pub struct SetupStateMachine<'a> {
client: &'a dyn SetupStorageClient,
root_key: &'a KeyBundle,
pgs: &'a mut PersistedGlobalState,
allowed_states: Vec<&'static str>,
sequence: Vec<&'static str>,
engine_updates: Option<&'a HashMap<String, bool>>,
interruptee: &'a dyn Interruptee,
pub(crate) changes_needed: Option<EngineChangesNeeded>,
}
impl<'a> SetupStateMachine<'a> {
pub fn for_full_sync(
client: &'a dyn SetupStorageClient,
root_key: &'a KeyBundle,
pgs: &'a mut PersistedGlobalState,
engine_updates: Option<&'a HashMap<String, bool>>,
interruptee: &'a dyn Interruptee,
) -> SetupStateMachine<'a> {
SetupStateMachine::with_allowed_states(
client,
root_key,
pgs,
interruptee,
engine_updates,
vec![
"Initial",
"InitialWithConfig",
"InitialWithInfo",
"InitialWithMetaGlobal",
"Ready",
"FreshStartRequired",
"WithPreviousState",
],
)
}
pub fn for_fast_sync(
client: &'a dyn SetupStorageClient,
root_key: &'a KeyBundle,
pgs: &'a mut PersistedGlobalState,
engine_updates: Option<&'a HashMap<String, bool>>,
interruptee: &'a dyn Interruptee,
) -> SetupStateMachine<'a> {
SetupStateMachine::with_allowed_states(
client,
root_key,
pgs,
interruptee,
engine_updates,
vec!["Ready", "WithPreviousState"],
)
}
pub fn for_readonly_sync(
client: &'a dyn SetupStorageClient,
root_key: &'a KeyBundle,
pgs: &'a mut PersistedGlobalState,
interruptee: &'a dyn Interruptee,
) -> SetupStateMachine<'a> {
SetupStateMachine::with_allowed_states(
client,
root_key,
pgs,
interruptee,
None,
vec![
"Initial",
"InitialWithConfig",
"InitialWithInfo",
"InitialWithMetaGlobal",
"Ready",
"WithPreviousState",
],
)
}
fn with_allowed_states(
client: &'a dyn SetupStorageClient,
root_key: &'a KeyBundle,
pgs: &'a mut PersistedGlobalState,
interruptee: &'a dyn Interruptee,
engine_updates: Option<&'a HashMap<String, bool>>,
allowed_states: Vec<&'static str>,
) -> SetupStateMachine<'a> {
SetupStateMachine {
client,
root_key,
pgs,
sequence: Vec::new(),
allowed_states,
engine_updates,
interruptee,
changes_needed: None,
}
}
fn advance(&mut self, from: SetupState) -> error::Result<SetupState> {
match from {
Initial => {
let config = match self.client.fetch_info_configuration()? {
Sync15ClientResponse::Success { record, .. } => record,
Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
InfoConfiguration::default()
}
other => return Err(other.create_storage_error().into()),
};
Ok(InitialWithConfig { config })
}
InitialWithConfig { config } => {
match self.client.fetch_info_collections()? {
Sync15ClientResponse::Success {
record: collections,
..
} => Ok(InitialWithInfo {
config,
collections,
}),
Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
Ok(FreshStartRequired { config })
}
other => Err(other.create_storage_error().into()),
}
}
InitialWithInfo {
config,
collections,
} => {
match self.client.fetch_meta_global()? {
Sync15ClientResponse::Success {
record: mut global,
last_modified: mut global_timestamp,
..
} => {
if global.storage_version > STORAGE_VERSION {
return Err(ErrorKind::ClientUpgradeRequired.into());
}
if global.storage_version < STORAGE_VERSION {
Ok(FreshStartRequired { config })
} else {
log::info!("Have info/collections and meta/global. Computing new engine states");
let initial_global_declined: HashSet<String> =
global.declined.iter().cloned().collect();
let result = compute_engine_states(EngineStateInput {
local_declined: self.pgs.get_declined().iter().cloned().collect(),
user_changes: self.engine_updates.cloned().unwrap_or_default(),
remote: Some(RemoteEngineState {
declined: initial_global_declined.clone(),
info_collections: collections.keys().cloned().collect(),
}),
});
self.pgs
.set_declined(result.declined.iter().cloned().collect());
let fixed_declined = if result.declined != initial_global_declined {
global.declined = result.declined.iter().cloned().collect();
log::info!(
"Uploading new declined {:?} to meta/global with timestamp {:?}",
global.declined,
global_timestamp,
);
true
} else {
false
};
let fixed_ids = if fixup_meta_global(&mut global) {
log::info!(
"Uploading corrected meta/global with timestamp {:?}",
global_timestamp,
);
true
} else {
false
};
if fixed_declined || fixed_ids {
global_timestamp =
self.client.put_meta_global(global_timestamp, &global)?;
log::debug!("new global_timestamp: {:?}", global_timestamp);
}
if self.changes_needed.is_some() {
log::warn!("Already have a set of changes needed, Overwriting...");
}
self.changes_needed = Some(result.changes_needed);
Ok(InitialWithMetaGlobal {
config,
collections,
global,
global_timestamp,
})
}
}
Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
Ok(FreshStartRequired { config })
}
other => Err(other.create_storage_error().into()),
}
}
InitialWithMetaGlobal {
config,
collections,
global,
global_timestamp,
} => {
match self.client.fetch_crypto_keys()? {
Sync15ClientResponse::Success {
record,
last_modified,
..
} => {
assert_eq!(last_modified, record.modified);
let state = GlobalState {
config,
collections,
global,
global_timestamp,
keys: record,
};
Ok(Ready { state })
}
Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
Ok(FreshStartRequired { config })
}
other => Err(other.create_storage_error().into()),
}
}
WithPreviousState { old_state } => match self.client.fetch_info_collections()? {
Sync15ClientResponse::Success {
record: collections,
..
} => Ok(
if self.engine_updates.is_none()
&& is_same_timestamp(old_state.global_timestamp, &collections, "meta")
&& is_same_timestamp(old_state.keys.modified, &collections, "crypto")
{
Ready {
state: GlobalState {
collections,
..old_state
},
}
} else {
InitialWithConfig {
config: old_state.config,
}
},
),
_ => Ok(InitialWithConfig {
config: old_state.config,
}),
},
Ready { state } => Ok(Ready { state }),
FreshStartRequired { config } => {
log::info!("Fresh start: wiping remote");
self.client.wipe_all_remote()?;
log::info!("Uploading meta/global");
let computed = compute_engine_states(EngineStateInput {
local_declined: self.pgs.get_declined().iter().cloned().collect(),
user_changes: self.engine_updates.cloned().unwrap_or_default(),
remote: None,
});
self.pgs
.set_declined(computed.declined.iter().cloned().collect());
self.changes_needed = Some(computed.changes_needed);
let new_global = new_global(self.pgs)?;
self.client
.put_meta_global(ServerTimestamp::default(), &new_global)?;
let new_keys = CollectionKeys::new_random()?.to_encrypted_bso(&self.root_key)?;
self.client
.put_crypto_keys(ServerTimestamp::default(), &new_keys)?;
Ok(InitialWithConfig { config })
}
}
}
pub fn run_to_ready(&mut self, state: Option<GlobalState>) -> error::Result<GlobalState> {
let mut s = match state {
Some(old_state) => WithPreviousState { old_state },
None => Initial,
};
loop {
self.interruptee.err_if_interrupted()?;
let label = &s.label();
log::trace!("global state: {:?}", label);
match s {
Ready { state } => {
self.sequence.push(label);
return Ok(state);
}
FreshStartRequired { .. } | WithPreviousState { .. } | Initial => {
if self.sequence.contains(&label) {
return Err(ErrorKind::SetupRace.into());
}
}
_ => {
if !self.allowed_states.contains(&label) {
return Err(ErrorKind::SetupRequired.into());
}
}
};
self.sequence.push(label);
s = self.advance(s)?;
}
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum SetupState {
Initial,
InitialWithConfig {
config: InfoConfiguration,
},
InitialWithInfo {
config: InfoConfiguration,
collections: InfoCollections,
},
InitialWithMetaGlobal {
config: InfoConfiguration,
collections: InfoCollections,
global: MetaGlobalRecord,
global_timestamp: ServerTimestamp,
},
WithPreviousState {
old_state: GlobalState,
},
Ready {
state: GlobalState,
},
FreshStartRequired {
config: InfoConfiguration,
},
}
impl SetupState {
fn label(&self) -> &'static str {
match self {
Initial { .. } => "Initial",
InitialWithConfig { .. } => "InitialWithConfig",
InitialWithInfo { .. } => "InitialWithInfo",
InitialWithMetaGlobal { .. } => "InitialWithMetaGlobal",
Ready { .. } => "Ready",
WithPreviousState { .. } => "WithPreviousState",
FreshStartRequired { .. } => "FreshStartRequired",
}
}
}
fn is_same_timestamp(local: ServerTimestamp, collections: &InfoCollections, key: &str) -> bool {
collections.get(key).map_or(false, |ts| local == *ts)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bso_record::{BsoRecord, EncryptedBso, EncryptedPayload, Payload};
use crate::record_types::CryptoKeysRecord;
use interrupt_support::NeverInterrupts;
struct InMemoryClient {
info_configuration: error::Result<Sync15ClientResponse<InfoConfiguration>>,
info_collections: error::Result<Sync15ClientResponse<InfoCollections>>,
meta_global: error::Result<Sync15ClientResponse<MetaGlobalRecord>>,
crypto_keys: error::Result<Sync15ClientResponse<BsoRecord<EncryptedPayload>>>,
}
impl SetupStorageClient for InMemoryClient {
fn fetch_info_configuration(
&self,
) -> error::Result<Sync15ClientResponse<InfoConfiguration>> {
match &self.info_configuration {
Ok(client_response) => Ok(client_response.clone()),
Err(_) => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
status: 500,
route: "test/path".into(),
})),
}
}
fn fetch_info_collections(&self) -> error::Result<Sync15ClientResponse<InfoCollections>> {
match &self.info_collections {
Ok(collections) => Ok(collections.clone()),
Err(_) => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
status: 500,
route: "test/path".into(),
})),
}
}
fn fetch_meta_global(&self) -> error::Result<Sync15ClientResponse<MetaGlobalRecord>> {
match &self.meta_global {
Ok(global) => Ok(global.clone()),
Err(_) => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
status: 500,
route: "test/path".into(),
})),
}
}
fn put_meta_global(
&self,
xius: ServerTimestamp,
global: &MetaGlobalRecord,
) -> error::Result<ServerTimestamp> {
assert!(DEFAULT_ENGINES
.iter()
.filter(|e| e.0 != "logins")
.all(|&(k, _v)| global.engines.contains_key(k)));
assert!(!global.engines.contains_key("logins"));
assert_eq!(global.declined, vec!["logins".to_string()]);
Ok(ServerTimestamp(xius.0 + 1))
}
fn fetch_crypto_keys(&self) -> error::Result<Sync15ClientResponse<EncryptedBso>> {
match &self.crypto_keys {
Ok(keys) => Ok(keys.clone()),
Err(_) => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
status: 500,
route: "test/path".into(),
})),
}
}
fn put_crypto_keys(
&self,
xius: ServerTimestamp,
_keys: &EncryptedBso,
) -> error::Result<()> {
assert_eq!(xius, ServerTimestamp(888_800));
Err(ErrorKind::StorageHttpError(ErrorResponse::ServerError {
status: 500,
route: "crypto/keys".to_string(),
})
.into())
}
fn wipe_all_remote(&self) -> error::Result<()> {
Ok(())
}
}
fn mocked_success_ts<T>(t: T, ts: i64) -> error::Result<Sync15ClientResponse<T>> {
Ok(Sync15ClientResponse::Success {
status: 200,
record: t,
last_modified: ServerTimestamp(ts),
route: "test/path".into(),
})
}
fn mocked_success<T>(t: T) -> error::Result<Sync15ClientResponse<T>> {
mocked_success_ts(t, 0)
}
impl CollectionKeys {
pub fn to_encrypted_bso_with_timestamp(
&self,
root_key: &KeyBundle,
modified: ServerTimestamp,
) -> error::Result<EncryptedBso> {
let record = CryptoKeysRecord {
id: "keys".into(),
collection: "crypto".into(),
default: self.default.to_b64_array(),
collections: self
.collections
.iter()
.map(|kv| (kv.0.clone(), kv.1.to_b64_array()))
.collect(),
};
let mut bso =
crate::CleartextBso::from_payload(Payload::from_record(record)?, "crypto");
bso.modified = modified;
Ok(bso.encrypt(root_key)?)
}
}
#[test]
fn test_state_machine_ready_from_empty() {
let root_key = KeyBundle::new_random().unwrap();
let keys = CollectionKeys {
timestamp: ServerTimestamp(123_400),
default: KeyBundle::new_random().unwrap(),
collections: HashMap::new(),
};
let mg = MetaGlobalRecord {
sync_id: "syncIDAAAAAA".into(),
storage_version: 5usize,
engines: vec![(
"bookmarks",
MetaGlobalEngine {
version: 1usize,
sync_id: "syncIDBBBBBB".into(),
},
)]
.into_iter()
.map(|(key, value)| (key.to_owned(), value))
.collect(),
declined: vec!["logins".to_string()],
};
let client = InMemoryClient {
info_configuration: mocked_success(InfoConfiguration::default()),
info_collections: mocked_success(InfoCollections::new(
vec![("meta", 123_456), ("crypto", 145_000)]
.into_iter()
.map(|(key, value)| (key.to_owned(), ServerTimestamp(value)))
.collect(),
)),
meta_global: mocked_success_ts(mg, 999_000),
crypto_keys: mocked_success_ts(
keys.to_encrypted_bso_with_timestamp(&root_key, ServerTimestamp(888_000))
.expect("should always work in this test"),
888_000,
),
};
let mut pgs = PersistedGlobalState::V2 { declined: None };
let mut state_machine =
SetupStateMachine::for_full_sync(&client, &root_key, &mut pgs, None, &NeverInterrupts);
assert!(
state_machine.run_to_ready(None).is_ok(),
"Should drive state machine to ready"
);
assert_eq!(
state_machine.sequence,
vec![
"Initial",
"InitialWithConfig",
"InitialWithInfo",
"InitialWithMetaGlobal",
"Ready",
],
"Should cycle through all states"
);
}
#[test]
fn test_from_previous_state_declined() {
let _ = env_logger::try_init();
let sm_seq_restarted = vec![
"WithPreviousState",
"InitialWithConfig",
"InitialWithInfo",
"InitialWithMetaGlobal",
"Ready",
];
let sm_seq_used_previous = vec!["WithPreviousState", "Ready"];
fn do_test(
client: &dyn SetupStorageClient,
root_key: &KeyBundle,
mut pgs: &mut PersistedGlobalState,
engine_updates: Option<&HashMap<String, bool>>,
old_state: GlobalState,
expected_states: &[&str],
) {
let mut state_machine = SetupStateMachine::for_full_sync(
client,
root_key,
&mut pgs,
engine_updates,
&NeverInterrupts,
);
assert!(
state_machine.run_to_ready(Some(old_state)).is_ok(),
"Should drive state machine to ready"
);
assert_eq!(state_machine.sequence, expected_states);
}
let ts_metaglobal = 123_456;
let ts_keys = 145_000;
let root_key = KeyBundle::new_random().unwrap();
let keys = CollectionKeys {
timestamp: ServerTimestamp(ts_keys + 1),
default: KeyBundle::new_random().unwrap(),
collections: HashMap::new(),
};
let mg = MetaGlobalRecord {
sync_id: "syncIDAAAAAA".into(),
storage_version: 5usize,
engines: vec![(
"bookmarks",
MetaGlobalEngine {
version: 1usize,
sync_id: "syncIDBBBBBB".into(),
},
)]
.into_iter()
.map(|(key, value)| (key.to_owned(), value))
.collect(),
declined: vec!["logins".to_string()],
};
let collections = InfoCollections::new(
vec![("meta", ts_metaglobal), ("crypto", ts_keys)]
.into_iter()
.map(|(key, value)| (key.to_owned(), ServerTimestamp(value)))
.collect(),
);
let client = InMemoryClient {
info_configuration: mocked_success(InfoConfiguration::default()),
info_collections: mocked_success(collections.clone()),
meta_global: mocked_success_ts(mg.clone(), ts_metaglobal),
crypto_keys: mocked_success_ts(
keys.to_encrypted_bso_with_timestamp(&root_key, ServerTimestamp(ts_keys))
.expect("should always work in this test"),
ts_keys,
),
};
{
let mut pgs = PersistedGlobalState::V2 { declined: None };
let old_state = GlobalState {
config: InfoConfiguration::default(),
collections: collections.clone(),
global: mg.clone(),
global_timestamp: ServerTimestamp(ts_metaglobal),
keys: keys
.to_encrypted_bso_with_timestamp(&root_key, ServerTimestamp(ts_keys))
.expect("should always work in this test"),
};
do_test(
&client,
&root_key,
&mut pgs,
None,
old_state,
&sm_seq_used_previous,
);
}
{
let mut pgs = PersistedGlobalState::V2 { declined: None };
let old_state = GlobalState {
config: InfoConfiguration::default(),
collections: collections.clone(),
global: mg.clone(),
global_timestamp: ServerTimestamp(999_999),
keys: keys
.to_encrypted_bso_with_timestamp(&root_key, ServerTimestamp(ts_keys))
.expect("should always work in this test"),
};
do_test(
&client,
&root_key,
&mut pgs,
None,
old_state,
&sm_seq_restarted,
);
}
{
let mut pgs = PersistedGlobalState::V2 { declined: None };
let old_state = GlobalState {
config: InfoConfiguration::default(),
collections: collections.clone(),
global: mg.clone(),
global_timestamp: ServerTimestamp(ts_metaglobal),
keys: keys
.to_encrypted_bso_with_timestamp(&root_key, ServerTimestamp(999_999))
.expect("should always work in this test"),
};
do_test(
&client,
&root_key,
&mut pgs,
None,
old_state,
&sm_seq_restarted,
);
}
{
let mut pgs = PersistedGlobalState::V2 { declined: None };
let old_state = GlobalState {
config: InfoConfiguration::default(),
collections,
global: mg,
global_timestamp: ServerTimestamp(ts_metaglobal),
keys: keys
.to_encrypted_bso_with_timestamp(&root_key, ServerTimestamp(ts_keys))
.expect("should always work in this test"),
};
let mut engine_updates = HashMap::<String, bool>::new();
engine_updates.insert("logins".to_string(), false);
do_test(
&client,
&root_key,
&mut pgs,
Some(&engine_updates),
old_state,
&sm_seq_restarted,
);
let declined = match pgs {
PersistedGlobalState::V2 { declined: d } => d,
};
assert_eq!(declined, Some(vec!["logins".to_string()]));
}
}
fn string_set(s: &[&str]) -> HashSet<String> {
s.iter().map(ToString::to_string).collect()
}
fn string_map<T: Clone>(s: &[(&str, T)]) -> HashMap<String, T> {
s.iter().map(|v| (v.0.to_string(), v.1.clone())).collect()
}
#[test]
fn test_engine_states() {
assert_eq!(
compute_engine_states(EngineStateInput {
local_declined: string_set(&["foo", "bar"]),
remote: None,
user_changes: Default::default(),
}),
EngineStateOutput {
declined: string_set(&["foo", "bar"]),
changes_needed: Default::default(),
}
);
assert_eq!(
compute_engine_states(EngineStateInput {
local_declined: string_set(&["foo", "bar"]),
remote: Some(RemoteEngineState {
declined: string_set(&["foo"]),
info_collections: string_set(&["bar"])
}),
user_changes: Default::default(),
}),
EngineStateOutput {
declined: string_set(&["foo"]),
changes_needed: Default::default(),
}
);
assert_eq!(
compute_engine_states(EngineStateInput {
local_declined: string_set(&["foo", "bar"]),
remote: Some(RemoteEngineState {
declined: string_set(&["foo", "bar", "quux"]),
info_collections: string_set(&[])
}),
user_changes: Default::default(),
}),
EngineStateOutput {
declined: string_set(&["foo", "bar", "quux"]),
changes_needed: EngineChangesNeeded {
local_resets: string_set(&["quux"]),
remote_wipes: string_set(&[]),
}
}
);
assert_eq!(
compute_engine_states(EngineStateInput {
local_declined: string_set(&["bar", "baz"]),
remote: Some(RemoteEngineState {
declined: string_set(&["bar", "baz",]),
info_collections: string_set(&["quux"])
}),
user_changes: string_map(&[("bar", true)]),
}),
EngineStateOutput {
declined: string_set(&["baz"]),
changes_needed: Default::default()
}
);
assert_eq!(
compute_engine_states(EngineStateInput {
local_declined: string_set(&["bar", "baz"]),
remote: Some(RemoteEngineState {
declined: string_set(&["bar", "baz"]),
info_collections: string_set(&["foo"])
}),
user_changes: string_map(&[("foo", false)]),
}),
EngineStateOutput {
declined: string_set(&["baz", "bar", "foo"]),
changes_needed: EngineChangesNeeded {
local_resets: string_set(&["foo"]),
remote_wipes: string_set(&["foo"]),
}
}
);
}
}