lean_ctx/core/patterns/
pulumi.rs1macro_rules! static_regex {
10 ($pattern:expr_2021) => {{
11 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
12 RE.get_or_init(|| {
13 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
14 })
15 }};
16}
17
18fn summary_re() -> &'static regex::Regex {
20 static_regex!(
21 r"^[+~\-]?\s*\d+\s+(to create|to update|to delete|to replace|created|updated|deleted|replaced|unchanged|changed)\b"
22 )
23}
24
25pub fn compress(command: &str, output: &str) -> Option<String> {
26 let c = command.trim();
27 let sub = c
28 .strip_prefix("pulumi")
29 .map_or("", str::trim_start)
30 .split_whitespace()
31 .next()
32 .unwrap_or("");
33 match sub {
34 "up" | "update" | "preview" | "destroy" | "refresh" => Some(compress_update(output)),
35 _ => Some(compress_generic(output)),
36 }
37}
38
39fn compress_update(output: &str) -> String {
40 let mut kept: Vec<String> = Vec::new();
41 let mut in_outputs = false;
42
43 for raw in output.lines() {
44 let t = raw.trim();
45 if t.is_empty() {
46 in_outputs = false;
47 continue;
48 }
49 let tl = t.to_ascii_lowercase();
50
51 if t == "Outputs:" {
52 in_outputs = true;
53 kept.push(t.to_string());
54 continue;
55 }
56 if in_outputs {
57 if t == "Resources:" || tl.starts_with("duration:") {
60 in_outputs = false;
61 } else {
62 kept.push(format!(" {t}"));
63 continue;
64 }
65 }
66
67 if t == "Resources:" || tl.starts_with("duration:") {
68 kept.push(t.to_string());
69 continue;
70 }
71 if summary_re().is_match(t) {
72 kept.push(t.to_string());
73 continue;
74 }
75 if tl.starts_with("updating (")
76 || tl.starts_with("previewing update (")
77 || tl.starts_with("destroying (")
78 {
79 kept.push(t.to_string());
80 continue;
81 }
82 if tl.contains("error:")
83 || tl.starts_with("error")
84 || tl.starts_with("diagnostics:")
85 || tl.contains("failed")
86 || tl.contains("panic:")
87 {
88 kept.push(t.to_string());
89 }
90 }
91
92 if kept.is_empty() {
93 return compress_generic(output);
94 }
95 kept.join("\n")
96}
97
98fn compress_generic(output: &str) -> String {
99 let lines: Vec<&str> = output
100 .lines()
101 .map(str::trim_end)
102 .filter(|l| !l.trim().is_empty())
103 .collect();
104 if lines.is_empty() {
105 return "pulumi: ok".to_string();
106 }
107 let max = 15;
108 if lines.len() <= max {
109 return lines.join("\n");
110 }
111 format!(
112 "{}\n... (+{} lines)",
113 lines[..max].join("\n"),
114 lines.len() - max
115 )
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 const UP: &str = "Updating (dev):\n Type Name Status\n + pulumi:pulumi:Stack proj-dev created\n + ├─ aws:s3:Bucket my-bucket created\n ~ └─ aws:s3:BucketPolicy pol updated\n\nOutputs:\n bucketName: \"my-bucket-abc123\"\n url : \"https://my-bucket.s3.amazonaws.com\"\n\nResources:\n + 5 created\n ~ 2 updated\n 10 unchanged\n\nDuration: 35s\n";
123
124 #[test]
125 fn keeps_outputs_summary_duration_drops_tree() {
126 let r = compress("pulumi up", UP).unwrap();
127 assert!(
128 r.contains("url : \"https://my-bucket"),
129 "keeps outputs: {r}"
130 );
131 assert!(r.contains("+ 5 created"), "keeps summary: {r}");
132 assert!(r.contains("Duration: 35s"), "keeps duration: {r}");
133 assert!(
134 !r.contains("pulumi:pulumi:Stack"),
135 "drops resource tree: {r}"
136 );
137 assert!(!r.contains("aws:s3:Bucket "), "drops resource tree: {r}");
138 }
139
140 #[test]
141 fn keeps_errors() {
142 let out = "Updating (dev):\n + aws:s3:Bucket b created\nDiagnostics:\n aws:s3:Bucket (b):\n error: creating S3 Bucket: BucketAlreadyExists";
143 let r = compress("pulumi up", out).unwrap();
144 assert!(r.contains("error: creating S3 Bucket"), "{r}");
145 }
146
147 #[test]
148 fn shorter_than_input() {
149 let r = compress("pulumi up", UP).unwrap();
150 assert!(r.len() < UP.len(), "compressed shorter");
151 }
152}