lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

mod.rs (25451B)


      1 pub mod encode;
      2 
      3 #[cfg(feature = "json")]
      4 pub mod decode;
      5 
      6 #[cfg(all(test, feature = "json"))]
      7 mod tests {
      8     use radroots_event::{
      9         envelope::kind::KIND_POST,
     10         farm::crdt::{
     11             CrdtBackend, FarmCrdtChange, FarmCrdtDocumentKind, FarmSemanticKind,
     12             KIND_FARM_CRDT_CHANGE, RADROOTS_FARM_CRDT_CHANGE_SCHEMA, RADROOTS_FARM_CRDT_TAG,
     13         },
     14         farm::workspace::{FarmWorkspaceRef, KIND_FARM_WORKSPACE_MANIFEST},
     15     };
     16 
     17     use crate::error::{EventEncodeError, EventParseError};
     18     use crate::farm_crdt::decode::{
     19         data_from_event, farm_crdt_change_from_event, farm_crdt_change_from_event_with_author,
     20         parsed_from_event,
     21     };
     22     use crate::farm_crdt::encode::{
     23         farm_crdt_change_build_tags, farm_crdt_change_build_tags_with_author, to_wire_parts,
     24         to_wire_parts_with_author, to_wire_parts_with_kind, to_wire_parts_with_kind_and_author,
     25     };
     26 
     27     const WORKSPACE_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
     28     const DOCUMENT_ID: &str = "AAAAAAAAAAAAAAAAAAAAAg";
     29     const GROUP_ID: &str = "field-group";
     30     const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
     31     const AUTHOR: &str = crate::test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX;
     32     const EVENT_SIG: &str = concat!(
     33         "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
     34         "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
     35     );
     36 
     37     #[test]
     38     fn farm_crdt_change_encodes_and_decodes_task_change() {
     39         let change = sample_change();
     40         let parts = to_wire_parts_with_author(&change, AUTHOR).expect("crdt wire parts");
     41 
     42         assert_eq!(parts.kind, KIND_FARM_CRDT_CHANGE);
     43         assert!(parts.tags.contains(&tag("h", GROUP_ID)));
     44         assert!(parts.tags.contains(&tag("d", DOCUMENT_ID)));
     45         assert!(
     46             parts
     47                 .tags
     48                 .contains(&tag("a", "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA"))
     49         );
     50         assert!(parts.tags.contains(&tag("p", AUTHOR)));
     51         assert!(parts.tags.contains(&tag("t", RADROOTS_FARM_CRDT_TAG)));
     52 
     53         let decoded = farm_crdt_change_from_event_with_author(
     54             parts.kind,
     55             &parts.tags,
     56             &parts.content,
     57             AUTHOR,
     58         )
     59         .expect("crdt decode");
     60         assert_eq!(decoded.schema, RADROOTS_FARM_CRDT_CHANGE_SCHEMA);
     61         assert_eq!(decoded.document_id, DOCUMENT_ID);
     62         assert_eq!(decoded.workspace.d_tag, WORKSPACE_D_TAG);
     63         assert_eq!(decoded.business_time_ms, 1_780_000_000_000);
     64     }
     65 
     66     #[test]
     67     fn farm_crdt_change_roundtrips_representative_mvp_semantics() {
     68         let cases = vec![
     69             (
     70                 FarmCrdtDocumentKind::FarmTask,
     71                 FarmSemanticKind::FarmTaskCreate,
     72             ),
     73             (
     74                 FarmCrdtDocumentKind::FarmTask,
     75                 FarmSemanticKind::FarmTaskStatusSet,
     76             ),
     77             (
     78                 FarmCrdtDocumentKind::FarmWorkSession,
     79                 FarmSemanticKind::FarmWorkSessionStart,
     80             ),
     81             (
     82                 FarmCrdtDocumentKind::FarmWorkSession,
     83                 FarmSemanticKind::FarmWorkSessionStop,
     84             ),
     85             (
     86                 FarmCrdtDocumentKind::FarmWorkSession,
     87                 FarmSemanticKind::FarmWorkSessionSubmit,
     88             ),
     89             (
     90                 FarmCrdtDocumentKind::FarmWorkSession,
     91                 FarmSemanticKind::FarmWorkSessionApprove,
     92             ),
     93             (
     94                 FarmCrdtDocumentKind::FarmWorkSession,
     95                 FarmSemanticKind::FarmWorkSessionReject,
     96             ),
     97             (
     98                 FarmCrdtDocumentKind::FarmWorkSession,
     99                 FarmSemanticKind::FarmWorkSessionCorrect,
    100             ),
    101             (
    102                 FarmCrdtDocumentKind::FarmHarvestRecord,
    103                 FarmSemanticKind::FarmHarvestLineAdd,
    104             ),
    105             (
    106                 FarmCrdtDocumentKind::FarmHarvestRecord,
    107                 FarmSemanticKind::FarmHarvestLineCorrect,
    108             ),
    109             (
    110                 FarmCrdtDocumentKind::FarmHarvestRecord,
    111                 FarmSemanticKind::FarmHarvestLineVoid,
    112             ),
    113             (
    114                 FarmCrdtDocumentKind::FarmMembership,
    115                 FarmSemanticKind::FarmMemberInviteCreate,
    116             ),
    117             (
    118                 FarmCrdtDocumentKind::FarmMembership,
    119                 FarmSemanticKind::FarmMemberApprove,
    120             ),
    121             (
    122                 FarmCrdtDocumentKind::FarmMembership,
    123                 FarmSemanticKind::FarmMemberRoleSet,
    124             ),
    125             (
    126                 FarmCrdtDocumentKind::FarmPayPeriod,
    127                 FarmSemanticKind::FarmPayPeriodClose,
    128             ),
    129             (
    130                 FarmCrdtDocumentKind::FarmPayPeriod,
    131                 FarmSemanticKind::FarmReportExportMark,
    132             ),
    133         ];
    134 
    135         for (index, (document_kind, semantic_kind)) in cases.into_iter().enumerate() {
    136             let document_id = document_id(index);
    137             let change = sample_change_with(document_id.as_str(), document_kind, semantic_kind);
    138             let parts = to_wire_parts_with_author(&change, AUTHOR).expect("crdt wire parts");
    139             let decoded = farm_crdt_change_from_event_with_author(
    140                 parts.kind,
    141                 &parts.tags,
    142                 &parts.content,
    143                 AUTHOR,
    144             )
    145             .expect("crdt decode");
    146 
    147             assert_eq!(decoded.document_id, document_id);
    148             assert_eq!(decoded.document_kind, change.document_kind);
    149             assert_eq!(decoded.semantic_kind, change.semantic_kind);
    150         }
    151     }
    152 
    153     #[test]
    154     fn farm_crdt_change_rejects_missing_t_and_d_mismatch() {
    155         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    156         let without_t = parts
    157             .tags
    158             .iter()
    159             .filter(|tag| tag.first().map(|value| value.as_str()) != Some("t"))
    160             .cloned()
    161             .collect::<Vec<_>>();
    162 
    163         let missing_t =
    164             farm_crdt_change_from_event(parts.kind, &without_t, &parts.content).unwrap_err();
    165         assert!(matches!(missing_t, EventParseError::MissingTag("t")));
    166 
    167         let mut mismatched_d = parts.tags.clone();
    168         for tag in mismatched_d.iter_mut() {
    169             if tag.first().map(|value| value.as_str()) == Some("d") {
    170                 tag[1] = WORKSPACE_D_TAG.to_string();
    171             }
    172         }
    173         let mismatch =
    174             farm_crdt_change_from_event(parts.kind, &mismatched_d, &parts.content).unwrap_err();
    175         assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
    176     }
    177 
    178     #[test]
    179     fn farm_crdt_change_rejects_bad_workspace_address_and_author() {
    180         let parts = to_wire_parts_with_author(&sample_change(), AUTHOR).expect("crdt wire parts");
    181         let mut bad_workspace = parts.tags.clone();
    182         for tag in bad_workspace.iter_mut() {
    183             if tag.first().map(|value| value.as_str()) == Some("a") {
    184                 tag[1] = format!("{KIND_FARM_WORKSPACE_MANIFEST}:workspace_pubkey:bad");
    185             }
    186         }
    187         let workspace_err =
    188             farm_crdt_change_from_event(parts.kind, &bad_workspace, &parts.content).unwrap_err();
    189         assert!(matches!(workspace_err, EventParseError::InvalidTag("a")));
    190 
    191         let author_err = farm_crdt_change_from_event_with_author(
    192             parts.kind,
    193             &parts.tags,
    194             &parts.content,
    195             "other_author",
    196         )
    197         .unwrap_err();
    198         assert!(matches!(author_err, EventParseError::InvalidTag("p")));
    199     }
    200 
    201     #[test]
    202     fn farm_crdt_change_rejects_bad_encoded_change_missing_h_and_kind() {
    203         let mut bad_change = sample_change();
    204         bad_change.encoded_change = "abc/def".to_string();
    205         let encode_err = farm_crdt_change_build_tags(&bad_change).unwrap_err();
    206         assert!(matches!(
    207             encode_err,
    208             EventEncodeError::InvalidField("encoded_change")
    209         ));
    210 
    211         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    212         let without_h = parts
    213             .tags
    214             .iter()
    215             .filter(|tag| tag.first().map(|value| value.as_str()) != Some("h"))
    216             .cloned()
    217             .collect::<Vec<_>>();
    218         let missing_h =
    219             farm_crdt_change_from_event(parts.kind, &without_h, &parts.content).unwrap_err();
    220         assert!(matches!(missing_h, EventParseError::MissingTag("h")));
    221 
    222         let wrong_kind = to_wire_parts_with_kind(&sample_change(), KIND_POST).unwrap_err();
    223         assert!(matches!(
    224             wrong_kind,
    225             EventEncodeError::InvalidKind(KIND_POST)
    226         ));
    227 
    228         let decode_wrong_kind =
    229             farm_crdt_change_from_event(KIND_POST, &parts.tags, &parts.content).unwrap_err();
    230         assert!(matches!(
    231             decode_wrong_kind,
    232             EventParseError::InvalidKind {
    233                 expected: "78",
    234                 got: KIND_POST
    235             }
    236         ));
    237     }
    238 
    239     #[test]
    240     fn farm_crdt_change_rejects_zero_business_time_and_schema_mismatch() {
    241         let mut zero_time = sample_change();
    242         zero_time.business_time_ms = 0;
    243         let zero_err = to_wire_parts(&zero_time).unwrap_err();
    244         assert!(matches!(
    245             zero_err,
    246             EventEncodeError::InvalidField("business_time_ms")
    247         ));
    248 
    249         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    250         let mut bad_schema = sample_change();
    251         bad_schema.schema = "radroots.farm.crdt.invalid".to_string();
    252         let content = serde_json::to_string(&bad_schema).expect("bad schema content");
    253         let schema_err =
    254             farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    255         assert!(matches!(schema_err, EventParseError::InvalidJson("schema")));
    256     }
    257 
    258     #[test]
    259     fn farm_crdt_change_wrappers_preserve_event_metadata() {
    260         let change = sample_change();
    261         let parts = to_wire_parts_with_author(&change, AUTHOR).expect("crdt wire parts");
    262 
    263         let data = data_from_event(
    264             EVENT_ID.to_string(),
    265             AUTHOR.to_string(),
    266             99,
    267             parts.kind,
    268             parts.content.clone(),
    269             parts.tags.clone(),
    270         )
    271         .expect("parsed data");
    272         assert_eq!(data.id, EVENT_ID);
    273         assert_eq!(data.author, AUTHOR);
    274         assert_eq!(data.published_at, 99);
    275         assert_eq!(data.kind, KIND_FARM_CRDT_CHANGE);
    276         assert_eq!(data.data, change);
    277 
    278         let parsed = parsed_from_event(
    279             EVENT_ID.to_string(),
    280             AUTHOR.to_string(),
    281             99,
    282             parts.kind,
    283             parts.content,
    284             parts.tags,
    285             EVENT_SIG.to_string(),
    286         )
    287         .expect("parsed event");
    288         assert_eq!(parsed.event.signature_hex(), EVENT_SIG);
    289         assert_eq!(parsed.data.data, change);
    290 
    291         let no_author_parts = to_wire_parts(&change).expect("crdt wire parts");
    292         let decoded = farm_crdt_change_from_event_with_author(
    293             no_author_parts.kind,
    294             &no_author_parts.tags,
    295             &no_author_parts.content,
    296             AUTHOR,
    297         )
    298         .expect("author context without p tag remains valid");
    299         assert_eq!(decoded, change);
    300 
    301         let empty_author = farm_crdt_change_from_event_with_author(
    302             no_author_parts.kind,
    303             &no_author_parts.tags,
    304             &no_author_parts.content,
    305             " ",
    306         )
    307         .unwrap_err();
    308         assert!(matches!(empty_author, EventParseError::InvalidTag("p")));
    309     }
    310 
    311     #[test]
    312     fn farm_crdt_change_rejects_decode_tag_and_content_edges() {
    313         let parts = to_wire_parts_with_author(&sample_change(), AUTHOR).expect("crdt wire parts");
    314 
    315         let empty_content = farm_crdt_change_from_event(parts.kind, &parts.tags, " ").unwrap_err();
    316         assert!(matches!(
    317             empty_content,
    318             EventParseError::InvalidJson("content")
    319         ));
    320 
    321         let bad_json = farm_crdt_change_from_event(parts.kind, &parts.tags, "{").unwrap_err();
    322         assert!(matches!(bad_json, EventParseError::InvalidJson("content")));
    323 
    324         let mut empty_author_tag = parts.tags.clone();
    325         replace_first_tag(&mut empty_author_tag, "p", tag("p", " "));
    326         let err = farm_crdt_change_from_event_with_author(
    327             parts.kind,
    328             &empty_author_tag,
    329             &parts.content,
    330             AUTHOR,
    331         )
    332         .unwrap_err();
    333         assert!(matches!(err, EventParseError::InvalidTag("p")));
    334 
    335         let mut bad_document_tag = parts.tags.clone();
    336         replace_first_tag(&mut bad_document_tag, "d", tag("d", "bad"));
    337         let err =
    338             farm_crdt_change_from_event(parts.kind, &bad_document_tag, &parts.content).unwrap_err();
    339         assert!(matches!(err, EventParseError::InvalidTag("d")));
    340 
    341         for replacement in [
    342             tag("a", "30023:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA"),
    343             tag("a", "30078::AAAAAAAAAAAAAAAAAAAAAA"),
    344             tag("a", "30078:workspace_pubkey:bad d"),
    345             tag("a", "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA:extra"),
    346         ] {
    347             let mut tags = parts.tags.clone();
    348             replace_first_tag(&mut tags, "a", replacement);
    349             let err = farm_crdt_change_from_event(parts.kind, &tags, &parts.content).unwrap_err();
    350             assert!(matches!(err, EventParseError::InvalidTag("a")));
    351         }
    352 
    353         let mut wrong_marker = parts.tags.clone();
    354         remove_tags(&mut wrong_marker, "t");
    355         wrong_marker.push(tag("t", "radroots:farm:other"));
    356         let err =
    357             farm_crdt_change_from_event(parts.kind, &wrong_marker, &parts.content).unwrap_err();
    358         assert!(matches!(err, EventParseError::MissingTag("t")));
    359 
    360         let mut group_mismatch = parts.tags.clone();
    361         replace_first_tag(&mut group_mismatch, "h", tag("h", "other-group"));
    362         let err =
    363             farm_crdt_change_from_event(parts.kind, &group_mismatch, &parts.content).unwrap_err();
    364         assert!(matches!(err, EventParseError::InvalidTag("h")));
    365 
    366         let mut pubkey_mismatch = sample_change();
    367         pubkey_mismatch.workspace.pubkey = "other_workspace_pubkey".to_string();
    368         let content = serde_json::to_string(&pubkey_mismatch).expect("crdt content");
    369         let err = farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    370         assert!(matches!(err, EventParseError::InvalidTag("a")));
    371 
    372         let mut d_tag_mismatch = sample_change();
    373         d_tag_mismatch.workspace.d_tag = DOCUMENT_ID.to_string();
    374         let content = serde_json::to_string(&d_tag_mismatch).expect("crdt content");
    375         let err = farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    376         assert!(matches!(err, EventParseError::InvalidTag("a")));
    377     }
    378 
    379     #[test]
    380     fn farm_crdt_change_rejects_content_validation_edges() {
    381         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    382 
    383         for (change, expected) in [
    384             {
    385                 let mut change = sample_change();
    386                 change.farm_group_id.clear();
    387                 (change, EventParseError::InvalidTag("h"))
    388             },
    389             {
    390                 let mut change = sample_change();
    391                 change.document_id = "bad".to_string();
    392                 (change, EventParseError::InvalidTag("d"))
    393             },
    394             {
    395                 let mut change = sample_change();
    396                 change.workspace.pubkey.clear();
    397                 (change, EventParseError::InvalidTag("a"))
    398             },
    399             {
    400                 let mut change = sample_change();
    401                 change.workspace.d_tag = "bad".to_string();
    402                 (change, EventParseError::InvalidTag("a"))
    403             },
    404             {
    405                 let mut change = sample_change();
    406                 change.encoded_change = "abc/def".to_string();
    407                 (change, EventParseError::InvalidJson("encoded_change"))
    408             },
    409             {
    410                 let mut change = sample_change();
    411                 change.change_hash.clear();
    412                 (change, EventParseError::InvalidJson("change_hash"))
    413             },
    414             {
    415                 let mut change = sample_change();
    416                 change.business_time_ms = 0;
    417                 (change, EventParseError::InvalidJson("business_time_ms"))
    418             },
    419             {
    420                 let mut change = sample_change();
    421                 change.actor_id.clear();
    422                 (change, EventParseError::InvalidJson("actor_id"))
    423             },
    424             {
    425                 let mut change = sample_change();
    426                 change.dependencies.push(String::new());
    427                 (change, EventParseError::InvalidJson("dependencies"))
    428             },
    429             {
    430                 let mut change = sample_change();
    431                 change.crdt_backend_version = Some(" ".to_string());
    432                 (change, EventParseError::InvalidJson("crdt_backend_version"))
    433             },
    434             {
    435                 let mut change = sample_change();
    436                 change.author_member_id = Some(" ".to_string());
    437                 (change, EventParseError::InvalidJson("author_member_id"))
    438             },
    439             {
    440                 let mut change = sample_change();
    441                 change.app_version = Some(" ".to_string());
    442                 (change, EventParseError::InvalidJson("app_version"))
    443             },
    444         ] {
    445             let content = serde_json::to_string(&change).expect("crdt content");
    446             let err = farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    447             assert_same_parse_error(err, expected);
    448         }
    449     }
    450 
    451     #[test]
    452     fn farm_crdt_change_rejects_encoder_validation_edges() {
    453         for (change, expected) in [
    454             {
    455                 let mut change = sample_change();
    456                 change.schema = "radroots.farm.crdt.invalid".to_string();
    457                 (change, EventEncodeError::InvalidField("schema"))
    458             },
    459             {
    460                 let mut change = sample_change();
    461                 change.farm_group_id.clear();
    462                 (
    463                     change,
    464                     EventEncodeError::EmptyRequiredField("farm_group_id"),
    465                 )
    466             },
    467             {
    468                 let mut change = sample_change();
    469                 change.document_id = "bad".to_string();
    470                 (change, EventEncodeError::InvalidField("document_id"))
    471             },
    472             {
    473                 let mut change = sample_change();
    474                 change.workspace.pubkey.clear();
    475                 (
    476                     change,
    477                     EventEncodeError::EmptyRequiredField("workspace.pubkey"),
    478                 )
    479             },
    480             {
    481                 let mut change = sample_change();
    482                 change.workspace.d_tag = "bad".to_string();
    483                 (change, EventEncodeError::InvalidField("workspace.d_tag"))
    484             },
    485             {
    486                 let mut change = sample_change();
    487                 change.actor_id.clear();
    488                 (change, EventEncodeError::EmptyRequiredField("actor_id"))
    489             },
    490             {
    491                 let mut change = sample_change();
    492                 change.change_hash.clear();
    493                 (change, EventEncodeError::EmptyRequiredField("change_hash"))
    494             },
    495             {
    496                 let mut change = sample_change();
    497                 change.dependencies.push(String::new());
    498                 (change, EventEncodeError::EmptyRequiredField("dependencies"))
    499             },
    500             {
    501                 let mut change = sample_change();
    502                 change.encoded_change = "abc/def".to_string();
    503                 (change, EventEncodeError::InvalidField("encoded_change"))
    504             },
    505             {
    506                 let mut change = sample_change();
    507                 change.business_time_ms = 0;
    508                 (change, EventEncodeError::InvalidField("business_time_ms"))
    509             },
    510             {
    511                 let mut change = sample_change();
    512                 change.crdt_backend_version = Some(" ".to_string());
    513                 (
    514                     change,
    515                     EventEncodeError::EmptyRequiredField("crdt_backend_version"),
    516                 )
    517             },
    518             {
    519                 let mut change = sample_change();
    520                 change.author_member_id = Some(" ".to_string());
    521                 (
    522                     change,
    523                     EventEncodeError::EmptyRequiredField("author_member_id"),
    524                 )
    525             },
    526             {
    527                 let mut change = sample_change();
    528                 change.app_version = Some(" ".to_string());
    529                 (change, EventEncodeError::EmptyRequiredField("app_version"))
    530             },
    531         ] {
    532             let err = farm_crdt_change_build_tags(&change).unwrap_err();
    533             assert_same_encode_error(err, expected);
    534         }
    535 
    536         let author_err =
    537             farm_crdt_change_build_tags_with_author(&sample_change(), Some(" ")).unwrap_err();
    538         assert_same_encode_error(
    539             author_err,
    540             EventEncodeError::EmptyRequiredField("author_pubkey"),
    541         );
    542 
    543         let wrong_kind =
    544             to_wire_parts_with_kind_and_author(&sample_change(), KIND_POST, Some(AUTHOR))
    545                 .unwrap_err();
    546         assert_same_encode_error(wrong_kind, EventEncodeError::InvalidKind(KIND_POST));
    547     }
    548 
    549     fn sample_change() -> FarmCrdtChange {
    550         sample_change_with(
    551             DOCUMENT_ID,
    552             FarmCrdtDocumentKind::FarmTask,
    553             FarmSemanticKind::FarmTaskCreate,
    554         )
    555     }
    556 
    557     fn sample_change_with(
    558         document_id: &str,
    559         document_kind: FarmCrdtDocumentKind,
    560         semantic_kind: FarmSemanticKind,
    561     ) -> FarmCrdtChange {
    562         FarmCrdtChange {
    563             schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(),
    564             workspace: FarmWorkspaceRef {
    565                 pubkey: "workspace_pubkey".to_string(),
    566                 d_tag: WORKSPACE_D_TAG.to_string(),
    567             },
    568             farm_group_id: GROUP_ID.to_string(),
    569             document_id: document_id.to_string(),
    570             document_kind,
    571             crdt_backend: CrdtBackend::Automerge,
    572             crdt_backend_version: Some("0.x".to_string()),
    573             actor_id: "actor_abc".to_string(),
    574             change_hash: "crdt_hash_abc".to_string(),
    575             dependencies: Vec::new(),
    576             encoded_change: "abc-DEF_012".to_string(),
    577             semantic_kind,
    578             business_time_ms: 1_780_000_000_000,
    579             author_member_id: Some("member_abc".to_string()),
    580             app_version: Some("0.1.0".to_string()),
    581         }
    582     }
    583 
    584     fn document_id(index: usize) -> String {
    585         format!("{index:02}AAAAAAAAAAAAAAAAAAAA")
    586     }
    587 
    588     fn tag(key: &str, value: &str) -> Vec<String> {
    589         vec![key.to_string(), value.to_string()]
    590     }
    591 
    592     fn remove_tags(tags: &mut Vec<Vec<String>>, name: &str) {
    593         tags.retain(|tag| tag.first().map(String::as_str) != Some(name));
    594     }
    595 
    596     fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<String>) {
    597         let tag = tags
    598             .iter_mut()
    599             .find(|tag| tag.first().map(String::as_str) == Some(name))
    600             .expect("tag");
    601         *tag = replacement;
    602     }
    603 
    604     #[cfg_attr(coverage_nightly, coverage(off))]
    605     fn assert_same_parse_error(actual: EventParseError, expected: EventParseError) {
    606         match (actual, expected) {
    607             (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected))
    608             | (EventParseError::InvalidTag(actual), EventParseError::InvalidTag(expected))
    609             | (EventParseError::InvalidJson(actual), EventParseError::InvalidJson(expected)) => {
    610                 assert_eq!(actual, expected);
    611             }
    612             (
    613                 EventParseError::InvalidKind {
    614                     expected: actual_expected,
    615                     got: actual_got,
    616                 },
    617                 EventParseError::InvalidKind { expected, got },
    618             ) => {
    619                 assert_eq!(actual_expected, expected);
    620                 assert_eq!(actual_got, got);
    621             }
    622             (
    623                 EventParseError::InvalidNumber(actual, _),
    624                 EventParseError::InvalidNumber(expected, _),
    625             ) => {
    626                 assert_eq!(actual, expected);
    627             }
    628             (actual, expected) => {
    629                 panic!("unexpected parse error {actual:?}, expected {expected:?}")
    630             }
    631         }
    632     }
    633 
    634     #[cfg_attr(coverage_nightly, coverage(off))]
    635     fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) {
    636         match (actual, expected) {
    637             (
    638                 EventEncodeError::EmptyRequiredField(actual),
    639                 EventEncodeError::EmptyRequiredField(expected),
    640             )
    641             | (EventEncodeError::InvalidField(actual), EventEncodeError::InvalidField(expected)) => {
    642                 assert_eq!(actual, expected);
    643             }
    644             (EventEncodeError::InvalidKind(actual), EventEncodeError::InvalidKind(expected)) => {
    645                 assert_eq!(actual, expected);
    646             }
    647             (EventEncodeError::Json, EventEncodeError::Json) => {}
    648             (actual, expected) => {
    649                 panic!("unexpected encode error {actual:?}, expected {expected:?}")
    650             }
    651         }
    652     }
    653 }