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}