Skip to main content

gen_models/
manifest.rs

1use gen_core::{HashId, traits::Capnp};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    annotations::{AnnotationFile, AnnotationFileInfo},
6    db::OperationsConnection,
7    gen_models_capnp::{
8        manifest, manifest_annotation_file_addition, manifest_diff, manifest_operation,
9    },
10    operations::{FileAddition, Operation, OperationSummary},
11    traits::Query,
12};
13
14#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
15pub struct ManifestAnnotationFileAddition {
16    pub file_addition: FileAddition,
17    pub index_file_addition: Option<FileAddition>,
18    pub name: Option<String>,
19}
20
21impl<'a> Capnp<'a> for ManifestAnnotationFileAddition {
22    type Builder = manifest_annotation_file_addition::Builder<'a>;
23    type Reader = manifest_annotation_file_addition::Reader<'a>;
24
25    fn write_capnp(&self, builder: &mut Self::Builder) {
26        let mut file_addition_builder = builder.reborrow().init_file_addition();
27        self.file_addition.write_capnp(&mut file_addition_builder);
28        match &self.name {
29            Some(name) => builder.reborrow().get_name().set_some(name),
30            None => builder.reborrow().get_name().set_none(()),
31        }
32        match &self.index_file_addition {
33            Some(index_file_addition) => {
34                let mut index_file_builder =
35                    builder.reborrow().get_index_file_addition().init_some();
36                index_file_addition.write_capnp(&mut index_file_builder);
37            }
38            None => builder.reborrow().get_index_file_addition().set_none(()),
39        }
40    }
41
42    fn read_capnp(reader: Self::Reader) -> Self {
43        let file_addition = FileAddition::read_capnp(reader.get_file_addition().unwrap());
44        let name = match reader.get_name().which().unwrap() {
45            manifest_annotation_file_addition::name::None(()) => None,
46            manifest_annotation_file_addition::name::Some(name_reader) => {
47                Some(name_reader.unwrap().to_string().unwrap())
48            }
49        };
50        let index_file_addition = match reader.get_index_file_addition().which().unwrap() {
51            manifest_annotation_file_addition::index_file_addition::None(()) => None,
52            manifest_annotation_file_addition::index_file_addition::Some(file_reader) => {
53                Some(FileAddition::read_capnp(file_reader.unwrap()))
54            }
55        };
56        ManifestAnnotationFileAddition {
57            file_addition,
58            index_file_addition,
59            name,
60        }
61    }
62}
63
64#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
65pub struct ManifestOperation {
66    pub operation: Operation,
67    pub file_additions: Vec<FileAddition>,
68    pub annotation_file_additions: Vec<ManifestAnnotationFileAddition>,
69    pub operation_summary: Option<OperationSummary>,
70}
71
72impl<'a> Capnp<'a> for ManifestOperation {
73    type Builder = manifest_operation::Builder<'a>;
74    type Reader = manifest_operation::Reader<'a>;
75
76    fn write_capnp(&self, builder: &mut Self::Builder) {
77        let mut operation_builder = builder.reborrow().init_operation();
78        self.operation.write_capnp(&mut operation_builder);
79
80        let mut file_additions_builder = builder
81            .reborrow()
82            .init_file_additions(self.file_additions.len() as u32);
83        for (i, file_addition) in self.file_additions.iter().enumerate() {
84            let mut file_addition_builder = file_additions_builder.reborrow().get(i as u32);
85            file_addition.write_capnp(&mut file_addition_builder);
86        }
87
88        let mut annotation_file_additions_builder = builder
89            .reborrow()
90            .init_annotation_file_additions(self.annotation_file_additions.len() as u32);
91        for (i, file_addition) in self.annotation_file_additions.iter().enumerate() {
92            let mut file_addition_builder =
93                annotation_file_additions_builder.reborrow().get(i as u32);
94            file_addition
95                .file_addition
96                .write_capnp(&mut file_addition_builder);
97        }
98
99        let mut annotation_file_details_builder = builder
100            .reborrow()
101            .init_annotation_file_details(self.annotation_file_additions.len() as u32);
102        for (i, file_addition) in self.annotation_file_additions.iter().enumerate() {
103            let mut detail_builder = annotation_file_details_builder.reborrow().get(i as u32);
104            file_addition.write_capnp(&mut detail_builder);
105        }
106
107        match &self.operation_summary {
108            None => {
109                builder.reborrow().get_operation_summary().set_none(());
110            }
111            Some(summary) => {
112                let mut summary_builder = builder.reborrow().get_operation_summary().init_some();
113                summary.write_capnp(&mut summary_builder);
114            }
115        }
116    }
117
118    fn read_capnp(reader: Self::Reader) -> Self {
119        let operation = Operation::read_capnp(reader.get_operation().unwrap());
120        let file_additions_reader = reader.get_file_additions().unwrap();
121        let mut file_additions = Vec::new();
122        for file_addition_reader in file_additions_reader.iter() {
123            file_additions.push(FileAddition::read_capnp(file_addition_reader));
124        }
125
126        let annotation_file_additions = if reader.has_annotation_file_details() {
127            let annotation_file_details_reader = reader.get_annotation_file_details().unwrap();
128            annotation_file_details_reader
129                .iter()
130                .map(ManifestAnnotationFileAddition::read_capnp)
131                .collect()
132        } else {
133            let annotation_file_additions_reader = reader.get_annotation_file_additions().unwrap();
134            annotation_file_additions_reader
135                .iter()
136                .map(|file_addition_reader| ManifestAnnotationFileAddition {
137                    file_addition: FileAddition::read_capnp(file_addition_reader),
138                    index_file_addition: None,
139                    name: None,
140                })
141                .collect()
142        };
143
144        let operation_summary = match reader.get_operation_summary().which().unwrap() {
145            manifest_operation::operation_summary::None(()) => None,
146            manifest_operation::operation_summary::Some(summary_reader) => {
147                Some(OperationSummary::read_capnp(summary_reader.unwrap()))
148            }
149        };
150
151        ManifestOperation {
152            operation,
153            file_additions,
154            annotation_file_additions,
155            operation_summary,
156        }
157    }
158}
159
160#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
161pub struct Manifest {
162    pub manifest_version: String,
163    pub branch_name: String,
164    pub end_hash: Option<HashId>,
165    pub operations: Vec<ManifestOperation>,
166}
167
168impl<'a> Capnp<'a> for Manifest {
169    type Builder = manifest::Builder<'a>;
170    type Reader = manifest::Reader<'a>;
171
172    fn write_capnp(&self, builder: &mut Self::Builder) {
173        builder.set_manifest_version(&self.manifest_version);
174        builder.set_branch_name(&self.branch_name);
175        let mut end_hash_builder = builder.reborrow().get_end_hash();
176        match &self.end_hash {
177            Some(hash) => end_hash_builder.set_some(&hash.0).unwrap(),
178            None => end_hash_builder.set_none(()),
179        }
180
181        let mut operations_builder = builder
182            .reborrow()
183            .init_operations(self.operations.len() as u32);
184        for (i, operation) in self.operations.iter().enumerate() {
185            let mut operation_builder = operations_builder.reborrow().get(i as u32);
186            operation.write_capnp(&mut operation_builder);
187        }
188    }
189
190    fn read_capnp(reader: Self::Reader) -> Self {
191        let manifest_version = reader.get_manifest_version().unwrap().to_string().unwrap();
192        let branch_name = reader.get_branch_name().unwrap().to_string().unwrap();
193
194        let operations_reader = reader.get_operations().unwrap();
195        let mut operations = Vec::new();
196        for operation_reader in operations_reader.iter() {
197            operations.push(ManifestOperation::read_capnp(operation_reader));
198        }
199
200        let end_hash = match reader.get_end_hash().which().unwrap() {
201            manifest::end_hash::None(()) => None,
202            manifest::end_hash::Some(hash_reader) => {
203                let hash_reader = hash_reader.unwrap();
204                let slice = hash_reader.as_slice().unwrap();
205                Some(slice.try_into().unwrap())
206            }
207        };
208
209        Manifest {
210            manifest_version,
211            branch_name,
212            end_hash,
213            operations,
214        }
215    }
216}
217
218#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
219pub struct ManifestDiff {
220    pub missing_in_manifest2: Vec<ManifestOperation>,
221    pub missing_in_manifest1: Vec<ManifestOperation>,
222}
223
224impl<'a> Capnp<'a> for ManifestDiff {
225    type Builder = manifest_diff::Builder<'a>;
226    type Reader = manifest_diff::Reader<'a>;
227
228    fn write_capnp(&self, builder: &mut Self::Builder) {
229        let mut missing_in_manifest2_builder = builder
230            .reborrow()
231            .init_missing_in_manifest2(self.missing_in_manifest2.len() as u32);
232        for (i, operation) in self.missing_in_manifest2.iter().enumerate() {
233            let mut operation_builder = missing_in_manifest2_builder.reborrow().get(i as u32);
234            operation.write_capnp(&mut operation_builder);
235        }
236
237        let mut missing_in_manifest1_builder = builder
238            .reborrow()
239            .init_missing_in_manifest1(self.missing_in_manifest1.len() as u32);
240        for (i, operation) in self.missing_in_manifest1.iter().enumerate() {
241            let mut operation_builder = missing_in_manifest1_builder.reborrow().get(i as u32);
242            operation.write_capnp(&mut operation_builder);
243        }
244    }
245
246    fn read_capnp(reader: Self::Reader) -> Self {
247        let missing_in_manifest2_reader = reader.get_missing_in_manifest2().unwrap();
248        let mut missing_in_manifest2 = Vec::new();
249        for operation_reader in missing_in_manifest2_reader.iter() {
250            missing_in_manifest2.push(ManifestOperation::read_capnp(operation_reader));
251        }
252
253        let missing_in_manifest1_reader = reader.get_missing_in_manifest1().unwrap();
254        let mut missing_in_manifest1 = Vec::new();
255        for operation_reader in missing_in_manifest1_reader.iter() {
256            missing_in_manifest1.push(ManifestOperation::read_capnp(operation_reader));
257        }
258
259        ManifestDiff {
260            missing_in_manifest2,
261            missing_in_manifest1,
262        }
263    }
264}
265
266pub struct ManifestGenerator<'a> {
267    conn: &'a OperationsConnection,
268}
269
270impl<'a> ManifestGenerator<'a> {
271    pub fn new(conn: &'a OperationsConnection) -> Self {
272        Self { conn }
273    }
274
275    pub fn generate_manifest(
276        &self,
277        branch_name: &str,
278        end_hash: Option<&HashId>,
279    ) -> Result<Manifest, ManifestError> {
280        let mut manifest_operations = vec![];
281
282        if let Some(target_hash) = end_hash {
283            let hashes = Operation::get_upstream(self.conn, target_hash);
284            let mut operations_map = std::collections::HashMap::new();
285            for op in Operation::query_by_ids(self.conn, &hashes) {
286                operations_map.insert(op.hash, op.clone());
287            }
288
289            for hash in hashes.iter() {
290                if let Some(op) = operations_map.get(hash) {
291                    let file_additions = FileAddition::get_files_for_operation(self.conn, &op.hash);
292                    let annotation_file_additions =
293                        AnnotationFile::get_files_for_operation(self.conn, &op.hash)
294                            .into_iter()
295                            .map(|entry: AnnotationFileInfo| ManifestAnnotationFileAddition {
296                                file_addition: entry.file_addition,
297                                index_file_addition: entry.index_file_addition,
298                                name: entry.name,
299                            })
300                            .collect();
301                    let operation_summary = OperationSummary::query(
302                        self.conn,
303                        "select * from operation_summaries where operation_hash = ?1",
304                        rusqlite::params![op.hash],
305                    )
306                    .into_iter()
307                    .next();
308
309                    manifest_operations.push(ManifestOperation {
310                        operation: op.clone(),
311                        file_additions,
312                        annotation_file_additions,
313                        operation_summary,
314                    });
315                }
316            }
317        }
318
319        Ok(Manifest {
320            manifest_version: "1.0".to_string(),
321            branch_name: branch_name.to_string(),
322            end_hash: end_hash.copied(),
323            operations: manifest_operations,
324        })
325    }
326}
327
328#[derive(Debug, thiserror::Error)]
329pub enum ManifestError {
330    #[error("Database error: {0}")]
331    DatabaseError(#[from] rusqlite::Error),
332    #[error("Serialization error: {0}")]
333    SerializationError(#[from] serde_json::Error),
334    #[error("Operation not found")]
335    OperationNotFound(String),
336    #[error("Branch not found")]
337    BranchNotFound,
338}
339
340pub struct ManifestComparer;
341
342impl ManifestComparer {
343    pub fn diff_manifests(
344        manifest1: &Manifest,
345        manifest2: &Manifest,
346    ) -> Result<ManifestDiff, ManifestDiffError> {
347        if manifest1.manifest_version != manifest2.manifest_version {
348            return Err(ManifestDiffError::IncompatibleVersions);
349        }
350
351        let ops1_hashes: std::collections::HashSet<_> = manifest1
352            .operations
353            .iter()
354            .map(|op| &op.operation.hash)
355            .collect();
356        let ops2_hashes: std::collections::HashSet<_> = manifest2
357            .operations
358            .iter()
359            .map(|op| &op.operation.hash)
360            .collect();
361
362        let missing_in_manifest2: Vec<ManifestOperation> = manifest1
363            .operations
364            .iter()
365            .filter(|op| !ops2_hashes.contains(&op.operation.hash))
366            .cloned()
367            .collect();
368
369        let missing_in_manifest1: Vec<ManifestOperation> = manifest2
370            .operations
371            .iter()
372            .filter(|op| !ops1_hashes.contains(&op.operation.hash))
373            .cloned()
374            .collect();
375
376        Ok(ManifestDiff {
377            missing_in_manifest2,
378            missing_in_manifest1,
379        })
380    }
381}
382
383#[derive(Debug, thiserror::Error)]
384pub enum ManifestDiffError {
385    #[error("Incompatible manifest versions")]
386    IncompatibleVersions,
387}
388
389#[cfg(test)]
390mod tests {
391    use std::fs;
392
393    use capnp::message::TypedBuilder;
394
395    use super::*;
396    use crate::{
397        annotations::{AnnotationFile, AnnotationFileAdditionInput},
398        file_types::FileTypes,
399        operations::OperationInfo,
400        session_operations::{end_operation, start_operation},
401        test_helpers::setup_gen,
402    };
403
404    #[test]
405    fn test_manifest_operation_capnp_serialization() {
406        let context = setup_gen();
407        let conn = context.graph().conn();
408        let op_conn = context.operations().conn();
409
410        let db_uuid = crate::metadata::get_db_uuid(conn);
411        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
412
413        let mut session = start_operation(conn);
414        crate::sequence::Sequence::new()
415            .sequence("ACGT")
416            .sequence_type("DNA")
417            .save(conn);
418        let op_info = OperationInfo {
419            files: vec![],
420            description: "test op".to_string(),
421        };
422        let operation = end_operation(&context, &mut session, &op_info, "test", None).unwrap();
423
424        let manifest_operation = ManifestOperation {
425            operation: operation.clone(),
426            file_additions: vec![FileAddition {
427                id: HashId([1u8; 32]),
428                file_path: "/path/to/file.fa".to_string(),
429                file_type: FileTypes::Fasta,
430                checksum: HashId([2u8; 32]),
431            }],
432            annotation_file_additions: vec![ManifestAnnotationFileAddition {
433                file_addition: FileAddition {
434                    id: HashId([3u8; 32]),
435                    file_path: "/path/to/annotation.gff3".to_string(),
436                    file_type: FileTypes::Gff3,
437                    checksum: HashId([4u8; 32]),
438                },
439                index_file_addition: None,
440                name: Some("track-a".to_string()),
441            }],
442            operation_summary: Some(OperationSummary {
443                id: 1,
444                operation_hash: operation.hash,
445                summary: "Test operation summary".to_string(),
446            }),
447        };
448
449        let mut message = TypedBuilder::<manifest_operation::Owned>::new_default();
450        let mut root = message.init_root();
451        manifest_operation.write_capnp(&mut root);
452
453        let deserialized = ManifestOperation::read_capnp(root.into_reader());
454        assert_eq!(manifest_operation, deserialized);
455    }
456
457    #[test]
458    fn test_manifest_capnp_serialization() {
459        let context = setup_gen();
460        let conn = context.graph().conn();
461        let op_conn = context.operations().conn();
462
463        let db_uuid = crate::metadata::get_db_uuid(conn);
464        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
465
466        let mut session = start_operation(conn);
467        crate::sequence::Sequence::new()
468            .sequence("ACGT")
469            .sequence_type("DNA")
470            .save(conn);
471        let op_info = OperationInfo {
472            files: vec![],
473            description: "test op".to_string(),
474        };
475        let operation = end_operation(&context, &mut session, &op_info, "test", None).unwrap();
476
477        let manifest = Manifest {
478            manifest_version: "1.0".to_string(),
479            branch_name: "main".to_string(),
480            end_hash: Some(operation.hash),
481            operations: vec![ManifestOperation {
482                operation,
483                file_additions: vec![],
484                annotation_file_additions: vec![],
485                operation_summary: None,
486            }],
487        };
488
489        let mut message = TypedBuilder::<manifest::Owned>::new_default();
490        let mut root = message.init_root();
491        manifest.write_capnp(&mut root);
492
493        let deserialized = Manifest::read_capnp(root.into_reader());
494        assert_eq!(manifest, deserialized);
495    }
496
497    #[test]
498    fn test_manifest_diff_capnp_serialization() {
499        let context = setup_gen();
500        let conn = context.graph().conn();
501        let op_conn = context.operations().conn();
502
503        let db_uuid = crate::metadata::get_db_uuid(conn);
504        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
505
506        let mut session = start_operation(conn);
507        crate::sequence::Sequence::new()
508            .sequence("ACGT")
509            .sequence_type("DNA")
510            .save(conn);
511        let op_info = OperationInfo {
512            files: vec![],
513            description: "test op".to_string(),
514        };
515        let operation = end_operation(&context, &mut session, &op_info, "test", None).unwrap();
516
517        let manifest_operation = ManifestOperation {
518            operation,
519            file_additions: vec![],
520            annotation_file_additions: vec![],
521            operation_summary: None,
522        };
523
524        let manifest_diff = ManifestDiff {
525            missing_in_manifest2: vec![manifest_operation.clone()],
526            missing_in_manifest1: vec![manifest_operation],
527        };
528
529        let mut message = TypedBuilder::<manifest_diff::Owned>::new_default();
530        let mut root = message.init_root();
531        manifest_diff.write_capnp(&mut root);
532
533        let deserialized = ManifestDiff::read_capnp(root.into_reader());
534        assert_eq!(manifest_diff, deserialized);
535    }
536
537    #[test]
538    fn test_manifest_generator() {
539        let context = setup_gen();
540        let conn = context.graph().conn();
541        let op_conn = context.operations().conn();
542
543        let db_uuid = crate::metadata::get_db_uuid(conn);
544        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
545
546        let mut session = start_operation(conn);
547        crate::sequence::Sequence::new()
548            .sequence("ACGT")
549            .sequence_type("DNA")
550            .save(conn);
551        let op_info = OperationInfo {
552            files: vec![],
553            description: "first op".to_string(),
554        };
555        let op1 = end_operation(&context, &mut session, &op_info, "test", None).unwrap();
556
557        let mut session = start_operation(conn);
558        crate::sequence::Sequence::new()
559            .sequence("TGCA")
560            .sequence_type("DNA")
561            .save(conn);
562        let op_info = OperationInfo {
563            files: vec![],
564            description: "second op".to_string(),
565        };
566        let op2 = end_operation(&context, &mut session, &op_info, "test", None).unwrap();
567
568        let generator = ManifestGenerator::new(op_conn);
569        let manifest = generator
570            .generate_manifest("main", Some(&op2.hash))
571            .unwrap();
572
573        assert_eq!(manifest.branch_name, "main");
574        assert_eq!(manifest.operations.len(), 2);
575        assert_eq!(manifest.operations[0].operation.hash, op1.hash);
576        assert_eq!(manifest.operations[1].operation.hash, op2.hash);
577
578        let manifest = generator
579            .generate_manifest("main", Some(&op1.hash))
580            .unwrap();
581        assert_eq!(manifest.operations.len(), 1);
582        assert_eq!(manifest.operations[0].operation.hash, op1.hash);
583    }
584
585    #[test]
586    fn test_manifest_generator_includes_annotation_files() {
587        let context = setup_gen();
588        let conn = context.graph().conn();
589        let op_conn = context.operations().conn();
590
591        let db_uuid = crate::metadata::get_db_uuid(conn);
592        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
593
594        let mut session = start_operation(conn);
595        crate::sequence::Sequence::new()
596            .sequence("ACGT")
597            .sequence_type("DNA")
598            .save(conn);
599        let op_info = OperationInfo {
600            files: vec![],
601            description: "annotation op".to_string(),
602        };
603        let operation = end_operation(&context, &mut session, &op_info, "test", None).unwrap();
604
605        let repo_root = context.workspace().repo_root().unwrap();
606        let annotation_path = repo_root.join("fixtures").join("manifest_annotation.gff3");
607        fs::create_dir_all(annotation_path.parent().unwrap()).unwrap();
608        fs::write(&annotation_path, "##gff-version 3\n").unwrap();
609
610        let file_addition = AnnotationFile::add_to_operation(
611            context.workspace(),
612            op_conn,
613            &operation.hash,
614            &AnnotationFileAdditionInput {
615                file_path: "fixtures/manifest_annotation.gff3".to_string(),
616                file_type: FileTypes::Gff3,
617                checksum_override: None,
618                name: Some("manifest-track".to_string()),
619                index_file_path: None,
620            },
621        )
622        .unwrap();
623
624        let generator = ManifestGenerator::new(op_conn);
625        let manifest = generator
626            .generate_manifest("main", Some(&operation.hash))
627            .unwrap();
628
629        assert_eq!(manifest.operations.len(), 1);
630        assert_eq!(
631            manifest.operations[0].annotation_file_additions,
632            vec![ManifestAnnotationFileAddition {
633                file_addition,
634                index_file_addition: None,
635                name: Some("manifest-track".to_string()),
636            }]
637        );
638    }
639
640    #[test]
641    fn test_manifest_comparer() {
642        let context = setup_gen();
643        let conn = context.graph().conn();
644        let op_conn = context.operations().conn();
645
646        let db_uuid = crate::metadata::get_db_uuid(conn);
647        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
648
649        let mut session = start_operation(conn);
650        crate::sequence::Sequence::new()
651            .sequence("ACGT")
652            .sequence_type("DNA")
653            .save(conn);
654        let op_info = OperationInfo {
655            files: vec![],
656            description: "first op".to_string(),
657        };
658        let op1 = end_operation(&context, &mut session, &op_info, "test", None).unwrap();
659
660        let mut session = start_operation(conn);
661        crate::sequence::Sequence::new()
662            .sequence("TTTT")
663            .sequence_type("DNA")
664            .save(conn);
665        let op_info = OperationInfo {
666            files: vec![],
667            description: "second op".to_string(),
668        };
669        let op2 = end_operation(&context, &mut session, &op_info, "test 2", None).unwrap();
670
671        let mut session = start_operation(conn);
672        crate::sequence::Sequence::new()
673            .sequence("AAAA")
674            .sequence_type("DNA")
675            .save(conn);
676        let op_info = OperationInfo {
677            files: vec![],
678            description: "third op".to_string(),
679        };
680        let op3 = end_operation(&context, &mut session, &op_info, "test 3", None).unwrap();
681
682        let manifest1 = Manifest {
683            manifest_version: "1.0".to_string(),
684            branch_name: "main".to_string(),
685            end_hash: Some(op2.hash),
686            operations: vec![
687                ManifestOperation {
688                    operation: op1.clone(),
689                    file_additions: vec![],
690                    annotation_file_additions: vec![],
691                    operation_summary: None,
692                },
693                ManifestOperation {
694                    operation: op2.clone(),
695                    file_additions: vec![],
696                    annotation_file_additions: vec![],
697                    operation_summary: None,
698                },
699            ],
700        };
701
702        let manifest2 = Manifest {
703            manifest_version: "1.0".to_string(),
704            branch_name: "main".to_string(),
705            end_hash: Some(op3.hash),
706            operations: vec![
707                ManifestOperation {
708                    operation: op2.clone(),
709                    file_additions: vec![],
710                    annotation_file_additions: vec![],
711                    operation_summary: None,
712                },
713                ManifestOperation {
714                    operation: op3.clone(),
715                    file_additions: vec![],
716                    annotation_file_additions: vec![],
717                    operation_summary: None,
718                },
719            ],
720        };
721
722        let diff = ManifestComparer::diff_manifests(&manifest1, &manifest2).unwrap();
723
724        assert_eq!(diff.missing_in_manifest2.len(), 1);
725        assert_eq!(diff.missing_in_manifest2[0].operation.hash, op1.hash);
726
727        assert_eq!(diff.missing_in_manifest1.len(), 1);
728        assert_eq!(diff.missing_in_manifest1[0].operation.hash, op3.hash);
729    }
730
731    #[test]
732    fn test_manifest_generator_operation_not_found() {
733        let context = setup_gen();
734        let conn = context.graph().conn();
735        let op_conn = context.operations().conn();
736
737        let db_uuid = crate::metadata::get_db_uuid(conn);
738        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
739
740        let mut session = start_operation(conn);
741        crate::sequence::Sequence::new()
742            .sequence("ACGT")
743            .sequence_type("DNA")
744            .save(conn);
745        let op_info = OperationInfo {
746            files: vec![],
747            description: "first op".to_string(),
748        };
749        end_operation(&context, &mut session, &op_info, "test", None).unwrap();
750
751        let generator = ManifestGenerator::new(op_conn);
752        let manifest = generator
753            .generate_manifest("main", Some(&HashId::convert_str("non_existent_op")))
754            .unwrap();
755        assert!(manifest.operations.is_empty());
756    }
757}