/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file tries to simulate "full syncs" - ie, from local state changing, to // fetching incoming items, generating items to upload, then updating the local // state (including the mirror) as a result.
usecrate::api::{clear, get, set}; usecrate::error::*; usecrate::schema::create_empty_sync_temp_tables; usecrate::sync::incoming::{apply_actions, get_incoming, plan_incoming, stage_incoming}; usecrate::sync::outgoing::{get_outgoing, record_uploaded, stage_outgoing}; usecrate::sync::test::new_syncable_mem_db; usecrate::sync::WebextRecord; use interrupt_support::NeverInterrupts; use rusqlite::{Connection, Row, Transaction}; use serde_json::json; use sql_support::ConnExt; use sync15::bso::{IncomingBso, IncomingContent, OutgoingBso}; use sync_guid::Guid;
// Here we try and simulate everything done by a "full sync", just minus the // engine. Returns the records we uploaded. fn do_sync(
tx: &Transaction<'_>,
incoming_payloads: &[IncomingContent<WebextRecord>],
) -> Result<Vec<OutgoingBso>> {
log::info!("test do_sync() starting"); // First we stage the incoming in the temp tables.
stage_incoming(tx, incoming_payloads, &NeverInterrupts)?; // Then we process them getting a Vec of (item, state), which we turn into // a Vec of (item, action) let actions = get_incoming(tx)?
.into_iter()
.map(|(item, state)| (item, plan_incoming(state)))
.collect();
log::debug!("do_sync applying {:?}", actions);
apply_actions(tx, actions, &NeverInterrupts)?; // So we've done incoming - do outgoing.
stage_outgoing(tx)?; let outgoing = get_outgoing(tx, &NeverInterrupts)?;
log::debug!("do_sync has outgoing {:?}", outgoing);
record_uploaded(
tx,
outgoing
.iter()
.map(|p| p.envelope.id.clone())
.collect::<Vec<Guid>>()
.as_slice(),
&NeverInterrupts,
)?;
create_empty_sync_temp_tables(tx)?;
log::info!("test do_sync() complete");
Ok(outgoing)
}
// Check *both* the mirror and local API have ended up with the specified data. fn check_finished_with(conn: &Connection, ext_id: &str, val: serde_json::Value) -> Result<()> { let local = get(conn, ext_id, serde_json::Value::Null)?;
assert_eq!(local, val); let guid = get_mirror_guid(conn, ext_id)?; let mirror = get_mirror_data(conn, &guid);
assert_eq!(mirror, DbData::Data(val.to_string())); // and there should be zero items with a change counter. let count = conn.query_row_and_then( "SELECT COUNT(*) FROM storage_sync_data WHERE sync_change_counter != 0;",
[],
|row| row.get::<_, u32>(0),
)?;
assert_eq!(count, 0);
Ok(())
}
fn get_mirror_guid(conn: &Connection, extid: &str) -> Result<Guid> { let guid = conn.query_row_and_then( "SELECT m.guid FROM storage_sync_mirror m WHERE m.ext_id = ?;",
[extid],
|row| row.get::<_, Guid>(0),
)?;
Ok(guid)
}
#[test] fn test_simple_outgoing_sync() -> Result<()> { // So we are starting with an empty local store and empty server store. let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"});
set(&tx, "ext-id", data.clone())?;
assert_eq!(do_sync(&tx, &[])?.len(), 1);
check_finished_with(&tx, "ext-id", data)?;
Ok(())
}
#[test] fn test_simple_incoming_sync() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"}); let bridge_record = make_incoming(&Guid::new("guid"), "ext-id", &data);
assert_eq!(do_sync(&tx, &[bridge_record])?.len(), 0); let key1_from_api = get(&tx, "ext-id", json!("key1"))?;
assert_eq!(key1_from_api, json!({"key1": "key1-value"}));
check_finished_with(&tx, "ext-id", data)?;
Ok(())
}
#[test] fn test_outgoing_tombstone() -> Result<()> { // Tombstones are only kept when the mirror has that record - so first // test that, then arrange for the mirror to have the record. let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"});
set(&tx, "ext-id", data.clone())?;
assert_eq!(
get_local_data(&tx, "ext-id"),
DbData::Data(data.to_string())
); // hasn't synced yet, so clearing shouldn't write a tombstone.
clear(&tx, "ext-id")?;
assert_eq!(get_local_data(&tx, "ext-id"), DbData::NoRow); // now set data again and sync and *then* remove.
set(&tx, "ext-id", data)?;
assert_eq!(do_sync(&tx, &[])?.len(), 1);
assert!(get_local_data(&tx, "ext-id").has_data()); let guid = get_mirror_guid(&tx, "ext-id")?;
assert!(get_mirror_data(&tx, &guid).has_data());
clear(&tx, "ext-id")?;
assert_eq!(get_local_data(&tx, "ext-id"), DbData::NullRow); // then after syncing, the tombstone will be in the mirror but the local row // has been removed.
assert_eq!(do_sync(&tx, &[])?.len(), 1);
assert_eq!(get_local_data(&tx, "ext-id"), DbData::NoRow);
assert_eq!(get_mirror_data(&tx, &guid), DbData::NullRow);
Ok(())
}
#[test] fn test_incoming_tombstone_exists() -> Result<()> { // An incoming tombstone for a record we've previously synced (and thus // have data for) let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"});
set(&tx, "ext-id", data.clone())?;
assert_eq!(
get_local_data(&tx, "ext-id"),
DbData::Data(data.to_string())
); // sync to get data in our mirror.
assert_eq!(do_sync(&tx, &[])?.len(), 1);
assert!(get_local_data(&tx, "ext-id").has_data()); let guid = get_mirror_guid(&tx, "ext-id")?;
assert!(get_mirror_data(&tx, &guid).has_data()); // Now an incoming tombstone for it. let tombstone = make_incoming_tombstone(&guid);
assert_eq!(
do_sync(&tx, &[tombstone])?.len(), 0, "expect no outgoing records"
);
assert_eq!(get_local_data(&tx, "ext-id"), DbData::NoRow);
assert_eq!(get_mirror_data(&tx, &guid), DbData::NullRow);
Ok(())
}
#[test] fn test_incoming_tombstone_not_exists() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; // An incoming tombstone for something that's not anywhere locally. let guid = Guid::new("guid"); let tombstone = make_incoming_tombstone(&guid);
assert_eq!(
do_sync(&tx, &[tombstone])?.len(), 0, "expect no outgoing records"
); // But we still keep the tombstone in the mirror.
assert_eq!(get_local_data(&tx, "ext-id"), DbData::NoRow);
assert_eq!(get_mirror_data(&tx, &guid), DbData::NullRow);
Ok(())
}
#[test] fn test_reconciled() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value"});
set(&tx, "ext-id", data)?; // Incoming payload with the same data let record = make_incoming(&Guid::new("guid"), "ext-id", &json!({"key1": "key1-value"})); // Should be no outgoing records as we reconciled.
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
check_finished_with(&tx, "ext-id", json!({"key1": "key1-value"}))?;
Ok(())
}
/// Tests that we handle things correctly if we get a payload that is /// identical to what is in the mirrored table. #[test] fn test_reconcile_with_null_payload() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value"});
set(&tx, "ext-id", data.clone())?; // We try to push this change on the next sync.
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?;
assert_eq!(get_mirror_data(&tx, &guid), DbData::Data(data.to_string())); // Incoming payload with the same data. // This could happen if, for example, another client changed the // key and then put it back the way it was. let record = make_incoming(&guid, "ext-id", &data); // Should be no outgoing records as we reconciled.
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
check_finished_with(&tx, "ext-id", data)?;
Ok(())
}
#[test] fn test_accept_incoming_when_local_is_deleted() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; // We only record an extension as deleted locally if it has been // uploaded before being deleted. let data = json!({"key1": "key1-value"});
set(&tx, "ext-id", data)?;
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?;
clear(&tx, "ext-id")?; // Incoming payload without 'key1'. Because we previously uploaded // key1, this means another client deleted it. let record = make_incoming(&guid, "ext-id", &json!({"key2": "key2-value"}));
// We completely accept the incoming record.
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
check_finished_with(&tx, "ext-id", json!({"key2": "key2-value"}))?;
Ok(())
}
#[test] fn test_accept_incoming_when_local_is_deleted_no_mirror() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value"});
set(&tx, "ext-id", data)?;
assert_eq!(do_sync(&tx, &[])?.len(), 1);
clear(&tx, "ext-id")?;
// Use a random guid so that we don't find the mirrored data. // This test is somewhat bad because deduping might obviate // the need for it. let record = make_incoming(&Guid::new("guid"), "ext-id", &json!({"key2": "key2-value"}));
// We completely accept the incoming record.
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
check_finished_with(&tx, "ext-id", json!({"key2": "key2-value"}))?;
Ok(())
}
#[test] fn test_accept_deleted_key_mirrored() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"});
set(&tx, "ext-id", data)?;
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?; // Incoming payload without 'key1'. Because we previously uploaded // key1, this means another client deleted it. let record = make_incoming(&guid, "ext-id", &json!({"key2": "key2-value"})); // We completely accept the incoming record.
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
check_finished_with(&tx, "ext-id", json!({"key2": "key2-value"}))?;
Ok(())
}
#[test] fn test_merged_no_mirror() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value"});
set(&tx, "ext-id", data)?; // Incoming payload without 'key1' and some data for 'key2'. // Because we never uploaded 'key1', we merge our local values // with the remote. let record = make_incoming(&Guid::new("guid"), "ext-id", &json!({"key2": "key2-value"}));
assert_eq!(do_sync(&tx, &[record])?.len(), 1);
check_finished_with(
&tx, "ext-id",
json!({"key1": "key1-value", "key2": "key2-value"}),
)?;
Ok(())
}
#[test] fn test_merged_incoming() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let old_data = json!({"key1": "key1-value", "key2": "key2-value", "doomed_key": "deletable"});
set(&tx, "ext-id", old_data)?;
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?; // We update 'key1' locally. let local_data = json!({"key1": "key1-new", "key2": "key2-value", "doomed_key": "deletable"});
set(&tx, "ext-id", local_data)?; // Incoming payload where another client set 'key2' and removed // the 'doomed_key'. // Because we never uploaded our data, we'll merge our // key1 in, but otherwise keep the server's changes. let record = make_incoming(
&guid, "ext-id",
&json!({"key1": "key1-value", "key2": "key2-incoming"}),
); // We should send our 'key1'
assert_eq!(do_sync(&tx, &[record])?.len(), 1);
check_finished_with(
&tx, "ext-id",
json!({"key2": "key2-incoming", "key1": "key1-new"}),
)?;
Ok(())
}
#[test] fn test_merged_with_null_payload() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let old_data = json!({"key1": "key1-value"});
set(&tx, "ext-id", old_data.clone())?; // Push this change remotely.
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?;
assert_eq!(
get_mirror_data(&tx, &guid),
DbData::Data(old_data.to_string())
); let local_data = json!({"key1": "key1-new", "key2": "key2-value"});
set(&tx, "ext-id", local_data.clone())?; // Incoming payload with the same old data. let record = make_incoming(&guid, "ext-id", &old_data); // Three-way-merge will not detect any change in key1, so we // should keep our entire new value.
assert_eq!(do_sync(&tx, &[record])?.len(), 1);
check_finished_with(&tx, "ext-id", local_data)?;
Ok(())
}
#[test] fn test_deleted_mirrored_object_accept() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"});
set(&tx, "ext-id", data)?;
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?; // Incoming payload with data deleted. // We synchronize this deletion by deleting the keys we think // were on the server. let record = make_incoming_tombstone(&guid);
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
assert_eq!(get_local_data(&tx, "ext-id"), DbData::NoRow);
assert_eq!(get_mirror_data(&tx, &guid), DbData::NullRow);
Ok(())
}
#[test] fn test_deleted_mirrored_object_merged() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?;
set(&tx, "ext-id", json!({"key1": "key1-value"}))?;
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?;
set(
&tx, "ext-id",
json!({"key1": "key1-new", "key2": "key2-value"}),
)?; // Incoming payload with data deleted. // We synchronize this deletion by deleting the keys we think // were on the server. let record = make_incoming_tombstone(&guid); // This overrides the change to 'key1', but we still upload 'key2'.
assert_eq!(do_sync(&tx, &[record])?.len(), 1);
check_finished_with(&tx, "ext-id", json!({"key2": "key2-value"}))?;
Ok(())
}
/// Like the above test, but with a mirrored tombstone. #[test] fn test_deleted_mirrored_tombstone_merged() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; // Sync some data so we can get the guid for this extension.
set(&tx, "ext-id", json!({"key1": "key1-value"}))?;
assert_eq!(do_sync(&tx, &[])?.len(), 1); let guid = get_mirror_guid(&tx, "ext-id")?; // Sync a delete for this data so we have a tombstone in the mirror. let record = make_incoming_tombstone(&guid);
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
assert_eq!(get_mirror_data(&tx, &guid), DbData::NullRow);
// Set some data and sync it simultaneously with another incoming delete.
set(&tx, "ext-id", json!({"key2": "key2-value"}))?; let record = make_incoming_tombstone(&guid); // We cannot delete any matching keys because there are no // matching keys. Instead we push our data.
assert_eq!(do_sync(&tx, &[record])?.len(), 1);
check_finished_with(&tx, "ext-id", json!({"key2": "key2-value"}))?;
Ok(())
}
#[test] fn test_deleted_not_mirrored_object_merged() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"});
set(&tx, "ext-id", data)?; // Incoming payload with data deleted. let record = make_incoming_tombstone(&Guid::new("guid")); // We normally delete the keys we think were on the server, but // here we have no information about what was on the server, so we // don't delete anything. We merge in all undeleted keys.
assert_eq!(do_sync(&tx, &[record])?.len(), 1);
check_finished_with(
&tx, "ext-id",
json!({"key1": "key1-value", "key2": "key2-value"}),
)?;
Ok(())
}
#[test] fn test_conflicting_incoming() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let data = json!({"key1": "key1-value", "key2": "key2-value"});
set(&tx, "ext-id", data)?; // Incoming payload without 'key1' and conflicting for 'key2'. // Because we never uploaded either of our keys, we'll merge our // key1 in, but the server key2 wins. let record = make_incoming(
&Guid::new("guid"), "ext-id",
&json!({"key2": "key2-incoming"}),
); // We should send our 'key1'
assert_eq!(do_sync(&tx, &[record])?.len(), 1);
check_finished_with(
&tx, "ext-id",
json!({"key1": "key1-value", "key2": "key2-incoming"}),
)?;
Ok(())
}
#[test] fn test_invalid_incoming() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection().expect("should retrieve connection"); let tx = conn.unchecked_transaction()?; let json = json!({"id": "id", "payload": json!("").to_string()}); let bso = serde_json::from_value::<IncomingBso>(json).unwrap(); let record = bso.into_content();
// Should do nothing.
assert_eq!(do_sync(&tx, &[record])?.len(), 0);
Ok(())
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-23)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.