Skip to main content
Glama
property_tests.rs10.6 kB
//! Property-based tests for element detection. //! //! Uses proptest to generate random inputs and verify detector invariants. use proptest::prelude::*; use std::sync::Arc; use terminal_mcp_core::{Dimensions, Position}; use terminal_mcp_detector::{ BorderDetector, ButtonDetector, CheckboxDetector, DetectionContext, DetectionPipeline, ElementDetector, InputDetector, MenuDetector, ProgressDetector, StatusBarDetector, TableDetector, }; use terminal_mcp_emulator::{Grid, Parser}; /// Generate a random grid size within reasonable bounds. fn grid_dimensions() -> impl Strategy<Value = (u16, u16)> { (10u16..100, 40u16..200) } /// Generate random alphanumeric text for button labels. fn button_label() -> impl Strategy<Value = String> { "[A-Za-z0-9 ]{1,15}".prop_map(|s| s.trim().to_string()) } /// Generate a random button pattern. fn button_text() -> impl Strategy<Value = String> { prop_oneof![ button_label().prop_map(|l| format!("[ {} ]", l)), button_label().prop_map(|l| format!("[{}]", l)), button_label().prop_map(|l| format!("< {} >", l)), button_label().prop_map(|l| format!("( {} )", l)), ] } /// Create a grid from text content. fn create_grid_from_text(rows: u16, cols: u16, text: &str) -> Grid { let grid = Grid::new(Dimensions::new(rows, cols)); let mut parser = Parser::new(grid); parser.process(text.as_bytes()); parser.into_grid() } proptest! { /// Detectors should never panic on any grid size. #[test] fn detectors_never_panic_on_any_grid_size((rows, cols) in grid_dimensions()) { let grid = Grid::new(Dimensions::new(rows, cols)); let context = DetectionContext::new(Position::origin()); // Test all detectors let detectors: Vec<Box<dyn ElementDetector>> = vec![ Box::new(BorderDetector::new()), Box::new(MenuDetector::new()), Box::new(TableDetector::new()), Box::new(InputDetector::new()), Box::new(ButtonDetector::new()), Box::new(ProgressDetector::new()), Box::new(CheckboxDetector::new()), Box::new(StatusBarDetector::new()), ]; for detector in &detectors { let _ = detector.detect(&grid, &context); } } /// Detection pipeline should never panic on any grid size. #[test] fn pipeline_never_panics((rows, cols) in grid_dimensions()) { let grid = Grid::new(Dimensions::new(rows, cols)); let mut pipeline = DetectionPipeline::new(); pipeline.add_detector(Arc::new(BorderDetector::new())); pipeline.add_detector(Arc::new(MenuDetector::new())); pipeline.add_detector(Arc::new(TableDetector::new())); pipeline.add_detector(Arc::new(InputDetector::new())); pipeline.add_detector(Arc::new(ButtonDetector::new())); pipeline.add_detector(Arc::new(ProgressDetector::new())); pipeline.add_detector(Arc::new(CheckboxDetector::new())); pipeline.add_detector(Arc::new(StatusBarDetector::new())); let cursor = Position::origin(); let _ = pipeline.detect(&grid, cursor); } /// Detectors should never panic with any cursor position. #[test] fn detectors_handle_any_cursor_position( (rows, cols) in grid_dimensions(), cursor_row in 0u16..200, cursor_col in 0u16..400 ) { let grid = Grid::new(Dimensions::new(rows, cols)); // Cursor might be out of bounds - detectors should handle this let context = DetectionContext::new(Position::new(cursor_row, cursor_col)); let detector = ButtonDetector::new(); let _ = detector.detect(&grid, &context); } /// Button detector should find valid button patterns. #[test] fn button_detector_finds_valid_buttons(button_text in button_text()) { let grid = create_grid_from_text(10, 80, &format!("{}\r\n", button_text)); let context = DetectionContext::new(Position::origin()); let detector = ButtonDetector::new(); let detected = detector.detect(&grid, &context); // Should find at least one button (unless label was empty after trimming) // Empty labels are intentionally rejected let label_text = button_text .trim_start_matches(&['[', '<', '(', ' '][..]) .trim_end_matches(&[']', '>', ')', ' '][..]) .trim(); if !label_text.is_empty() { prop_assert!(!detected.is_empty(), "Button should be detected for: {}", button_text); } } /// Detected elements should have valid bounds within grid. #[test] fn detected_elements_have_valid_bounds((rows, cols) in grid_dimensions()) { let text = "[ OK ] [ Cancel ]\r\n[X] Checkbox\r\nProgress: ████████░░░░ 66%\r\n"; let grid = create_grid_from_text(rows, cols, text); let mut pipeline = DetectionPipeline::new(); pipeline.add_detector(Arc::new(ButtonDetector::new())); pipeline.add_detector(Arc::new(CheckboxDetector::new())); pipeline.add_detector(Arc::new(ProgressDetector::new())); let elements = pipeline.detect(&grid, Position::origin()); for elem in &elements { let bounds = &elem.bounds; // Bounds should be within grid prop_assert!(bounds.row < rows, "Row {} >= grid rows {}", bounds.row, rows); prop_assert!(bounds.col < cols, "Col {} >= grid cols {}", bounds.col, cols); prop_assert!(bounds.row + bounds.height <= rows, "Row {} + height {} > rows {}", bounds.row, bounds.height, rows); prop_assert!(bounds.col + bounds.width <= cols, "Col {} + width {} > cols {}", bounds.col, bounds.width, cols); } } /// Checkbox detector should handle random checkbox patterns. #[test] fn checkbox_detector_finds_checkboxes( checked in prop::bool::ANY, label in "[A-Za-z ]{1,20}" ) { let marker = if checked { "X" } else { " " }; let text = format!("[{}] {}\r\n", marker, label); let grid = create_grid_from_text(10, 80, &text); let context = DetectionContext::new(Position::origin()); let detector = CheckboxDetector::new(); let detected = detector.detect(&grid, &context); // Should find the checkbox (label must not be empty) let trimmed_label = label.trim(); if !trimmed_label.is_empty() { prop_assert!(!detected.is_empty(), "Checkbox should be detected"); } } /// Progress detector should handle various percentage values. #[test] fn progress_detector_handles_percentages(percent in 0u8..=100) { let filled = (percent as usize * 20) / 100; let empty = 20 - filled; let bar = format!( "[{}{}] {}%\r\n", "█".repeat(filled), "░".repeat(empty), percent ); let grid = create_grid_from_text(10, 80, &bar); let context = DetectionContext::new(Position::origin()); let detector = ProgressDetector::new(); let _ = detector.detect(&grid, &context); // Just ensure no panic - detection accuracy is tested elsewhere } /// Safe_slice should never panic on any input. #[test] fn safe_slice_helper_never_panics( s in ".*", start in 0usize..1000, end in 0usize..1000 ) { // Simulate the safe_slice function fn safe_slice(s: &str, start: usize, end: usize) -> Option<&str> { if start <= end && end <= s.len() && s.is_char_boundary(start) && s.is_char_boundary(end) { Some(&s[start..end]) } else { None } } // Should never panic let _ = safe_slice(&s, start, end); } /// Unicode strings should be handled safely. #[test] fn detectors_handle_unicode_safely(text in "[^\x00-\x1f]{0,100}") { let grid = create_grid_from_text(24, 80, &format!("{}\r\n", text)); let context = DetectionContext::new(Position::origin()); let detector = ButtonDetector::new(); let _ = detector.detect(&grid, &context); let detector = CheckboxDetector::new(); let _ = detector.detect(&grid, &context); let detector = ProgressDetector::new(); let _ = detector.detect(&grid, &context); } /// Grid text extraction should never panic. #[test] fn grid_text_extraction_never_panics((rows, cols) in grid_dimensions()) { let grid = Grid::new(Dimensions::new(rows, cols)); // Full grid extraction let bounds = terminal_mcp_core::Bounds::new(0, 0, cols, rows); let _ = grid.extract_text(&bounds); // Random region extraction (may be out of bounds) let bounds = terminal_mcp_core::Bounds::new( rows / 2, cols / 2, cols, // Intentionally larger than remaining rows, // Intentionally larger than remaining ); let _ = grid.extract_text(&bounds); } } #[cfg(test)] mod additional_tests { use super::*; #[test] fn test_empty_grid() { let grid = Grid::new(Dimensions::new(1, 1)); let context = DetectionContext::new(Position::origin()); let detector = ButtonDetector::new(); let detected = detector.detect(&grid, &context); assert!(detected.is_empty()); } #[test] fn test_minimal_grid() { let grid = Grid::new(Dimensions::new(5, 10)); let mut pipeline = DetectionPipeline::new(); pipeline.add_detector(Arc::new(BorderDetector::new())); pipeline.add_detector(Arc::new(ButtonDetector::new())); let elements = pipeline.detect(&grid, Position::origin()); // Empty grid should have no elements assert!(elements.is_empty()); } #[test] fn test_box_drawing_characters() { // Test with various box-drawing Unicode characters let text = "┌────────────────────┐\r\n│ Test Content │\r\n└────────────────────┘\r\n"; let grid = create_grid_from_text(24, 80, text); let mut pipeline = DetectionPipeline::new(); pipeline.add_detector(Arc::new(BorderDetector::new())); pipeline.add_detector(Arc::new(ButtonDetector::new())); // Should not panic let _ = pipeline.detect(&grid, Position::origin()); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aybelatchane/mcp-server-terminal'

If you have feedback or need assistance with the MCP directory API, please join our Discord server