Skip to main content

perfgate_app/
promote.rs

1//! Promote use case for copying run receipts to become baselines.
2//!
3//! This module provides functionality for promoting a current run receipt
4//! to become the new baseline for subsequent comparisons. This is typically
5//! used on trusted branches (e.g., main) after successful benchmark runs.
6
7use perfgate_types::{HostInfo, RunMeta, RunReceipt};
8
9/// Request for promoting a run receipt to become a baseline.
10#[derive(Debug, Clone)]
11pub struct PromoteRequest {
12    /// The run receipt to promote.
13    pub receipt: RunReceipt,
14
15    /// If true, strip run-specific fields (run_id, timestamps) to make
16    /// the baseline more stable across runs.
17    pub normalize: bool,
18}
19
20/// Result of a promote operation.
21#[derive(Debug, Clone)]
22pub struct PromoteResult {
23    /// The (possibly normalized) receipt to be written as baseline.
24    pub receipt: RunReceipt,
25}
26
27/// Use case for promoting run receipts to baselines.
28pub struct PromoteUseCase;
29
30impl PromoteUseCase {
31    /// Execute the promote operation.
32    ///
33    /// If `normalize` is true, the receipt will have run-specific fields
34    /// (run_id, started_at, ended_at) replaced with placeholder values
35    /// to make the baseline more stable for comparison purposes.
36    pub fn execute(req: PromoteRequest) -> PromoteResult {
37        let receipt = if req.normalize {
38            Self::normalize_receipt(req.receipt)
39        } else {
40            req.receipt
41        };
42
43        PromoteResult { receipt }
44    }
45
46    /// Normalize a receipt by stripping run-specific fields.
47    fn normalize_receipt(mut receipt: RunReceipt) -> RunReceipt {
48        receipt.run = RunMeta {
49            id: "baseline".to_string(),
50            started_at: "1970-01-01T00:00:00Z".to_string(),
51            ended_at: "1970-01-01T00:00:00Z".to_string(),
52            host: HostInfo {
53                os: receipt.run.host.os,
54                arch: receipt.run.host.arch,
55                cpu_count: receipt.run.host.cpu_count,
56                memory_bytes: receipt.run.host.memory_bytes,
57                hostname_hash: receipt.run.host.hostname_hash,
58            },
59        };
60        receipt
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use perfgate_types::{BenchMeta, HostInfo, RunMeta, Sample, Stats, ToolInfo, U64Summary};
68
69    fn create_test_receipt() -> RunReceipt {
70        RunReceipt {
71            schema: "perfgate.run.v1".to_string(),
72            tool: ToolInfo {
73                name: "perfgate".to_string(),
74                version: "0.1.0".to_string(),
75            },
76            run: RunMeta {
77                id: "unique-run-id-12345".to_string(),
78                started_at: "2024-01-15T10:00:00Z".to_string(),
79                ended_at: "2024-01-15T10:00:05Z".to_string(),
80                host: HostInfo {
81                    os: "linux".to_string(),
82                    arch: "x86_64".to_string(),
83                    cpu_count: Some(4),
84                    memory_bytes: Some(8_000_000_000),
85                    hostname_hash: Some("testhash123".to_string()),
86                },
87            },
88            bench: BenchMeta {
89                name: "test-benchmark".to_string(),
90                cwd: None,
91                command: vec!["echo".to_string(), "hello".to_string()],
92                repeat: 5,
93                warmup: 0,
94                work_units: None,
95                timeout_ms: None,
96            },
97            samples: vec![Sample {
98                wall_ms: 100,
99                exit_code: 0,
100                warmup: false,
101                timed_out: false,
102                cpu_ms: None,
103                page_faults: None,
104                ctx_switches: None,
105                max_rss_kb: Some(1024),
106                binary_bytes: None,
107                stdout: None,
108                stderr: None,
109            }],
110            stats: Stats {
111                wall_ms: U64Summary {
112                    median: 100,
113                    min: 98,
114                    max: 102,
115                },
116                cpu_ms: None,
117                page_faults: None,
118                ctx_switches: None,
119                max_rss_kb: Some(U64Summary {
120                    median: 1024,
121                    min: 1020,
122                    max: 1028,
123                }),
124                binary_bytes: None,
125                throughput_per_s: None,
126            },
127        }
128    }
129
130    #[test]
131    fn test_promote_without_normalize() {
132        let receipt = create_test_receipt();
133        let original_run_id = receipt.run.id.clone();
134        let original_started_at = receipt.run.started_at.clone();
135
136        let result = PromoteUseCase::execute(PromoteRequest {
137            receipt,
138            normalize: false,
139        });
140
141        // Without normalize, the receipt should be unchanged
142        assert_eq!(result.receipt.run.id, original_run_id);
143        assert_eq!(result.receipt.run.started_at, original_started_at);
144    }
145
146    #[test]
147    fn test_promote_with_normalize() {
148        let receipt = create_test_receipt();
149
150        let result = PromoteUseCase::execute(PromoteRequest {
151            receipt,
152            normalize: true,
153        });
154
155        // With normalize, run-specific fields should be replaced
156        assert_eq!(result.receipt.run.id, "baseline");
157        assert_eq!(result.receipt.run.started_at, "1970-01-01T00:00:00Z");
158        assert_eq!(result.receipt.run.ended_at, "1970-01-01T00:00:00Z");
159
160        // Host info should be preserved
161        assert_eq!(result.receipt.run.host.os, "linux");
162        assert_eq!(result.receipt.run.host.arch, "x86_64");
163
164        // Other fields should be unchanged
165        assert_eq!(result.receipt.bench.name, "test-benchmark");
166        assert_eq!(result.receipt.stats.wall_ms.median, 100);
167    }
168
169    #[test]
170    fn test_normalize_preserves_bench_data() {
171        let receipt = create_test_receipt();
172
173        let result = PromoteUseCase::execute(PromoteRequest {
174            receipt: receipt.clone(),
175            normalize: true,
176        });
177
178        // Verify bench metadata is preserved
179        assert_eq!(result.receipt.bench.name, receipt.bench.name);
180        assert_eq!(result.receipt.bench.command, receipt.bench.command);
181        assert_eq!(result.receipt.bench.repeat, receipt.bench.repeat);
182        assert_eq!(result.receipt.bench.warmup, receipt.bench.warmup);
183
184        // Verify samples are preserved
185        assert_eq!(result.receipt.samples.len(), receipt.samples.len());
186        assert_eq!(
187            result.receipt.samples[0].wall_ms,
188            receipt.samples[0].wall_ms
189        );
190
191        // Verify stats are preserved
192        assert_eq!(
193            result.receipt.stats.wall_ms.median,
194            receipt.stats.wall_ms.median
195        );
196    }
197
198    #[test]
199    fn test_normalize_preserves_schema_and_tool() {
200        let receipt = create_test_receipt();
201
202        let result = PromoteUseCase::execute(PromoteRequest {
203            receipt: receipt.clone(),
204            normalize: true,
205        });
206
207        assert_eq!(result.receipt.schema, receipt.schema);
208        assert_eq!(result.receipt.tool.name, receipt.tool.name);
209        assert_eq!(result.receipt.tool.version, receipt.tool.version);
210    }
211
212    #[test]
213    fn test_promote_preserves_optional_none_fields() {
214        let mut receipt = create_test_receipt();
215        receipt.run.host.cpu_count = None;
216        receipt.run.host.memory_bytes = None;
217        receipt.run.host.hostname_hash = None;
218        receipt.bench.cwd = None;
219        receipt.bench.work_units = None;
220        receipt.bench.timeout_ms = None;
221
222        let result = PromoteUseCase::execute(PromoteRequest {
223            receipt,
224            normalize: true,
225        });
226
227        assert!(result.receipt.run.host.cpu_count.is_none());
228        assert!(result.receipt.run.host.memory_bytes.is_none());
229        assert!(result.receipt.run.host.hostname_hash.is_none());
230        assert!(result.receipt.bench.cwd.is_none());
231        assert!(result.receipt.bench.work_units.is_none());
232    }
233
234    #[test]
235    fn test_promote_normalize_idempotent() {
236        let receipt = create_test_receipt();
237
238        let first = PromoteUseCase::execute(PromoteRequest {
239            receipt,
240            normalize: true,
241        });
242
243        let second = PromoteUseCase::execute(PromoteRequest {
244            receipt: first.receipt.clone(),
245            normalize: true,
246        });
247
248        assert_eq!(first.receipt.run.id, second.receipt.run.id);
249        assert_eq!(first.receipt.run.started_at, second.receipt.run.started_at);
250        assert_eq!(first.receipt.run.ended_at, second.receipt.run.ended_at);
251    }
252}