Skip to main content

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}