1use crate::{
7 Platform, Resource, ResourceLifecycle, ResourceOutputs, ResourceOutputsDefinition, ResourceRef,
8 ResourceStatus,
9};
10
11use alien_error::AlienError;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fmt::Debug;
15use uuid::Uuid;
16
17use crate::{ErrorData, Result};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22#[serde(rename_all = "snake_case")]
23pub enum StackStatus {
24 Pending,
26 InProgress,
28 Running,
30 Deleted,
32 Failure,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
39#[serde(rename_all = "camelCase")]
40pub struct StackState {
41 pub platform: Platform,
43 pub resources: HashMap<String, StackResourceState>,
45 pub resource_prefix: String,
47}
48
49impl StackState {
50 pub fn new(platform: Platform) -> Self {
52 let letters = "abcdefghijklmnopqrstuvwxyz";
55 let first_char = letters
56 .chars()
57 .nth(Uuid::new_v4().as_bytes()[0] as usize % 26)
58 .unwrap();
59 let uuid_part = Uuid::new_v4().simple().to_string()[..7].to_string();
60 let prefix = format!("{}{}", first_char, uuid_part);
61
62 StackState {
63 platform,
64 resources: HashMap::new(),
65 resource_prefix: prefix,
66 }
67 }
68
69 pub fn resource(&self, id: &str) -> Option<&StackResourceState> {
71 self.resources.get(id)
72 }
73
74 pub fn compute_stack_status(&self) -> Result<StackStatus> {
77 let resource_statuses: Vec<ResourceStatus> = self
78 .resources
79 .values()
80 .map(|resource| resource.status)
81 .collect();
82
83 Self::compute_stack_status_from_resources(&resource_statuses)
84 }
85
86 pub fn compute_stack_status_from_resources(
89 resource_statuses: &[ResourceStatus],
90 ) -> Result<StackStatus> {
91 if resource_statuses.is_empty() {
93 return Ok(StackStatus::Pending);
94 }
95
96 if resource_statuses.iter().any(|status| {
98 matches!(
99 status,
100 ResourceStatus::ProvisionFailed
101 | ResourceStatus::UpdateFailed
102 | ResourceStatus::DeleteFailed
103 | ResourceStatus::RefreshFailed
104 )
105 }) {
106 return Ok(StackStatus::Failure);
107 }
108
109 if resource_statuses.iter().any(|status| {
111 matches!(
112 status,
113 ResourceStatus::Pending
114 | ResourceStatus::Provisioning
115 | ResourceStatus::Updating
116 | ResourceStatus::Deleting
117 )
118 }) {
119 return Ok(StackStatus::InProgress);
120 }
121
122 if resource_statuses
124 .iter()
125 .all(|status| matches!(status, ResourceStatus::Running))
126 {
127 return Ok(StackStatus::Running);
128 }
129
130 if resource_statuses
131 .iter()
132 .all(|status| matches!(status, ResourceStatus::Deleted))
133 {
134 return Ok(StackStatus::Deleted);
135 }
136
137 let has_running = resource_statuses
141 .iter()
142 .any(|status| matches!(status, ResourceStatus::Running));
143 let has_deleted = resource_statuses
144 .iter()
145 .any(|status| matches!(status, ResourceStatus::Deleted));
146 let only_running_or_deleted = resource_statuses
147 .iter()
148 .all(|status| matches!(status, ResourceStatus::Running | ResourceStatus::Deleted));
149
150 if has_running && has_deleted && only_running_or_deleted {
151 return Ok(StackStatus::InProgress);
152 }
153
154 let status_strings: Vec<String> = resource_statuses
156 .iter()
157 .map(|status| format!("{:?}", status).to_lowercase().replace('_', "-"))
158 .collect();
159
160 Err(AlienError::new(
161 ErrorData::UnexpectedResourceStatusCombination {
162 resource_statuses: status_strings,
163 operation: "stack status computation".to_string(),
164 },
165 ))
166 }
167
168 pub fn get_resource_outputs<T: ResourceOutputsDefinition + 'static>(
190 &self,
191 resource_id: &str,
192 ) -> Result<&T> {
193 let resource_state = self.resources.get(resource_id).ok_or_else(|| {
194 AlienError::new(ErrorData::ResourceNotFound {
195 resource_id: resource_id.to_string(),
196 available_resources: self.resources.keys().cloned().collect(),
197 })
198 })?;
199
200 let outputs = resource_state.outputs.as_ref().ok_or_else(|| {
201 AlienError::new(ErrorData::ResourceHasNoOutputs {
202 resource_id: resource_id.to_string(),
203 })
204 })?;
205
206 outputs.downcast_ref::<T>().ok_or_else(|| {
207 AlienError::new(ErrorData::UnexpectedResourceType {
208 resource_id: resource_id.to_string(),
209 expected: T::resource_type(),
210 actual: resource_state.resource_type.clone().into(),
211 })
212 })
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
218#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
219#[serde(rename_all = "camelCase")]
220pub struct StackResourceState {
221 #[serde(rename = "type")]
223 pub resource_type: String,
224
225 #[serde(rename = "_internal", skip_serializing_if = "Option::is_none")]
229 pub internal_state: Option<serde_json::Value>,
230
231 pub status: ResourceStatus,
233
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub outputs: Option<ResourceOutputs>,
237
238 pub config: Resource,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
244 pub previous_config: Option<Resource>,
245
246 #[serde(default, skip_serializing_if = "is_zero")]
248 #[builder(default)]
249 pub retry_attempt: u32,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub error: Option<AlienError>,
254
255 #[serde(default, skip_serializing_if = "is_false")]
258 #[builder(default)]
259 pub is_externally_provisioned: bool,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
264 pub lifecycle: Option<ResourceLifecycle>,
265
266 #[serde(default, skip_serializing_if = "Vec::is_empty")]
269 #[builder(default = vec![])]
270 pub dependencies: Vec<ResourceRef>,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
276 pub last_failed_state: Option<serde_json::Value>,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
283 pub remote_binding_params: Option<serde_json::Value>,
284}
285
286impl StackResourceState {
287 pub fn new_pending(
289 resource_type: String,
290 config: Resource,
291 lifecycle: Option<ResourceLifecycle>,
292 dependencies: Vec<ResourceRef>,
293 ) -> Self {
294 Self {
295 resource_type,
296 internal_state: None,
297 status: ResourceStatus::Pending,
298 outputs: None,
299 config,
300 previous_config: None,
301 retry_attempt: 0,
302 error: None,
303 is_externally_provisioned: false,
304 lifecycle,
305 dependencies,
306 last_failed_state: None,
307 remote_binding_params: None,
308 }
309 }
310
311 pub fn with_updates<F>(&self, update_fn: F) -> Self
313 where
314 F: FnOnce(&mut Self),
315 {
316 let mut new_state = self.clone();
317 update_fn(&mut new_state);
318 new_state
319 }
320
321 pub fn with_failure(&self, status: ResourceStatus, error: AlienError) -> Self {
323 self.with_updates(|state| {
324 state.status = status;
325 state.error = Some(error);
326 state.retry_attempt = 0;
327 })
328 }
329}
330
331fn is_zero(num: &u32) -> bool {
333 *num == 0
334}
335
336fn is_false(b: &bool) -> bool {
338 !*b
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::{Function, FunctionCode, FunctionOutputs, ResourceType, Storage, StorageOutputs};
345
346 #[test]
347 fn test_get_resource_outputs_success() {
348 let mut stack_state = StackState::new(Platform::Aws);
349
350 let function_outputs = FunctionOutputs {
352 function_name: "test-function".to_string(),
353 url: Some("https://example.lambda-url.us-east-1.on.aws/".to_string()),
354 identifier: Some(
355 "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
356 ),
357 load_balancer_endpoint: None,
358 };
359
360 let test_function = Function::new("test-function".to_string())
361 .code(FunctionCode::Image {
362 image: "test:latest".to_string(),
363 })
364 .permissions("test-profile".to_string())
365 .build();
366
367 let resource_state = StackResourceState::new_pending(
368 "function".to_string(),
369 Resource::new(test_function),
370 None,
371 Vec::new(),
372 )
373 .with_updates(|state| {
374 state.status = ResourceStatus::Running;
375 state.outputs = Some(ResourceOutputs::new(function_outputs.clone()));
376 });
377
378 stack_state
379 .resources
380 .insert("test-function".to_string(), resource_state);
381
382 let retrieved_outputs = stack_state
384 .get_resource_outputs::<FunctionOutputs>("test-function")
385 .unwrap();
386 assert_eq!(retrieved_outputs.function_name, "test-function");
387 assert_eq!(
388 retrieved_outputs.url,
389 Some("https://example.lambda-url.us-east-1.on.aws/".to_string())
390 );
391 assert_eq!(
392 retrieved_outputs.identifier,
393 Some("arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string())
394 );
395 }
396
397 #[test]
398 fn test_get_resource_outputs_resource_not_found() {
399 let stack_state = StackState::new(Platform::Aws);
400
401 let result = stack_state.get_resource_outputs::<FunctionOutputs>("nonexistent-function");
403 assert!(result.is_err());
404 let error = result.unwrap_err();
405
406 let error_data = &error.error;
408 if let Some(ErrorData::ResourceNotFound {
409 resource_id,
410 available_resources,
411 }) = error_data
412 {
413 assert_eq!(resource_id, "nonexistent-function");
414 assert_eq!(available_resources, &Vec::<String>::new());
415 } else {
416 panic!("Expected ResourceNotFound error, got: {:?}", error_data);
417 }
418
419 let error_message = error.to_string();
421 assert!(error_message.contains("Resource 'nonexistent-function' not found in stack state"));
422 assert!(error_message.contains("Available resources: []"));
423 }
424
425 #[test]
426 fn test_get_resource_outputs_no_outputs() {
427 let mut stack_state = StackState::new(Platform::Aws);
428
429 let test_function_2 = Function::new("test-function".to_string())
431 .code(FunctionCode::Image {
432 image: "test:latest".to_string(),
433 })
434 .permissions("test-profile".to_string())
435 .build();
436
437 let resource_state = StackResourceState::new_pending(
438 "function".to_string(),
439 Resource::new(test_function_2),
440 None,
441 Vec::new(),
442 )
443 .with_updates(|state| {
444 state.status = ResourceStatus::Provisioning;
445 });
446
447 stack_state
448 .resources
449 .insert("test-function".to_string(), resource_state);
450
451 let result = stack_state.get_resource_outputs::<FunctionOutputs>("test-function");
453 assert!(result.is_err());
454 let error = result.unwrap_err();
455
456 let error_data = &error.error;
458 if let Some(ErrorData::ResourceHasNoOutputs { resource_id, .. }) = error_data {
459 assert_eq!(resource_id, "test-function");
460 } else {
461 panic!("Expected ResourceHasNoOutputs error, got: {:?}", error_data);
462 }
463
464 let error_message = error.to_string();
466 assert!(error_message.contains("Resource 'test-function' has no outputs"));
467 }
468
469 #[test]
470 fn test_get_resource_outputs_wrong_type() {
471 let mut stack_state = StackState::new(Platform::Aws);
472
473 let storage_outputs = StorageOutputs {
475 bucket_name: "test-bucket".to_string(),
476 };
477
478 let test_storage = Storage::new("test-storage".to_string()).build();
479
480 let resource_state = StackResourceState::new_pending(
481 "storage".to_string(),
482 Resource::new(test_storage),
483 None,
484 Vec::new(),
485 )
486 .with_updates(|state| {
487 state.status = ResourceStatus::Running;
488 state.outputs = Some(ResourceOutputs::new(storage_outputs));
489 });
490
491 stack_state
492 .resources
493 .insert("test-storage".to_string(), resource_state);
494
495 let result = stack_state.get_resource_outputs::<FunctionOutputs>("test-storage");
497 assert!(result.is_err());
498 let error = result.unwrap_err();
499
500 let error_data = &error.error;
502 if let Some(ErrorData::UnexpectedResourceType {
503 resource_id,
504 expected,
505 actual,
506 }) = error_data
507 {
508 assert_eq!(resource_id, "test-storage");
509 assert_eq!(*expected, ResourceType::from_static("function"));
510 assert_eq!(*actual, ResourceType::from_static("storage"));
511 } else {
512 panic!(
513 "Expected UnexpectedResourceType error, got: {:?}",
514 error_data
515 );
516 }
517 }
518
519 #[test]
520 fn test_get_resource_outputs_usage_example() {
521 let mut stack_state = StackState::new(Platform::Aws);
522
523 let function_outputs = FunctionOutputs {
525 function_name: "test-alien-function".to_string(),
526 url: Some("https://test.lambda-url.us-east-1.on.aws/".to_string()),
527 identifier: Some(
528 "arn:aws:lambda:us-east-1:123456789012:function:test-alien-function".to_string(),
529 ),
530 load_balancer_endpoint: None,
531 };
532
533 let test_alien_function = Function::new("test-alien-function".to_string())
534 .code(FunctionCode::Image {
535 image: "test:latest".to_string(),
536 })
537 .permissions("test-profile".to_string())
538 .build();
539
540 let resource_state = StackResourceState {
541 resource_type: "function".to_string(),
542 internal_state: None,
543 status: ResourceStatus::Running,
544 outputs: Some(ResourceOutputs::new(function_outputs)),
545 config: Resource::new(test_alien_function),
546 previous_config: None,
547 retry_attempt: 0,
548 error: None,
549 is_externally_provisioned: false,
550 lifecycle: None,
551 dependencies: Vec::new(),
552 last_failed_state: None,
553 remote_binding_params: None,
554 };
555
556 stack_state
557 .resources
558 .insert("test-alien-function".to_string(), resource_state);
559
560 let function_outputs = stack_state
562 .get_resource_outputs::<FunctionOutputs>("test-alien-function")
563 .unwrap();
564
565 let function_url = function_outputs
566 .url
567 .as_ref()
568 .ok_or_else(|| "Function URL not found in stack state")
569 .unwrap();
570
571 assert_eq!(function_url, "https://test.lambda-url.us-east-1.on.aws/");
572 }
573
574 #[cfg(test)]
576 mod stack_status_tests {
577 use super::*;
578
579 #[test]
580 fn test_compute_stack_status_empty_resources() {
581 let result = StackState::compute_stack_status_from_resources(&[]).unwrap();
582 assert_eq!(result, StackStatus::Pending);
583 }
584
585 #[test]
586 fn test_compute_stack_status_single_pending() {
587 let result =
588 StackState::compute_stack_status_from_resources(&[ResourceStatus::Pending])
589 .unwrap();
590 assert_eq!(result, StackStatus::InProgress);
591 }
592
593 #[test]
594 fn test_compute_stack_status_single_provisioning() {
595 let result =
596 StackState::compute_stack_status_from_resources(&[ResourceStatus::Provisioning])
597 .unwrap();
598 assert_eq!(result, StackStatus::InProgress);
599 }
600
601 #[test]
602 fn test_compute_stack_status_single_updating() {
603 let result =
604 StackState::compute_stack_status_from_resources(&[ResourceStatus::Updating])
605 .unwrap();
606 assert_eq!(result, StackStatus::InProgress);
607 }
608
609 #[test]
610 fn test_compute_stack_status_single_deleting() {
611 let result =
612 StackState::compute_stack_status_from_resources(&[ResourceStatus::Deleting])
613 .unwrap();
614 assert_eq!(result, StackStatus::InProgress);
615 }
616
617 #[test]
618 fn test_compute_stack_status_single_provision_failed() {
619 let result =
620 StackState::compute_stack_status_from_resources(&[ResourceStatus::ProvisionFailed])
621 .unwrap();
622 assert_eq!(result, StackStatus::Failure);
623 }
624
625 #[test]
626 fn test_compute_stack_status_single_update_failed() {
627 let result =
628 StackState::compute_stack_status_from_resources(&[ResourceStatus::UpdateFailed])
629 .unwrap();
630 assert_eq!(result, StackStatus::Failure);
631 }
632
633 #[test]
634 fn test_compute_stack_status_single_delete_failed() {
635 let result =
636 StackState::compute_stack_status_from_resources(&[ResourceStatus::DeleteFailed])
637 .unwrap();
638 assert_eq!(result, StackStatus::Failure);
639 }
640
641 #[test]
642 fn test_compute_stack_status_single_refresh_failed() {
643 let result =
644 StackState::compute_stack_status_from_resources(&[ResourceStatus::RefreshFailed])
645 .unwrap();
646 assert_eq!(result, StackStatus::Failure);
647 }
648
649 #[test]
650 fn test_compute_stack_status_single_running() {
651 let result =
652 StackState::compute_stack_status_from_resources(&[ResourceStatus::Running])
653 .unwrap();
654 assert_eq!(result, StackStatus::Running);
655 }
656
657 #[test]
658 fn test_compute_stack_status_single_deleted() {
659 let result =
660 StackState::compute_stack_status_from_resources(&[ResourceStatus::Deleted])
661 .unwrap();
662 assert_eq!(result, StackStatus::Deleted);
663 }
664
665 #[test]
666 fn test_compute_stack_status_all_running() {
667 let statuses = vec![
668 ResourceStatus::Running,
669 ResourceStatus::Running,
670 ResourceStatus::Running,
671 ];
672 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
673 assert_eq!(result, StackStatus::Running);
674 }
675
676 #[test]
677 fn test_compute_stack_status_all_deleted() {
678 let statuses = vec![
679 ResourceStatus::Deleted,
680 ResourceStatus::Deleted,
681 ResourceStatus::Deleted,
682 ];
683 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
684 assert_eq!(result, StackStatus::Deleted);
685 }
686
687 #[test]
688 fn test_compute_stack_status_all_pending() {
689 let statuses = vec![
690 ResourceStatus::Pending,
691 ResourceStatus::Pending,
692 ResourceStatus::Pending,
693 ];
694 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
695 assert_eq!(result, StackStatus::InProgress);
696 }
697
698 #[test]
699 fn test_compute_stack_status_all_provisioning() {
700 let statuses = vec![
701 ResourceStatus::Provisioning,
702 ResourceStatus::Provisioning,
703 ResourceStatus::Provisioning,
704 ];
705 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
706 assert_eq!(result, StackStatus::InProgress);
707 }
708
709 #[test]
710 fn test_compute_stack_status_all_provision_failed() {
711 let statuses = vec![
712 ResourceStatus::ProvisionFailed,
713 ResourceStatus::ProvisionFailed,
714 ResourceStatus::ProvisionFailed,
715 ];
716 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
717 assert_eq!(result, StackStatus::Failure);
718 }
719
720 #[test]
721 fn test_compute_stack_status_mixed_with_failure() {
722 let statuses = vec![
723 ResourceStatus::Running,
724 ResourceStatus::ProvisionFailed,
725 ResourceStatus::Updating,
726 ];
727 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
728 assert_eq!(result, StackStatus::Failure);
729 }
730
731 #[test]
732 fn test_compute_stack_status_failure_with_success() {
733 let statuses = vec![
734 ResourceStatus::Running,
735 ResourceStatus::UpdateFailed,
736 ResourceStatus::Running,
737 ];
738 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
739 assert_eq!(result, StackStatus::Failure);
740 }
741
742 #[test]
743 fn test_compute_stack_status_failure_with_in_progress() {
744 let statuses = vec![
745 ResourceStatus::Provisioning,
746 ResourceStatus::DeleteFailed,
747 ResourceStatus::Deleting,
748 ];
749 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
750 assert_eq!(result, StackStatus::Failure);
751 }
752
753 #[test]
754 fn test_compute_stack_status_any_in_progress() {
755 let statuses = vec![
756 ResourceStatus::Running,
757 ResourceStatus::Updating,
758 ResourceStatus::Running,
759 ];
760 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
761 assert_eq!(result, StackStatus::InProgress);
762 }
763
764 #[test]
765 fn test_compute_stack_status_mixed_in_progress_states() {
766 let statuses = vec![
767 ResourceStatus::Pending,
768 ResourceStatus::Provisioning,
769 ResourceStatus::Updating,
770 ResourceStatus::Deleting,
771 ];
772 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
773 assert_eq!(result, StackStatus::InProgress);
774 }
775
776 #[test]
777 fn test_compute_stack_status_deletion_in_progress() {
778 let statuses = vec![ResourceStatus::Running, ResourceStatus::Deleted];
781 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
782 assert_eq!(result, StackStatus::InProgress);
783 }
784
785 #[test]
786 fn test_compute_stack_status_deletion_in_progress_many_resources() {
787 let statuses = vec![
789 ResourceStatus::Running,
790 ResourceStatus::Running,
791 ResourceStatus::Deleted,
792 ResourceStatus::Deleted,
793 ResourceStatus::Running,
794 ResourceStatus::Running,
795 ResourceStatus::Running,
796 ResourceStatus::Running,
797 ResourceStatus::Running,
798 ];
799 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
800 assert_eq!(result, StackStatus::InProgress);
801 }
802
803 #[test]
804 fn test_compute_stack_status_mixed_terminal_with_in_progress() {
805 let statuses = vec![
806 ResourceStatus::Running,
807 ResourceStatus::Deleted,
808 ResourceStatus::Pending,
809 ];
810 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
811 assert_eq!(result, StackStatus::InProgress);
812 }
813
814 #[test]
815 fn test_compute_stack_status_large_number_of_resources() {
816 let statuses: Vec<ResourceStatus> = (0..100).map(|_| ResourceStatus::Running).collect();
817 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
818 assert_eq!(result, StackStatus::Running);
819 }
820
821 #[test]
822 fn test_compute_stack_status_single_failure_among_many() {
823 let mut statuses: Vec<ResourceStatus> =
824 (0..50).map(|_| ResourceStatus::Running).collect();
825 statuses.push(ResourceStatus::ProvisionFailed);
826 statuses.extend((0..49).map(|_| ResourceStatus::Provisioning));
827
828 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
829 assert_eq!(result, StackStatus::Failure);
830 }
831
832 #[test]
833 fn test_compute_stack_status_failure_priority_over_in_progress() {
834 let statuses = vec![
835 ResourceStatus::ProvisionFailed,
836 ResourceStatus::UpdateFailed,
837 ResourceStatus::DeleteFailed,
838 ResourceStatus::Provisioning,
839 ResourceStatus::Updating,
840 ResourceStatus::Deleting,
841 ];
842 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
843 assert_eq!(result, StackStatus::Failure);
844 }
845
846 #[test]
847 fn test_compute_stack_status_mixed_success_and_in_progress() {
848 let statuses = vec![
849 ResourceStatus::Running,
850 ResourceStatus::Provisioning,
851 ResourceStatus::Running,
852 ];
853 let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
854 assert_eq!(result, StackStatus::InProgress);
855 }
856
857 #[test]
858 fn test_stack_state_status_computation() {
859 let mut stack_state = StackState::new(Platform::Aws);
860
861 assert_eq!(
863 stack_state.compute_stack_status().unwrap(),
864 StackStatus::Pending
865 );
866
867 let test_function = Function::new("test-function".to_string())
869 .code(FunctionCode::Image {
870 image: "test:latest".to_string(),
871 })
872 .permissions("test-profile".to_string())
873 .build();
874
875 let resource_state = StackResourceState::new_pending(
876 "function".to_string(),
877 Resource::new(test_function),
878 None,
879 Vec::new(),
880 )
881 .with_updates(|state| {
882 state.status = ResourceStatus::Running;
883 });
884
885 stack_state
886 .resources
887 .insert("test-function".to_string(), resource_state);
888
889 assert_eq!(
891 stack_state.compute_stack_status().unwrap(),
892 StackStatus::Running
893 );
894 }
895 }
896}