dynamic_cli/plugin/builtin/sysinfo.rs
1//! Standalone `SysInfoPlugin` (feature-gated).
2//!
3//! Prints basic runtime/OS information — operating system, architecture,
4//! and available parallelism — using only `std`. No new dependency: this
5//! is deliberately a minimal baseline (#45 / DD-025). A richer variant
6//! backed by the `sysinfo` crate is out of scope here; if pursued later,
7//! it stays under this same `sysinfo-plugin` feature flag rather than a
8//! second one, per the uniform feature-flag policy decided in the DD-025
9//! triage.
10//!
11//! Only available with the `sysinfo-plugin` feature.
12
13use crate::context::ExecutionContext;
14use crate::executor::CommandHandler;
15use crate::parser::ParsedArgs;
16use crate::plugin::Plugin;
17use crate::Result;
18
19/// Standalone plugin providing a single `sysinfo` command.
20///
21/// Reports the operating system, CPU architecture, and available
22/// parallelism (`std::thread::available_parallelism()`) — everything
23/// `std` can report without a third-party crate.
24///
25/// # YAML config
26///
27/// ```yaml
28/// commands:
29/// - name: sysinfo
30/// implementation: sysinfo_show
31/// description: "Show runtime/OS information"
32/// required: false
33/// arguments: []
34/// options: []
35/// ```
36///
37/// # Example
38///
39/// ```
40/// use dynamic_cli::plugin::{Plugin, SysInfoPlugin};
41///
42/// let plugin = SysInfoPlugin::new();
43/// assert_eq!(plugin.name(), "sysinfo");
44///
45/// let handlers = plugin.handlers();
46/// assert_eq!(handlers.len(), 1);
47/// assert_eq!(handlers[0].0, "sysinfo_show");
48/// ```
49pub struct SysInfoPlugin;
50
51impl SysInfoPlugin {
52 /// Create a new `SysInfoPlugin`.
53 ///
54 /// # Example
55 ///
56 /// ```
57 /// use dynamic_cli::plugin::{Plugin, SysInfoPlugin};
58 ///
59 /// let plugin = SysInfoPlugin::new();
60 /// assert_eq!(plugin.name(), "sysinfo");
61 /// ```
62 pub fn new() -> Self {
63 Self
64 }
65}
66
67impl Default for SysInfoPlugin {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Plugin for SysInfoPlugin {
74 fn name(&self) -> &str {
75 "sysinfo"
76 }
77
78 fn version(&self) -> &str {
79 env!("CARGO_PKG_VERSION")
80 }
81
82 fn description(&self) -> &str {
83 "Runtime/OS introspection (std-only baseline, feature-gated, #45)"
84 }
85
86 fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
87 vec![("sysinfo_show".to_string(), Box::new(SysInfoShowHandler))]
88 }
89}
90
91/// Handler for `sysinfo_show` — prints OS, architecture, and available
92/// parallelism.
93struct SysInfoShowHandler;
94
95impl CommandHandler for SysInfoShowHandler {
96 fn execute(&self, _ctx: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
97 let parallelism = std::thread::available_parallelism()
98 .map(|n| n.get())
99 .unwrap_or(1);
100
101 println!("OS: {}", std::env::consts::OS);
102 println!("Architecture: {}", std::env::consts::ARCH);
103 println!("Available parallelism: {}", parallelism);
104
105 Ok(())
106 }
107}
108
109// ============================================================================
110// Tests
111// ============================================================================
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::any::Any;
117
118 #[derive(Default)]
119 struct TestContext;
120
121 impl ExecutionContext for TestContext {
122 fn as_any(&self) -> &dyn Any {
123 self
124 }
125 fn as_any_mut(&mut self) -> &mut dyn Any {
126 self
127 }
128 }
129
130 #[test]
131 fn test_sysinfo_plugin_metadata() {
132 let p = SysInfoPlugin::new();
133 assert_eq!(p.name(), "sysinfo");
134 assert!(!p.version().is_empty());
135 assert!(!p.description().is_empty());
136 }
137
138 #[test]
139 // SysInfoPlugin::default() is what's under test here (the Default impl
140 // itself, kept for clippy::new_without_default compliance); rewriting
141 // it to the bare unit-struct literal per clippy's own suggestion would
142 // stop exercising that impl and defeat the test's purpose.
143 #[allow(clippy::default_constructed_unit_structs)]
144 fn test_sysinfo_plugin_default() {
145 let p = SysInfoPlugin::default();
146 assert_eq!(p.name(), "sysinfo");
147 }
148
149 #[test]
150 fn test_sysinfo_plugin_handler_name() {
151 let handlers = SysInfoPlugin::new().handlers();
152 assert_eq!(handlers.len(), 1);
153 assert_eq!(handlers[0].0, "sysinfo_show");
154 }
155
156 #[test]
157 fn test_sysinfo_handler_executes() {
158 let handlers = SysInfoPlugin::new().handlers();
159 let (_, handler) = &handlers[0];
160 let mut ctx = TestContext;
161 assert!(handler
162 .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
163 .is_ok());
164 }
165
166 #[test]
167 fn test_sysinfo_plugin_is_send_sync() {
168 fn assert_send_sync<T: Send + Sync>(_: T) {}
169 assert_send_sync(SysInfoPlugin::new());
170 }
171}