Skip to main content

jugar_probar/
brick_house.rs

1//! BrickHouse: Budgeted Composition of Bricks (PROBAR-SPEC-009)
2//!
3//! A `BrickHouse` composes multiple bricks with a total performance budget.
4//! Individual bricks contribute to the house budget, and the house validates
5//! that the sum of brick budgets does not exceed the house budget.
6//!
7//! # Design Philosophy
8//!
9//! ```text
10//! BrickHouse(1000ms)
11//! ├── StatusBrick(50ms)
12//! ├── WaveformBrick(100ms)
13//! ├── TranscriptionBrick(600ms)
14//! └── ControlsBrick(50ms)
15//! Total: 800ms < 1000ms budget ✓
16//! ```
17//!
18//! # Jidoka: Stop-the-Line
19//!
20//! If any brick exceeds its budget, the BrickHouse triggers a Jidoka alert
21//! and halts rendering. This prevents cascading performance failures.
22//!
23//! # Example
24//!
25//! ```rust,ignore
26//! use probar::brick_house::{BrickHouse, BrickHouseBuilder};
27//!
28//! let house = BrickHouseBuilder::new("whisper-app")
29//!     .budget_ms(1000)
30//!     .brick(status_brick, 50)
31//!     .brick(waveform_brick, 100)
32//!     .brick(transcription_brick, 600)
33//!     .build()?;
34//! ```
35//!
36//! # References
37//!
38//! - Toyota Production System: Jidoka (autonomation)
39//! - PROBAR-SPEC-009: Bug Hunting Probador
40
41use std::collections::HashMap;
42use std::sync::Arc;
43use std::time::{Duration, Instant};
44
45use crate::brick::{
46    Brick, BrickBudget, BrickError, BrickPhase, BrickResult, BrickVerification, BudgetViolation,
47};
48
49/// A composed house of bricks with a total performance budget.
50///
51/// The BrickHouse ensures:
52/// 1. Sum of brick budgets ≤ house budget
53/// 2. All brick assertions pass before rendering
54/// 3. Runtime budget violations trigger Jidoka alerts
55#[derive(Debug)]
56pub struct BrickHouse {
57    /// House name for identification
58    name: String,
59    /// Total budget for the house
60    budget: BrickBudget,
61    /// Bricks with their allocated budgets
62    bricks: Vec<BrickEntry>,
63    /// Budget report from last render
64    last_report: Option<BudgetReport>,
65}
66
67/// Entry for a brick in the house
68struct BrickEntry {
69    /// The brick instance
70    brick: Arc<dyn Brick>,
71    /// Allocated budget (may differ from brick's intrinsic budget)
72    allocated_ms: u32,
73    /// Last measured render time
74    last_render_time: Option<Duration>,
75}
76
77impl std::fmt::Debug for BrickEntry {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("BrickEntry")
80            .field("brick_name", &self.brick.brick_name())
81            .field("allocated_ms", &self.allocated_ms)
82            .field("last_render_time", &self.last_render_time)
83            .finish()
84    }
85}
86
87/// Report on budget usage after a render cycle
88#[derive(Debug, Clone)]
89pub struct BudgetReport {
90    /// House name
91    pub house_name: String,
92    /// Total budget allocated
93    pub total_budget_ms: u32,
94    /// Total time used
95    pub total_used_ms: u32,
96    /// Individual brick timings
97    pub brick_timings: HashMap<String, BrickTiming>,
98    /// Any budget violations
99    pub violations: Vec<BudgetViolation>,
100    /// Timestamp of report
101    pub timestamp: std::time::SystemTime,
102}
103
104/// Timing information for a single brick
105#[derive(Debug, Clone)]
106pub struct BrickTiming {
107    /// Brick name
108    pub name: String,
109    /// Allocated budget
110    pub budget_ms: u32,
111    /// Actual time used
112    pub used_ms: u32,
113    /// Whether budget was exceeded
114    pub exceeded: bool,
115}
116
117impl BudgetReport {
118    /// Check if the house stayed within budget
119    #[must_use]
120    pub fn within_budget(&self) -> bool {
121        self.violations.is_empty() && self.total_used_ms <= self.total_budget_ms
122    }
123
124    /// Get budget utilization as percentage
125    #[must_use]
126    pub fn utilization(&self) -> f32 {
127        if self.total_budget_ms == 0 {
128            0.0
129        } else {
130            (self.total_used_ms as f32 / self.total_budget_ms as f32) * 100.0
131        }
132    }
133
134    /// Get all violations
135    #[must_use]
136    pub fn violations(&self) -> &[BudgetViolation] {
137        &self.violations
138    }
139}
140
141impl BrickHouse {
142    /// Create a new brick house with the given name and budget
143    #[must_use]
144    pub fn new(name: impl Into<String>, budget_ms: u32) -> Self {
145        Self {
146            name: name.into(),
147            budget: BrickBudget::uniform(budget_ms),
148            bricks: Vec::new(),
149            last_report: None,
150        }
151    }
152
153    /// Add a brick with a specific budget allocation
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if adding this brick would exceed the house budget.
158    pub fn add_brick(&mut self, brick: Arc<dyn Brick>, budget_ms: u32) -> BrickResult<()> {
159        let current_total: u32 = self.bricks.iter().map(|b| b.allocated_ms).sum();
160        let new_total = current_total + budget_ms;
161
162        if new_total > self.budget.total_ms {
163            return Err(BrickError::BudgetExceeded(BudgetViolation {
164                brick_name: brick.brick_name().to_string(),
165                budget: self.budget,
166                actual: Duration::from_millis(new_total as u64),
167                phase: None,
168            }));
169        }
170
171        self.bricks.push(BrickEntry {
172            brick,
173            allocated_ms: budget_ms,
174            last_render_time: None,
175        });
176
177        Ok(())
178    }
179
180    /// Get the house name
181    #[must_use]
182    pub fn name(&self) -> &str {
183        &self.name
184    }
185
186    /// Get the total budget
187    #[must_use]
188    pub fn budget(&self) -> BrickBudget {
189        self.budget
190    }
191
192    /// Get the number of bricks
193    #[must_use]
194    pub fn brick_count(&self) -> usize {
195        self.bricks.len()
196    }
197
198    /// Get remaining budget after allocations
199    #[must_use]
200    pub fn remaining_budget_ms(&self) -> u32 {
201        let allocated: u32 = self.bricks.iter().map(|b| b.allocated_ms).sum();
202        self.budget.total_ms.saturating_sub(allocated)
203    }
204
205    /// Verify all bricks in the house
206    ///
207    /// Returns verification results for all bricks.
208    pub fn verify_all(&self) -> Vec<(&str, BrickVerification)> {
209        self.bricks
210            .iter()
211            .map(|entry| {
212                let name = entry.brick.brick_name();
213                let verification = entry.brick.verify();
214                (name, verification)
215            })
216            .collect()
217    }
218
219    /// Check if the house can render (all bricks valid)
220    #[must_use]
221    pub fn can_render(&self) -> bool {
222        self.bricks.iter().all(|entry| entry.brick.can_render())
223    }
224
225    /// Render all bricks and track timing
226    ///
227    /// Returns the generated HTML for all bricks.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if any brick exceeds its budget (Jidoka).
232    pub fn render(&mut self) -> BrickResult<String> {
233        let mut html_parts = Vec::new();
234        let mut timings = HashMap::new();
235        let mut violations = Vec::new();
236        let mut total_used_ms = 0u32;
237
238        for entry in &mut self.bricks {
239            let start = Instant::now();
240
241            // Verify before render
242            let verification = entry.brick.verify();
243            if !verification.is_valid() {
244                let (assertion, reason) = verification
245                    .failed
246                    .first()
247                    .map(|(a, r)| (a.clone(), r.clone()))
248                    .unwrap_or_else(|| {
249                        (crate::brick::BrickAssertion::TextVisible, "Unknown".into())
250                    });
251                return Err(BrickError::AssertionFailed { assertion, reason });
252            }
253
254            // Generate HTML
255            let html = entry.brick.to_html();
256            html_parts.push(html);
257
258            let elapsed = start.elapsed();
259            let elapsed_ms = elapsed.as_millis() as u32;
260            entry.last_render_time = Some(elapsed);
261            total_used_ms += elapsed_ms;
262
263            let exceeded = elapsed_ms > entry.allocated_ms;
264            let brick_name = entry.brick.brick_name().to_string();
265
266            timings.insert(
267                brick_name.clone(),
268                BrickTiming {
269                    name: brick_name.clone(),
270                    budget_ms: entry.allocated_ms,
271                    used_ms: elapsed_ms,
272                    exceeded,
273                },
274            );
275
276            if exceeded {
277                violations.push(BudgetViolation {
278                    brick_name,
279                    budget: BrickBudget::uniform(entry.allocated_ms),
280                    actual: elapsed,
281                    phase: Some(BrickPhase::Paint),
282                });
283            }
284        }
285
286        // Store report
287        self.last_report = Some(BudgetReport {
288            house_name: self.name.clone(),
289            total_budget_ms: self.budget.total_ms,
290            total_used_ms,
291            brick_timings: timings,
292            violations: violations.clone(),
293            timestamp: std::time::SystemTime::now(),
294        });
295
296        // Jidoka: stop-the-line on violations
297        if !violations.is_empty() {
298            return Err(BrickError::BudgetExceeded(
299                violations.into_iter().next().expect("violations not empty"),
300            ));
301        }
302
303        Ok(html_parts.join("\n"))
304    }
305
306    /// Get the last budget report
307    #[must_use]
308    pub fn last_report(&self) -> Option<&BudgetReport> {
309        self.last_report.as_ref()
310    }
311
312    /// Generate combined CSS for all bricks
313    #[must_use]
314    pub fn to_css(&self) -> String {
315        self.bricks
316            .iter()
317            .map(|entry| entry.brick.to_css())
318            .collect::<Vec<_>>()
319            .join("\n")
320    }
321}
322
323/// Builder for constructing a BrickHouse
324pub struct BrickHouseBuilder {
325    name: String,
326    budget_ms: u32,
327    bricks: Vec<(Arc<dyn Brick>, u32)>,
328}
329
330impl std::fmt::Debug for BrickHouseBuilder {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        f.debug_struct("BrickHouseBuilder")
333            .field("name", &self.name)
334            .field("budget_ms", &self.budget_ms)
335            .field("brick_count", &self.bricks.len())
336            .finish()
337    }
338}
339
340impl BrickHouseBuilder {
341    /// Create a new builder with the given house name
342    #[must_use]
343    pub fn new(name: impl Into<String>) -> Self {
344        Self {
345            name: name.into(),
346            budget_ms: 1000, // Default 1 second
347            bricks: Vec::new(),
348        }
349    }
350
351    /// Set the total budget in milliseconds
352    #[must_use]
353    pub fn budget_ms(mut self, ms: u32) -> Self {
354        self.budget_ms = ms;
355        self
356    }
357
358    /// Add a brick with a specific budget allocation
359    #[must_use]
360    pub fn brick(mut self, brick: Arc<dyn Brick>, budget_ms: u32) -> Self {
361        self.bricks.push((brick, budget_ms));
362        self
363    }
364
365    /// Build the BrickHouse
366    ///
367    /// # Errors
368    ///
369    /// Returns an error if the total brick budgets exceed the house budget.
370    pub fn build(self) -> BrickResult<BrickHouse> {
371        let total_brick_budget: u32 = self.bricks.iter().map(|(_, ms)| *ms).sum();
372
373        if total_brick_budget > self.budget_ms {
374            return Err(BrickError::BudgetExceeded(BudgetViolation {
375                brick_name: self.name.clone(),
376                budget: BrickBudget::uniform(self.budget_ms),
377                actual: Duration::from_millis(total_brick_budget as u64),
378                phase: None,
379            }));
380        }
381
382        let mut house = BrickHouse::new(self.name, self.budget_ms);
383        for (brick, budget) in self.bricks {
384            house.add_brick(brick, budget)?;
385        }
386
387        Ok(house)
388    }
389}
390
391/// Jidoka alert for budget violations
392///
393/// This struct captures the context when a brick exceeds its budget,
394/// enabling root cause analysis.
395#[derive(Debug, Clone)]
396pub struct JidokaAlert {
397    /// The brick house that triggered the alert
398    pub house_name: String,
399    /// The specific brick that exceeded budget
400    pub brick_name: String,
401    /// Budget that was exceeded
402    pub budget_ms: u32,
403    /// Actual time taken
404    pub actual_ms: u32,
405    /// Phase where violation occurred
406    pub phase: Option<BrickPhase>,
407    /// Timestamp of alert
408    pub timestamp: std::time::SystemTime,
409    /// Stack trace (if available)
410    pub stack_trace: Option<String>,
411}
412
413impl JidokaAlert {
414    /// Create a new alert from a budget violation
415    #[must_use]
416    pub fn from_violation(house_name: &str, violation: &BudgetViolation) -> Self {
417        Self {
418            house_name: house_name.to_string(),
419            brick_name: violation.brick_name.clone(),
420            budget_ms: violation.budget.total_ms,
421            actual_ms: violation.actual.as_millis() as u32,
422            phase: violation.phase,
423            timestamp: std::time::SystemTime::now(),
424            stack_trace: None,
425        }
426    }
427
428    /// Get the overage percentage
429    #[must_use]
430    pub fn overage_percent(&self) -> f32 {
431        if self.budget_ms == 0 {
432            0.0
433        } else {
434            ((self.actual_ms as f32 / self.budget_ms as f32) - 1.0) * 100.0
435        }
436    }
437}
438
439#[cfg(test)]
440#[allow(clippy::unwrap_used, clippy::expect_used)]
441mod tests {
442    use super::*;
443    use crate::brick::BrickAssertion;
444
445    struct SimpleBrick {
446        name: &'static str,
447    }
448
449    impl Brick for SimpleBrick {
450        fn brick_name(&self) -> &'static str {
451            self.name
452        }
453
454        fn assertions(&self) -> &[BrickAssertion] {
455            &[]
456        }
457
458        fn budget(&self) -> BrickBudget {
459            BrickBudget::uniform(16)
460        }
461
462        fn verify(&self) -> BrickVerification {
463            BrickVerification {
464                passed: vec![],
465                failed: vec![],
466                verification_time: Duration::from_micros(1),
467            }
468        }
469
470        fn to_html(&self) -> String {
471            format!("<div class=\"{}\">{}</div>", self.name, self.name)
472        }
473
474        fn to_css(&self) -> String {
475            format!(".{} {{ display: block; }}", self.name)
476        }
477    }
478
479    #[test]
480    fn test_brick_house_creation() {
481        let house = BrickHouse::new("test-house", 1000);
482        assert_eq!(house.name(), "test-house");
483        assert_eq!(house.budget().total_ms, 1000);
484        assert_eq!(house.brick_count(), 0);
485    }
486
487    #[test]
488    fn test_brick_house_add_brick() {
489        let mut house = BrickHouse::new("test-house", 1000);
490        let brick = Arc::new(SimpleBrick { name: "test" });
491
492        house.add_brick(brick, 100).expect("should add brick");
493        assert_eq!(house.brick_count(), 1);
494        assert_eq!(house.remaining_budget_ms(), 900);
495    }
496
497    #[test]
498    fn test_brick_house_budget_exceeded() {
499        let mut house = BrickHouse::new("test-house", 100);
500        let brick1 = Arc::new(SimpleBrick { name: "brick1" });
501        let brick2 = Arc::new(SimpleBrick { name: "brick2" });
502
503        house.add_brick(brick1, 60).expect("should add first brick");
504        let result = house.add_brick(brick2, 60);
505
506        assert!(result.is_err());
507    }
508
509    #[test]
510    fn test_brick_house_builder() {
511        let brick1 = Arc::new(SimpleBrick { name: "status" });
512        let brick2 = Arc::new(SimpleBrick { name: "content" });
513
514        let house = BrickHouseBuilder::new("app")
515            .budget_ms(1000)
516            .brick(brick1, 100)
517            .brick(brick2, 200)
518            .build()
519            .expect("should build house");
520
521        assert_eq!(house.brick_count(), 2);
522        assert_eq!(house.remaining_budget_ms(), 700);
523    }
524
525    #[test]
526    fn test_brick_house_builder_exceeds_budget() {
527        let brick1 = Arc::new(SimpleBrick { name: "big" });
528        let brick2 = Arc::new(SimpleBrick { name: "bigger" });
529
530        let result = BrickHouseBuilder::new("app")
531            .budget_ms(100)
532            .brick(brick1, 60)
533            .brick(brick2, 60)
534            .build();
535
536        assert!(result.is_err());
537    }
538
539    #[test]
540    fn test_brick_house_render() {
541        let brick = Arc::new(SimpleBrick { name: "test" });
542        let mut house = BrickHouse::new("test-house", 1000);
543        house.add_brick(brick, 100).expect("should add brick");
544
545        let html = house.render().expect("should render");
546        assert!(html.contains("test"));
547    }
548
549    #[test]
550    fn test_jidoka_alert() {
551        let violation = BudgetViolation {
552            brick_name: "slow-brick".into(),
553            budget: BrickBudget::uniform(100),
554            actual: Duration::from_millis(150),
555            phase: Some(BrickPhase::Paint),
556        };
557
558        let alert = JidokaAlert::from_violation("app", &violation);
559        assert_eq!(alert.overage_percent(), 50.0);
560    }
561
562    #[test]
563    fn test_budget_report_within_budget() {
564        let report = BudgetReport {
565            house_name: "test".into(),
566            total_budget_ms: 1000,
567            total_used_ms: 500,
568            brick_timings: HashMap::new(),
569            violations: vec![],
570            timestamp: std::time::SystemTime::now(),
571        };
572        assert!(report.within_budget());
573        assert_eq!(report.utilization(), 50.0);
574    }
575
576    #[test]
577    fn test_budget_report_over_budget() {
578        let violation = BudgetViolation {
579            brick_name: "test".into(),
580            budget: BrickBudget::uniform(100),
581            actual: Duration::from_millis(150),
582            phase: None,
583        };
584        let report = BudgetReport {
585            house_name: "test".into(),
586            total_budget_ms: 1000,
587            total_used_ms: 1500,
588            brick_timings: HashMap::new(),
589            violations: vec![violation],
590            timestamp: std::time::SystemTime::now(),
591        };
592        assert!(!report.within_budget());
593        assert!(!report.violations().is_empty());
594    }
595
596    #[test]
597    fn test_budget_report_zero_budget() {
598        let report = BudgetReport {
599            house_name: "test".into(),
600            total_budget_ms: 0,
601            total_used_ms: 0,
602            brick_timings: HashMap::new(),
603            violations: vec![],
604            timestamp: std::time::SystemTime::now(),
605        };
606        assert_eq!(report.utilization(), 0.0);
607    }
608
609    #[test]
610    fn test_brick_timing() {
611        let timing = BrickTiming {
612            name: "test".into(),
613            budget_ms: 100,
614            used_ms: 50,
615            exceeded: false,
616        };
617        assert_eq!(timing.name, "test");
618        assert!(!timing.exceeded);
619    }
620
621    #[test]
622    fn test_brick_timing_exceeded() {
623        let timing = BrickTiming {
624            name: "slow".into(),
625            budget_ms: 100,
626            used_ms: 150,
627            exceeded: true,
628        };
629        assert!(timing.exceeded);
630    }
631
632    #[test]
633    fn test_brick_house_verify_all() {
634        let brick1 = Arc::new(SimpleBrick { name: "brick1" });
635        let brick2 = Arc::new(SimpleBrick { name: "brick2" });
636        let mut house = BrickHouse::new("test", 1000);
637        house.add_brick(brick1, 100).unwrap();
638        house.add_brick(brick2, 100).unwrap();
639
640        let verifications = house.verify_all();
641        assert_eq!(verifications.len(), 2);
642    }
643
644    #[test]
645    fn test_brick_house_can_render() {
646        let brick = Arc::new(SimpleBrick { name: "test" });
647        let mut house = BrickHouse::new("test", 1000);
648        house.add_brick(brick, 100).unwrap();
649
650        assert!(house.can_render());
651    }
652
653    #[test]
654    fn test_brick_entry_debug() {
655        let brick = Arc::new(SimpleBrick { name: "test" });
656        let entry = BrickEntry {
657            brick,
658            allocated_ms: 100,
659            last_render_time: Some(Duration::from_millis(50)),
660        };
661        let debug_str = format!("{:?}", entry);
662        assert!(debug_str.contains("test"));
663        assert!(debug_str.contains("100"));
664    }
665
666    #[test]
667    fn test_brick_house_to_css() {
668        let brick1 = Arc::new(SimpleBrick { name: "brick1" });
669        let brick2 = Arc::new(SimpleBrick { name: "brick2" });
670        let mut house = BrickHouse::new("test", 1000);
671        house.add_brick(brick1, 100).unwrap();
672        house.add_brick(brick2, 100).unwrap();
673
674        let css = house.to_css();
675        assert!(css.contains("brick1"));
676        assert!(css.contains("brick2"));
677    }
678
679    #[test]
680    fn test_brick_house_last_report_none() {
681        let house = BrickHouse::new("test", 1000);
682        assert!(house.last_report().is_none());
683    }
684
685    #[test]
686    fn test_brick_house_render_populates_report() {
687        let brick = Arc::new(SimpleBrick { name: "test" });
688        let mut house = BrickHouse::new("test-house", 1000);
689        house.add_brick(brick, 100).unwrap();
690
691        let _ = house.render().unwrap();
692
693        let report = house.last_report();
694        assert!(report.is_some());
695        let report = report.unwrap();
696        assert_eq!(report.house_name, "test-house");
697        assert!(report.brick_timings.contains_key("test"));
698    }
699
700    #[test]
701    fn test_budget_report_violations() {
702        let violation = BudgetViolation {
703            brick_name: "slow".into(),
704            budget: BrickBudget::uniform(100),
705            actual: Duration::from_millis(150),
706            phase: Some(BrickPhase::Paint),
707        };
708        let report = BudgetReport {
709            house_name: "test".into(),
710            total_budget_ms: 1000,
711            total_used_ms: 150,
712            brick_timings: HashMap::new(),
713            violations: vec![violation],
714            timestamp: std::time::SystemTime::now(),
715        };
716
717        assert!(!report.within_budget());
718        assert_eq!(report.violations().len(), 1);
719    }
720
721    #[test]
722    fn test_brick_house_builder_debug() {
723        let builder = BrickHouseBuilder::new("test-app").budget_ms(500);
724        let debug_str = format!("{:?}", builder);
725        assert!(debug_str.contains("test-app"));
726        assert!(debug_str.contains("500"));
727    }
728
729    #[test]
730    fn test_brick_house_debug() {
731        let mut house = BrickHouse::new("test-house", 1000);
732        let brick = Arc::new(SimpleBrick { name: "test" });
733        house.add_brick(brick, 100).unwrap();
734
735        let debug_str = format!("{:?}", house);
736        assert!(debug_str.contains("test-house"));
737        assert!(debug_str.contains("1000"));
738    }
739
740    #[test]
741    fn test_jidoka_alert_zero_budget() {
742        let violation = BudgetViolation {
743            brick_name: "test".into(),
744            budget: BrickBudget::uniform(0),
745            actual: Duration::from_millis(10),
746            phase: None,
747        };
748
749        let alert = JidokaAlert::from_violation("house", &violation);
750        assert_eq!(alert.overage_percent(), 0.0);
751    }
752
753    #[test]
754    fn test_jidoka_alert_fields() {
755        let violation = BudgetViolation {
756            brick_name: "slow-brick".into(),
757            budget: BrickBudget::uniform(100),
758            actual: Duration::from_millis(200),
759            phase: Some(BrickPhase::Layout),
760        };
761
762        let alert = JidokaAlert::from_violation("my-house", &violation);
763        assert_eq!(alert.house_name, "my-house");
764        assert_eq!(alert.brick_name, "slow-brick");
765        assert_eq!(alert.budget_ms, 100);
766        assert_eq!(alert.actual_ms, 200);
767        assert!(alert.phase.is_some());
768        assert!(alert.stack_trace.is_none());
769        assert_eq!(alert.overage_percent(), 100.0);
770    }
771
772    #[test]
773    fn test_brick_entry_debug_no_render_time() {
774        let brick = Arc::new(SimpleBrick { name: "test" });
775        let entry = BrickEntry {
776            brick,
777            allocated_ms: 100,
778            last_render_time: None,
779        };
780        let debug_str = format!("{:?}", entry);
781        assert!(debug_str.contains("None"));
782    }
783
784    // Test for a brick that fails verification
785    struct FailingBrick {
786        name: &'static str,
787    }
788
789    impl Brick for FailingBrick {
790        fn brick_name(&self) -> &'static str {
791            self.name
792        }
793
794        fn assertions(&self) -> &[BrickAssertion] {
795            &[BrickAssertion::TextVisible]
796        }
797
798        fn budget(&self) -> BrickBudget {
799            BrickBudget::uniform(16)
800        }
801
802        fn verify(&self) -> BrickVerification {
803            BrickVerification {
804                passed: vec![],
805                failed: vec![(BrickAssertion::TextVisible, "Text not visible".to_string())],
806                verification_time: Duration::from_micros(1),
807            }
808        }
809
810        fn to_html(&self) -> String {
811            format!("<div class=\"{}\">{}</div>", self.name, self.name)
812        }
813
814        fn to_css(&self) -> String {
815            format!(".{} {{ display: block; }}", self.name)
816        }
817
818        fn can_render(&self) -> bool {
819            false
820        }
821    }
822
823    #[test]
824    fn test_brick_house_render_failing_brick() {
825        let brick = Arc::new(FailingBrick { name: "failing" });
826        let mut house = BrickHouse::new("test-house", 1000);
827        house.add_brick(brick, 100).unwrap();
828
829        let result = house.render();
830        assert!(result.is_err());
831    }
832
833    #[test]
834    fn test_brick_house_can_render_with_failing_brick() {
835        let brick = Arc::new(FailingBrick { name: "failing" });
836        let mut house = BrickHouse::new("test-house", 1000);
837        house.add_brick(brick, 100).unwrap();
838
839        assert!(!house.can_render());
840    }
841
842    #[test]
843    fn test_brick_house_multiple_bricks_render() {
844        let brick1 = Arc::new(SimpleBrick { name: "header" });
845        let brick2 = Arc::new(SimpleBrick { name: "content" });
846        let brick3 = Arc::new(SimpleBrick { name: "footer" });
847
848        let mut house = BrickHouse::new("page", 1000);
849        house.add_brick(brick1, 100).unwrap();
850        house.add_brick(brick2, 200).unwrap();
851        house.add_brick(brick3, 100).unwrap();
852
853        let html = house.render().unwrap();
854        assert!(html.contains("header"));
855        assert!(html.contains("content"));
856        assert!(html.contains("footer"));
857    }
858
859    #[test]
860    fn test_brick_house_builder_with_many_bricks() {
861        let brick1 = Arc::new(SimpleBrick { name: "a" });
862        let brick2 = Arc::new(SimpleBrick { name: "b" });
863        let brick3 = Arc::new(SimpleBrick { name: "c" });
864
865        let house = BrickHouseBuilder::new("multi")
866            .budget_ms(1000)
867            .brick(brick1, 100)
868            .brick(brick2, 200)
869            .brick(brick3, 300)
870            .build()
871            .unwrap();
872
873        assert_eq!(house.brick_count(), 3);
874        assert_eq!(house.remaining_budget_ms(), 400);
875    }
876
877    #[test]
878    fn test_budget_report_utilization_100_percent() {
879        let report = BudgetReport {
880            house_name: "test".into(),
881            total_budget_ms: 100,
882            total_used_ms: 100,
883            brick_timings: HashMap::new(),
884            violations: vec![],
885            timestamp: std::time::SystemTime::now(),
886        };
887        assert_eq!(report.utilization(), 100.0);
888    }
889
890    #[test]
891    fn test_budget_report_utilization_over_budget() {
892        let report = BudgetReport {
893            house_name: "test".into(),
894            total_budget_ms: 100,
895            total_used_ms: 200,
896            brick_timings: HashMap::new(),
897            violations: vec![],
898            timestamp: std::time::SystemTime::now(),
899        };
900        assert_eq!(report.utilization(), 200.0);
901        // Over budget but no violations means it's not within budget
902        assert!(!report.within_budget());
903    }
904
905    #[test]
906    fn test_brick_house_empty_render() {
907        let mut house = BrickHouse::new("empty", 1000);
908        let html = house.render().unwrap();
909        assert!(html.is_empty());
910    }
911
912    #[test]
913    fn test_brick_house_empty_css() {
914        let house = BrickHouse::new("empty", 1000);
915        let css = house.to_css();
916        assert!(css.is_empty());
917    }
918
919    #[test]
920    fn test_brick_house_verify_all_empty() {
921        let house = BrickHouse::new("empty", 1000);
922        let verifications = house.verify_all();
923        assert!(verifications.is_empty());
924    }
925}