Skip to main content

io_stress/
parser.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::workloads::{
6    BURST_IO_PREALLOC_SIZE, RandomArgs, SequentialArgs, TransferArgs, WorkloadSubcommand,
7    get_effective_op_size,
8};
9use anyhow::Result;
10use argh::FromArgs;
11
12#[derive(FromArgs, Debug)]
13/// Dynamic Storage IO Stressor Harness
14struct TopLevelArgs {
15    /// duration to run workloads in seconds (global, default: 10s)
16    #[argh(option, default = "10")]
17    duration_secs: u64,
18
19    #[argh(subcommand)]
20    subcommand: WorkloadSubcommand,
21}
22
23#[derive(FromArgs, Debug)]
24/// Fake parent for standalone subcommand parsing
25struct FakeParent {
26    #[argh(subcommand)]
27    subcommand: WorkloadSubcommand,
28}
29
30pub fn parse_chained_workloads(args: &[String]) -> Result<(u64, Vec<WorkloadSubcommand>)> {
31    if args.is_empty() || args.len() == 1 {
32        anyhow::bail!(
33            "Error: No workload subcommand specified! (e.g. 'fx test io-stress-test -- random')"
34        );
35    }
36
37    let chunks: Vec<&[String]> =
38        args[1..].split(|arg| arg == "+").filter(|chunk| !chunk.is_empty()).collect();
39
40    if chunks.is_empty() {
41        anyhow::bail!(
42            "Error: No workload subcommand specified! (e.g. 'fx test io-stress-test -- random')"
43        );
44    }
45
46    let first_refs: Vec<&str> = chunks[0].iter().map(|s| s.as_str()).collect();
47    let top_args = TopLevelArgs::from_args(&["io_stress"], &first_refs)
48        .map_err(|err| anyhow::anyhow!("Failed to parse first subcommand: {:?}", err))?;
49
50    let duration_secs = top_args.duration_secs;
51    let mut subcommands = vec![top_args.subcommand];
52
53    for chunk in &chunks[1..] {
54        let refs: Vec<&str> = chunk.iter().map(|s| s.as_str()).collect();
55        let parent = FakeParent::from_args(&["workload"], &refs)
56            .map_err(|err| anyhow::anyhow!("Failed to parse chained subcommand: {:?}", err))?;
57        subcommands.push(parent.subcommand);
58    }
59
60    Ok((duration_secs, subcommands))
61}
62
63pub fn validate_workloads(subcommands: &[WorkloadSubcommand]) -> Result<()> {
64    for sub in subcommands {
65        match sub {
66            WorkloadSubcommand::Random(RandomArgs { op_size_bytes, file_size_bytes, .. })
67            | WorkloadSubcommand::Sequential(SequentialArgs {
68                op_size_bytes,
69                file_size_bytes,
70                ..
71            })
72            | WorkloadSubcommand::Transfer(TransferArgs {
73                op_size_bytes, file_size_bytes, ..
74            }) => {
75                let op_size = get_effective_op_size(*op_size_bytes);
76                if op_size > *file_size_bytes {
77                    anyhow::bail!(
78                        "Error: {} operation size ({} B) cannot exceed file size ({} B)!",
79                        sub.name(),
80                        op_size,
81                        file_size_bytes
82                    );
83                }
84            }
85            WorkloadSubcommand::Burst(args) => {
86                let op_size = get_effective_op_size(args.op_size_bytes);
87                if op_size > BURST_IO_PREALLOC_SIZE {
88                    anyhow::bail!(
89                        "Error: Burst op size ({} B) cannot exceed pre-allocated size ({} B)!",
90                        op_size,
91                        BURST_IO_PREALLOC_SIZE
92                    );
93                }
94            }
95        }
96    }
97    Ok(())
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_parse_chained_workloads_single() {
106        let args = vec![
107            "io_stress".to_string(),
108            "random".to_string(),
109            "--op-size-bytes".to_string(),
110            "1024".to_string(),
111        ];
112        let (duration, subcmds) = parse_chained_workloads(&args).unwrap();
113        assert_eq!(duration, 10);
114        assert_eq!(subcmds.len(), 1);
115        match &subcmds[0] {
116            WorkloadSubcommand::Random(a) => assert_eq!(a.op_size_bytes, 1024),
117            _ => panic!("Expected Random subcommand"),
118        }
119    }
120
121    #[test]
122    fn test_parse_chained_workloads_chained() {
123        let args = vec![
124            "io_stress".to_string(),
125            "--duration-secs".to_string(),
126            "20".to_string(),
127            "random".to_string(),
128            "+".to_string(),
129            "sequential".to_string(),
130            "--op-size-bytes".to_string(),
131            "2048".to_string(),
132        ];
133        let (duration, subcmds) = parse_chained_workloads(&args).unwrap();
134        assert_eq!(duration, 20);
135        assert_eq!(subcmds.len(), 2);
136        match &subcmds[0] {
137            WorkloadSubcommand::Random(_) => {}
138            _ => panic!("Expected Random subcommand"),
139        }
140        match &subcmds[1] {
141            WorkloadSubcommand::Sequential(a) => assert_eq!(a.op_size_bytes, 2048),
142            _ => panic!("Expected Sequential subcommand"),
143        }
144    }
145
146    #[test]
147    fn test_parse_chained_workloads_invalid() {
148        let args = vec!["io_stress".to_string()];
149        assert!(parse_chained_workloads(&args).is_err());
150
151        let args = vec!["io_stress".to_string(), "+".to_string()];
152        assert!(parse_chained_workloads(&args).is_err());
153    }
154}