rs_teststand_autodoc/data.rs
1//! Data models for extracted TestStand™ sequences and settings.
2
3use std::collections::BTreeMap;
4
5/// Documentation output profile.
6#[derive(
7 Debug,
8 Clone,
9 Copy,
10 PartialEq,
11 Eq,
12 Default,
13 clap::ValueEnum,
14 serde::Serialize,
15 serde::Deserialize,
16)]
17pub enum Profile {
18 /// Full step tables, variables, custom types, code-module dependencies.
19 #[default]
20 Engineer,
21 /// High-level logic overview with flowchart and condensed step list.
22 Business,
23 /// Standalone station options and search directories report.
24 Station,
25}
26
27impl Profile {
28 /// Parses profile from command line string.
29 #[must_use]
30 pub fn from_str_case_insensitive(s: &str) -> Self {
31 match s.to_lowercase().as_str() {
32 "business" => Self::Business,
33 "station" => Self::Station,
34 _ => Self::Engineer,
35 }
36 }
37}
38
39/// Variable scope filter for documentation output.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum VariableScope {
42 /// Local variables within a sequence.
43 Locals,
44 /// Parameter variables passed to a sequence.
45 Parameters,
46 /// File global variables in a sequence file.
47 FileGlobals,
48 /// Station global variables in the engine.
49 StationGlobals,
50}
51
52impl VariableScope {
53 /// Returns the scope name as a string slice.
54 #[must_use]
55 pub const fn as_str(self) -> &'static str {
56 match self {
57 Self::Locals => "Locals",
58 Self::Parameters => "Parameters",
59 Self::FileGlobals => "FileGlobals",
60 Self::StationGlobals => "StationGlobals",
61 }
62 }
63}
64
65/// Configuration options for the documentation generator.
66#[derive(Debug, Clone)]
67#[allow(
68 clippy::struct_excessive_bools,
69 reason = "CLI generator configuration flags"
70)]
71pub struct ExtractorConfig {
72 /// Report profile style.
73 ///
74 /// A profile is a preset for [`rules`](Self::rules); the renderer reads the
75 /// rules, never the profile, so a caller can start from a preset and change
76 /// one thing.
77 pub profile: Profile,
78 /// What the document includes.
79 pub rules: crate::rules::DocumentRules,
80 /// Document only these sequences, by name.
81 ///
82 /// Empty means every sequence in the file. Naming one or more narrows the
83 /// document to them, which is how a caller documents a single subsequence
84 /// out of a large file without generating the rest.
85 pub only_sequences: Vec<String>,
86 /// Whether to analyze and include process models.
87 pub include_process_models: bool,
88 /// Variable scopes to include in output.
89 pub include_scopes: Vec<VariableScope>,
90 /// Whether to omit steps whose run mode is Skip.
91 pub ignore_skipped: bool,
92 /// Whether to generate Mermaid control-flow diagrams.
93 pub include_flowcharts: bool,
94 /// Whether to append station options report.
95 pub include_station_options: bool,
96 /// Whether to append types report.
97 pub include_types: bool,
98 /// Whether to extract custom types defined in the file.
99 pub include_file_custom_data_types: bool,
100 /// When true, only report types attached to the file.
101 pub types_attached_only: bool,
102 /// Whether to estimate minimum software delays.
103 pub estimate_software_delays: bool,
104 /// Whether to include message details in MessagePopup diagrams.
105 pub detailed_popup_messages: bool,
106 /// Author name for the document header.
107 pub author: String,
108 /// Company name for the document header.
109 pub company: String,
110 /// Author email for the document header.
111 pub email: String,
112 /// Document version for the header.
113 pub version: String,
114 /// Whether to show file paths under sequence titles.
115 pub show_paths: bool,
116 /// Whether to recursively analyze and include called subsequence files.
117 pub recurse_subsequences: bool,
118 /// Maximum recursion depth for subsequence traversal.
119 pub max_depth: usize,
120 /// Path to an optional company logo image.
121 pub company_logo: Option<String>,
122}
123
124impl Default for ExtractorConfig {
125 fn default() -> Self {
126 Self {
127 profile: Profile::Engineer,
128 rules: crate::rules::DocumentRules::for_profile(Profile::Engineer),
129 only_sequences: Vec::new(),
130 include_process_models: false,
131 include_scopes: vec![
132 VariableScope::Locals,
133 VariableScope::Parameters,
134 VariableScope::FileGlobals,
135 VariableScope::StationGlobals,
136 ],
137 ignore_skipped: false,
138 include_flowcharts: true,
139 include_station_options: false,
140 include_types: false,
141 include_file_custom_data_types: false,
142 types_attached_only: true,
143 estimate_software_delays: false,
144 detailed_popup_messages: true,
145 // Empty by default. A document that names an author who did
146 // not write it is worse than one that names nobody.
147 author: String::new(),
148 company: String::new(),
149 email: String::new(),
150 version: "1.0.0".to_owned(),
151 show_paths: false,
152 recurse_subsequences: true,
153 max_depth: 3,
154 company_logo: None,
155 }
156 }
157}
158
159/// A variable (local, parameter, global) name, type, default value, and comment.
160#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
161pub struct Variable {
162 /// Variable identifier name.
163 pub name: String,
164 /// Type display name.
165 pub type_name: String,
166 /// Initial or default value representation.
167 pub default_value: Option<String>,
168 /// Optional documentation comment.
169 pub comment: String,
170}
171
172/// A field within a custom data type.
173#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
174pub struct FieldData {
175 /// Sub-property field name.
176 pub name: String,
177 /// Type display name.
178 pub type_name: String,
179}
180
181/// An enumerator item in an enumeration custom type.
182#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
183pub struct EnumeratorData {
184 /// Enumerator name.
185 pub name: String,
186 /// Integer value.
187 pub value: i64,
188}
189
190/// A custom data type defined or used in a sequence file.
191#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
192pub struct CustomDataType {
193 /// Type definition name.
194 pub name: String,
195 /// Display type string.
196 pub type_display: String,
197 /// List of child fields.
198 pub fields: Vec<FieldData>,
199 /// List of enumerator values if an enum type.
200 pub enumerators: Vec<EnumeratorData>,
201}
202
203/// Test limits and comparison operator for a test step.
204#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
205pub struct Limits {
206 /// Low limit string.
207 pub low: String,
208 /// High limit string.
209 pub high: String,
210 /// Expected target comparison string.
211 pub target: String,
212 /// Comparison operator.
213 pub comp: String,
214 /// Measurement unit.
215 pub unit: String,
216}
217
218/// A single measurement within a multiple limit test step.
219#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
220pub struct MeasurementData {
221 /// Name of the measurement.
222 pub name: String,
223 /// Test limits.
224 pub limits: Limits,
225}
226
227/// Information about a code module called by a step.
228#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
229pub struct ModuleInfo {
230 /// Path to the code module file.
231 pub path: String,
232 /// Adapter technology name.
233 pub adapter_type: String,
234 /// Number of occurrences across the file.
235 pub occurrences: usize,
236 /// The function, method or member called inside the module.
237 ///
238 /// Absent for adapters where the module is itself the callable unit, which
239 /// is the case for LabVIEW: the VI is the function.
240 pub entry_point: Option<String>,
241 /// Additional adapter-specific details.
242 pub extra: BTreeMap<String, String>,
243}
244
245impl ExtractorConfig {
246 /// A configuration carrying the rules a profile stands for.
247 ///
248 /// Prefer this over a struct literal: setting [`profile`](Self::profile) on
249 /// its own leaves [`rules`](Self::rules) at the default, and the renderer
250 /// reads the rules, so the document would not change. Override individual
251 /// rules afterwards:
252 ///
253 /// ```
254 /// use rs_teststand_autodoc::data::{ExtractorConfig, Profile};
255 ///
256 /// let mut config = ExtractorConfig::for_profile(Profile::Business);
257 /// config.rules.step_tables = true;
258 /// ```
259 #[must_use]
260 pub fn for_profile(profile: Profile) -> Self {
261 Self {
262 profile,
263 rules: crate::rules::DocumentRules::for_profile(profile),
264 ..Self::default()
265 }
266 }
267}
268
269/// How a sequence call runs what it calls.
270///
271/// A call normally runs in the caller's thread, but it can start a thread of
272/// its own, start a whole execution, or run on another machine. Each is a
273/// different relationship, and a hierarchy showing them alike would read as a
274/// sequential test when it is not one.
275#[derive(
276 Debug,
277 Clone,
278 Copy,
279 Default,
280 PartialEq,
281 Eq,
282 PartialOrd,
283 Ord,
284 serde::Serialize,
285 serde::Deserialize,
286)]
287pub enum CallKind {
288 /// Runs in the calling thread, which is the ordinary case.
289 #[default]
290 Sequential,
291 /// Runs in a new thread.
292 NewThread,
293 /// Runs as a new execution.
294 NewExecution,
295 /// Runs on another machine.
296 Remote,
297}
298
299impl CallKind {
300 /// Reads the engine's option flags.
301 ///
302 /// From `TS.SData.ThreadOpt`, whose values were read live from a shipped
303 /// example: a plain call reports 0, and a call launching its own execution
304 /// reports 2. The remote bit sits above the low byte.
305 #[must_use]
306 pub const fn from_option(option: i64) -> Self {
307 if option & 0x100 != 0 {
308 return Self::Remote;
309 }
310 match option & 0xff {
311 1 => Self::NewThread,
312 2 => Self::NewExecution,
313 _ => Self::Sequential,
314 }
315 }
316
317 /// How to describe the call, or nothing when it is an ordinary one.
318 #[must_use]
319 pub const fn describe(self) -> Option<&'static str> {
320 match self {
321 Self::Sequential => None,
322 Self::NewThread => Some("in a new thread"),
323 Self::NewExecution => Some("as a new execution"),
324 Self::Remote => Some("on another machine"),
325 }
326 }
327}
328
329/// Extracted data for a single step in a sequence.
330#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
331pub struct StepData {
332 /// Unique step ID.
333 pub id: String,
334 /// Step name.
335 pub name: String,
336 /// Step type identifier.
337 pub step_type: String,
338 /// Adapter key name.
339 pub adapter: String,
340 /// Step description text.
341 pub description: String,
342 /// Step comment text.
343 pub comment: String,
344 /// Precondition expression.
345 pub precondition: String,
346 /// Code module path.
347 pub module_path: String,
348 /// Detailed module info.
349 pub module_info: Option<ModuleInfo>,
350 /// Step limits.
351 pub limits: Limits,
352 /// Per-measurement limits for multiple numeric limit tests.
353 pub measurements: Vec<MeasurementData>,
354 /// Target sequence for SequenceCall steps.
355 pub target_sequence: String,
356 /// How the call runs its target.
357 pub call_kind: CallKind,
358 /// The call names its target through an expression rather than a literal.
359 ///
360 /// An expression is resolved while the sequence runs, so the target cannot
361 /// be followed by reading the file. A hierarchy that silently showed the
362 /// expression text as if it were a sequence name would be claiming a
363 /// certainty it does not have.
364 pub target_by_expression: bool,
365 /// Step expressions dictionary.
366 pub expressions: BTreeMap<String, String>,
367 /// Step settings dictionary.
368 pub step_settings: BTreeMap<String, String>,
369 /// Linked requirement IDs.
370 pub requirements: Vec<String>,
371 /// Run mode string (Normal, Skip, `ForcePass`, `ForceFail`).
372 pub run_mode: String,
373 /// Whether the step was marked as skipped.
374 pub skipped: bool,
375 /// Estimated minimum software delay in seconds.
376 pub estimated_software_delay: Option<f64>,
377}
378
379/// Sequence category for sorting and reporting.
380#[derive(
381 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
382)]
383pub enum SequenceCategory {
384 /// Main entry point or execution entry point.
385 EntryPoint = 0,
386 /// Process model callback.
387 ModelCallback = 1,
388 /// Engine callback.
389 EngineCallback = 2,
390 /// Front-end callback.
391 FrontEndCallback = 3,
392 /// General callback.
393 Callback = 4,
394 /// Ordinary subsequence.
395 Subsequence = 5,
396}
397
398impl SequenceCategory {
399 /// Returns the category display label.
400 #[must_use]
401 pub const fn as_str(self) -> &'static str {
402 match self {
403 Self::EntryPoint => "Entry Point",
404 Self::ModelCallback => "Model Callback",
405 Self::EngineCallback => "Engine Callback",
406 Self::FrontEndCallback => "Front-End Callback",
407 Self::Callback => "Callback",
408 Self::Subsequence => "Subsequence",
409 }
410 }
411}
412
413/// Extracted data for a single sequence.
414#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
415pub struct SequenceData {
416 /// Sequence name.
417 pub name: String,
418 /// Sequence category.
419 pub category: Option<SequenceCategory>,
420 /// Sequence comment.
421 pub comment: String,
422 /// Whether results recording is enabled for the sequence.
423 pub record_results: Option<bool>,
424 /// Sequence failure action (e.g. Goto Cleanup).
425 pub failure_action: Option<String>,
426 /// Linked requirement IDs at sequence level.
427 pub requirements: Vec<String>,
428 /// Sequence variables by scope.
429 pub variables: BTreeMap<String, Vec<Variable>>,
430 /// Step lists grouped by StepGroup name (Setup, Main, Cleanup).
431 pub step_groups: BTreeMap<String, Vec<StepData>>,
432 /// Estimated software delay in seconds.
433 pub estimated_software_delay: Option<f64>,
434}
435
436/// Extracted data for a complete sequence file.
437#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
438pub struct FileData {
439 /// Sequence file name.
440 pub name: String,
441 /// Absolute file path.
442 pub path: String,
443 /// Sequences in the file.
444 pub sequences: Vec<SequenceData>,
445 /// Hierarchy depth.
446 pub depth: usize,
447 /// File globals.
448 pub file_globals: Vec<Variable>,
449 /// Station globals in the engine.
450 pub station_globals: Vec<Variable>,
451 /// File version string.
452 pub file_version: String,
453 /// Path to the process model file.
454 pub model_file: String,
455 /// File load option.
456 pub load_opt: String,
457 /// File unload option.
458 pub unload_opt: String,
459 /// File-level requirement links.
460 pub requirements: Vec<String>,
461 /// Custom data types.
462 pub custom_data_types: Vec<CustomDataType>,
463 /// Estimated software delay in seconds.
464 pub estimated_software_delay: Option<f64>,
465}
466
467/// The order the engine runs step groups in.
468///
469/// Setup, then Main, then Cleanup. Storage is a `BTreeMap`, which orders keys
470/// alphabetically and so hands back Cleanup first; a document built from that
471/// shows the sequence running its teardown before its work.
472pub const STEP_GROUP_EXECUTION_ORDER: [&str; 3] = ["Setup", "Main", "Cleanup"];
473
474impl SequenceData {
475 /// Step groups in the order the engine executes them.
476 ///
477 /// Groups the engine does not name are kept, in map order, after the three
478 /// standard ones, so a custom model still lists everything it has.
479 #[must_use]
480 pub fn step_groups_in_execution_order(&self) -> Vec<(&String, &Vec<StepData>)> {
481 let mut ordered: Vec<(&String, &Vec<StepData>)> = Vec::new();
482 for wanted in STEP_GROUP_EXECUTION_ORDER {
483 if let Some((name, steps)) = self.step_groups.get_key_value(wanted) {
484 ordered.push((name, steps));
485 }
486 }
487 for (name, steps) in &self.step_groups {
488 if !STEP_GROUP_EXECUTION_ORDER.contains(&name.as_str()) {
489 ordered.push((name, steps));
490 }
491 }
492 ordered
493 }
494}
495
496#[cfg(test)]
497mod execution_order_tests {
498 use super::{SequenceData, StepData};
499 use std::collections::BTreeMap;
500
501 fn sequence_with(groups: &[&str]) -> SequenceData {
502 let mut step_groups = BTreeMap::new();
503 for name in groups {
504 step_groups.insert((*name).to_owned(), vec![StepData::default()]);
505 }
506 SequenceData {
507 step_groups,
508 ..SequenceData::default()
509 }
510 }
511
512 #[test]
513 fn groups_come_back_in_the_order_the_engine_runs_them() {
514 // Alphabetically this is Cleanup, Main, Setup. A document built in that
515 // order shows a sequence tearing down before it does its work.
516 let sequence = sequence_with(&["Cleanup", "Main", "Setup"]);
517 let order: Vec<&str> = sequence
518 .step_groups_in_execution_order()
519 .iter()
520 .map(|(name, _)| name.as_str())
521 .collect();
522 assert_eq!(order, ["Setup", "Main", "Cleanup"]);
523 }
524
525 #[test]
526 fn a_missing_group_is_skipped_rather_than_invented() {
527 let sequence = sequence_with(&["Cleanup", "Main"]);
528 let order: Vec<&str> = sequence
529 .step_groups_in_execution_order()
530 .iter()
531 .map(|(name, _)| name.as_str())
532 .collect();
533 assert_eq!(order, ["Main", "Cleanup"]);
534 }
535
536 #[test]
537 fn a_custom_group_is_kept_after_the_standard_three() {
538 // Custom models may name their own groups. Dropping them would silently
539 // lose steps from the document.
540 let sequence = sequence_with(&["Main", "Setup", "Diagnostics"]);
541 let order: Vec<&str> = sequence
542 .step_groups_in_execution_order()
543 .iter()
544 .map(|(name, _)| name.as_str())
545 .collect();
546 assert_eq!(order, ["Setup", "Main", "Diagnostics"]);
547 }
548}