• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jzombie / oxdock-rs / 20479548851

24 Dec 2025 06:01AM UTC coverage: 83.897% (+0.2%) from 83.742%
20479548851

Pull #70

github

web-flow
Merge 2e55fb723 into 13cbac824
Pull Request #70: Unify string interpolation

722 of 830 new or added lines in 16 files covered. (86.99%)

23 existing lines in 5 files now uncovered.

6351 of 7570 relevant lines covered (83.9%)

169.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.35
/crates/internal/oxdock-parser/src/ast.rs
1
use std::collections::HashMap;
2

3
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
4
pub enum Command {
5
    Workdir,
6
    Workspace,
7
    Env,
8
    Echo,
9
    Run,
10
    RunBg,
11
    Copy,
12
    CaptureToFile,
13
    WithIo,
14
    CopyGit,
15
    HashSha256,
16
    Symlink,
17
    Mkdir,
18
    Ls,
19
    Cwd,
20
    Cat,
21
    Write,
22
    Exit,
23
}
24

25
pub const COMMANDS: &[Command] = &[
26
    Command::Workdir,
27
    Command::Workspace,
28
    Command::Env,
29
    Command::Echo,
30
    Command::Run,
31
    Command::RunBg,
32
    Command::Copy,
33
    Command::CaptureToFile,
34
    Command::WithIo,
35
    Command::CopyGit,
36
    Command::HashSha256,
37
    Command::Symlink,
38
    Command::Mkdir,
39
    Command::Ls,
40
    Command::Cwd,
41
    Command::Cat,
42
    Command::Write,
43
    Command::Exit,
44
];
45

46
impl Command {
47
    pub const fn as_str(self) -> &'static str {
42✔
48
        match self {
42✔
49
            Command::Workdir => "WORKDIR",
6✔
50
            Command::Workspace => "WORKSPACE",
6✔
51
            Command::Env => "ENV",
6✔
52
            Command::Echo => "ECHO",
6✔
53
            Command::Run => "RUN",
6✔
54
            Command::RunBg => "RUN_BG",
1✔
55
            Command::Copy => "COPY",
1✔
56
            Command::CaptureToFile => "CAPTURE_TO_FILE",
1✔
57
            Command::WithIo => "WITH_IO",
1✔
58
            Command::CopyGit => "COPY_GIT",
1✔
59
            Command::HashSha256 => "HASH_SHA256",
1✔
60
            Command::Symlink => "SYMLINK",
1✔
61
            Command::Mkdir => "MKDIR",
1✔
62
            Command::Ls => "LS",
1✔
63
            Command::Cwd => "CWD",
1✔
64
            Command::Cat => "CAT",
1✔
65
            Command::Write => "WRITE",
1✔
66
            Command::Exit => "EXIT",
×
67
        }
68
    }
42✔
69

70
    pub const fn expects_inner_command(self) -> bool {
681✔
71
        matches!(self, Command::CaptureToFile | Command::WithIo)
681✔
72
    }
681✔
73

74
    pub fn parse(s: &str) -> Option<Self> {
2,195✔
75
        match s {
2,195✔
76
            "WORKDIR" => Some(Command::Workdir),
2,195✔
77
            "WORKSPACE" => Some(Command::Workspace),
2,140✔
78
            "ENV" => Some(Command::Env),
2,083✔
79
            "ECHO" => Some(Command::Echo),
1,999✔
80
            "RUN" => Some(Command::Run),
1,953✔
81
            "RUN_BG" => Some(Command::RunBg),
1,789✔
82
            "COPY" => Some(Command::Copy),
1,719✔
83
            "CAPTURE_TO_FILE" => Some(Command::CaptureToFile),
1,683✔
84
            "WITH_IO" => Some(Command::WithIo),
1,549✔
85
            "COPY_GIT" => Some(Command::CopyGit),
1,549✔
86
            "HASH_SHA256" => Some(Command::HashSha256),
1,505✔
87
            "SYMLINK" => Some(Command::Symlink),
1,469✔
88
            "MKDIR" => Some(Command::Mkdir),
1,414✔
89
            "LS" => Some(Command::Ls),
1,379✔
90
            "CWD" => Some(Command::Cwd),
1,319✔
91
            "CAT" => Some(Command::Cat),
1,285✔
92
            "WRITE" => Some(Command::Write),
1,250✔
93
            "EXIT" => Some(Command::Exit),
1,166✔
94
            _ => None,
1,136✔
95
        }
96
    }
2,195✔
97
}
98

99
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
100
pub enum PlatformGuard {
101
    Unix,
102
    Windows,
103
    Macos,
104
    Linux,
105
}
106

107
#[derive(Debug, Clone, Eq, PartialEq)]
108
pub enum Guard {
109
    Platform {
110
        target: PlatformGuard,
111
        invert: bool,
112
    },
113
    EnvExists {
114
        key: String,
115
        invert: bool,
116
    },
117
    EnvEquals {
118
        key: String,
119
        value: String,
120
        invert: bool,
121
    },
122
}
123

124
#[derive(Debug, Clone, Eq, PartialEq)]
125
pub struct TemplateString(pub String);
126

127
impl From<String> for TemplateString {
128
    fn from(s: String) -> Self {
1,418✔
129
        TemplateString(s)
1,418✔
130
    }
1,418✔
131
}
132

133
impl From<&str> for TemplateString {
134
    fn from(s: &str) -> Self {
107✔
135
        TemplateString(s.to_string())
107✔
136
    }
107✔
137
}
138

139
impl std::fmt::Display for TemplateString {
NEW
140
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
NEW
141
        write!(f, "{}", self.0)
×
NEW
142
    }
×
143
}
144

145
impl AsRef<str> for TemplateString {
NEW
146
    fn as_ref(&self) -> &str {
×
NEW
147
        &self.0
×
NEW
148
    }
×
149
}
150

151
impl PartialEq<str> for TemplateString {
152
    fn eq(&self, other: &str) -> bool {
21✔
153
        self.0 == other
21✔
154
    }
21✔
155
}
156

157
impl PartialEq<&str> for TemplateString {
NEW
158
    fn eq(&self, other: &&str) -> bool {
×
NEW
159
        self.0 == *other
×
NEW
160
    }
×
161
}
162

163
impl std::ops::Deref for TemplateString {
164
    type Target = str;
165

166
    fn deref(&self) -> &Self::Target {
326✔
167
        &self.0
326✔
168
    }
326✔
169
}
170

171
#[derive(Debug, Clone, Eq, PartialEq)]
172
pub enum StepKind {
173
    Workdir(TemplateString),
174
    Workspace(WorkspaceTarget),
175
    Env {
176
        key: String,
177
        value: TemplateString,
178
    },
179
    Run(TemplateString),
180
    Echo(TemplateString),
181
    RunBg(TemplateString),
182
    Copy {
183
        from: TemplateString,
184
        to: TemplateString,
185
    },
186
    Symlink {
187
        from: TemplateString,
188
        to: TemplateString,
189
    },
190
    Mkdir(TemplateString),
191
    Ls(Option<TemplateString>),
192
    Cwd,
193
    Cat(Option<TemplateString>),
194
    Write {
195
        path: TemplateString,
196
        contents: TemplateString,
197
    },
198
    CaptureToFile {
199
        path: TemplateString,
200
        cmd: Box<StepKind>,
201
    },
202
    WithIo {
203
        streams: Vec<String>,
204
        cmd: Box<StepKind>,
205
    },
206
    CopyGit {
207
        rev: TemplateString,
208
        from: TemplateString,
209
        to: TemplateString,
210
        include_dirty: bool,
211
    },
212
    HashSha256 {
213
        path: TemplateString,
214
    },
215
    Exit(i32),
216
}
217

218
#[derive(Debug, Clone, Eq, PartialEq)]
219
pub struct Step {
220
    pub guards: Vec<Vec<Guard>>,
221
    pub kind: StepKind,
222
    pub scope_enter: usize,
223
    pub scope_exit: usize,
224
}
225

226
#[derive(Debug, Clone, Eq, PartialEq)]
227
pub enum WorkspaceTarget {
228
    Snapshot,
229
    Local,
230
}
231

232
fn platform_matches(target: PlatformGuard) -> bool {
1✔
233
    #[allow(clippy::disallowed_macros)]
234
    match target {
1✔
235
        PlatformGuard::Unix => cfg!(unix),
1✔
236
        PlatformGuard::Windows => cfg!(windows),
×
237
        PlatformGuard::Macos => cfg!(target_os = "macos"),
×
238
        PlatformGuard::Linux => cfg!(target_os = "linux"),
×
239
    }
240
}
1✔
241

242
pub fn guard_allows(guard: &Guard, script_envs: &HashMap<String, String>) -> bool {
25✔
243
    match guard {
25✔
244
        Guard::Platform { target, invert } => {
1✔
245
            let res = platform_matches(*target);
1✔
246
            if *invert { !res } else { res }
1✔
247
        }
248
        Guard::EnvExists { key, invert } => {
14✔
249
            let res = script_envs
14✔
250
                .get(key)
14✔
251
                .cloned()
14✔
252
                .or_else(|| std::env::var(key).ok())
14✔
253
                .map(|v| !v.is_empty())
14✔
254
                .unwrap_or(false);
14✔
255
            if *invert { !res } else { res }
14✔
256
        }
257
        Guard::EnvEquals { key, value, invert } => {
10✔
258
            let res = script_envs
10✔
259
                .get(key)
10✔
260
                .cloned()
10✔
261
                .or_else(|| std::env::var(key).ok())
10✔
262
                .map(|v| v == *value)
10✔
263
                .unwrap_or(false);
10✔
264
            if *invert { !res } else { res }
10✔
265
        }
266
    }
267
}
25✔
268

269
pub fn guard_group_allows(group: &[Guard], script_envs: &HashMap<String, String>) -> bool {
21✔
270
    group.iter().all(|g| guard_allows(g, script_envs))
23✔
271
}
21✔
272

273
pub fn guards_allow_any(groups: &[Vec<Guard>], script_envs: &HashMap<String, String>) -> bool {
168✔
274
    if groups.is_empty() {
168✔
275
        return true;
149✔
276
    }
19✔
277
    groups.iter().any(|g| guard_group_allows(g, script_envs))
21✔
278
}
168✔
279

280
use std::fmt;
281

282
impl fmt::Display for PlatformGuard {
283
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64✔
284
        match self {
64✔
285
            PlatformGuard::Unix => write!(f, "unix"),
15✔
286
            PlatformGuard::Windows => write!(f, "windows"),
21✔
287
            PlatformGuard::Macos => write!(f, "macos"),
20✔
288
            PlatformGuard::Linux => write!(f, "linux"),
8✔
289
        }
290
    }
64✔
291
}
292

293
impl fmt::Display for Guard {
294
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196✔
295
        match self {
196✔
296
            Guard::Platform { target, invert } => {
64✔
297
                if *invert {
64✔
298
                    write!(f, "!platform:{}", target)
32✔
299
                } else {
300
                    write!(f, "platform:{}", target)
32✔
301
                }
302
            }
303
            Guard::EnvExists { key, invert } => {
68✔
304
                if *invert {
68✔
305
                    write!(f, "!")?
38✔
306
                }
30✔
307
                write!(f, "env:{}", key)
68✔
308
            }
309
            Guard::EnvEquals { key, value, invert } => {
64✔
310
                if *invert {
64✔
311
                    write!(f, "env:{}!={}", key, value)
38✔
312
                } else {
313
                    write!(f, "env:{}=={}", key, value)
26✔
314
                }
315
            }
316
        }
317
    }
196✔
318
}
319

320
impl fmt::Display for WorkspaceTarget {
321
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19✔
322
        match self {
19✔
323
            WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
8✔
324
            WorkspaceTarget::Local => write!(f, "LOCAL"),
11✔
325
        }
326
    }
19✔
327
}
328

329
fn quote_arg(s: &str) -> String {
222✔
330
    // Strict quoting to avoid parser ambiguity, especially with CAPTURE_TO_FILE command
331
    // where unquoted args followed by run_args can be consumed greedily.
332
    // Also quote if it starts with a digit to avoid invalid Rust tokens (e.g. 0o8) in macros.
333
    let is_safe = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1,737✔
334
        && !s.starts_with(|c: char| c.is_ascii_digit());
62✔
335
    if is_safe && !s.is_empty() {
222✔
336
        s.to_string()
54✔
337
    } else {
338
        format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
168✔
339
    }
340
}
222✔
341

342
fn quote_msg(s: &str) -> String {
34✔
343
    // Strict quoting to ensure round-trip stability through TokenStream (macro input).
344
    // The macro input reconstructor removes spaces around "sticky" characters (/-.:=)
345
    // and collapses multiple spaces, so we must quote strings containing them.
346
    // We also quote strings with spaces to be safe, as TokenStream does not preserve whitespace.
347
    // Also quote if it starts with a digit to avoid invalid Rust tokens.
348
    let is_safe = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
169✔
349
        && !s.starts_with(|c: char| c.is_ascii_digit());
8✔
350

351
    if is_safe && !s.is_empty() {
34✔
352
        s.to_string()
7✔
353
    } else {
354
        format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
27✔
355
    }
356
}
34✔
357

358
fn quote_run(s: &str) -> String {
57✔
359
    // For RUN commands, we want to preserve the raw string as much as possible.
360
    // However, to ensure round-trip stability through TokenStream (macro input),
361
    // we must ensure that the generated string is a valid sequence of Rust tokens.
362
    // Invalid tokens (like 0o8) must be quoted.
363
    // Also, sticky characters (like -) can merge with previous tokens in macro input,
364
    // so we quote words starting with them to ensure separation.
365

366
    let force_full_quote = s.is_empty()
57✔
367
        || s.chars().any(|c| c == ';' || c == '\n' || c == '\r')
717✔
368
        || s.contains("//")
57✔
369
        || s.contains("/*");
57✔
370

371
    if force_full_quote {
57✔
372
        return format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
×
373
    }
57✔
374

375
    s.split(' ')
57✔
376
        .map(|word| {
124✔
377
            let needs_quote = word.starts_with(|c: char| c.is_ascii_digit())
124✔
378
                || word.starts_with(['/', '.', '-', ':', '=']);
105✔
379
            if needs_quote {
124✔
380
                format!("\"{}\"", word.replace('\\', "\\\\").replace('"', "\\\""))
25✔
381
            } else {
382
                word.to_string()
99✔
383
            }
384
        })
124✔
385
        .collect::<Vec<_>>()
57✔
386
        .join(" ")
57✔
387
}
57✔
388

389
impl fmt::Display for StepKind {
390
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300✔
391
        match self {
300✔
392
            StepKind::Workdir(arg) => write!(f, "WORKDIR {}", quote_arg(arg)),
11✔
393
            StepKind::Workspace(target) => write!(f, "WORKSPACE {}", target),
19✔
394
            StepKind::Env { key, value } => write!(f, "ENV {}={}", key, quote_arg(value)),
20✔
395
            StepKind::Run(cmd) => write!(f, "RUN {}", quote_run(cmd)),
42✔
396
            StepKind::Echo(msg) => write!(f, "ECHO {}", quote_msg(msg)),
18✔
397
            StepKind::RunBg(cmd) => write!(f, "RUN_BG {}", quote_run(cmd)),
15✔
398
            StepKind::Copy { from, to } => write!(f, "COPY {} {}", quote_arg(from), quote_arg(to)),
15✔
399
            StepKind::Symlink { from, to } => {
17✔
400
                write!(f, "SYMLINK {} {}", quote_arg(from), quote_arg(to))
17✔
401
            }
402
            StepKind::Mkdir(arg) => write!(f, "MKDIR {}", quote_arg(arg)),
11✔
403
            StepKind::Ls(arg) => {
21✔
404
                write!(f, "LS")?;
21✔
405
                if let Some(a) = arg {
21✔
406
                    write!(f, " {}", quote_arg(a))?;
13✔
407
                }
8✔
408
                Ok(())
21✔
409
            }
410
            StepKind::Cwd => write!(f, "CWD"),
17✔
411
            StepKind::Cat(arg) => {
17✔
412
                write!(f, "CAT")?;
17✔
413
                if let Some(a) = arg {
17✔
414
                    write!(f, " {}", quote_arg(a))?;
9✔
415
                }
8✔
416
                Ok(())
17✔
417
            }
418
            StepKind::Write { path, contents } => {
16✔
419
                write!(f, "WRITE {} {}", quote_arg(path), quote_msg(contents))
16✔
420
            }
421
            StepKind::CaptureToFile { path, cmd } => {
13✔
422
                write!(f, "CAPTURE_TO_FILE {} {}", quote_arg(path), cmd)
13✔
423
            }
424
            StepKind::WithIo { streams, cmd } => {
×
425
                write!(f, "WITH_IO [{}] {}", streams.join(", "), cmd)
×
426
            }
427
            StepKind::CopyGit {
428
                rev,
16✔
429
                from,
16✔
430
                to,
16✔
431
                include_dirty,
16✔
432
            } => {
433
                if *include_dirty {
16✔
434
                    write!(
×
435
                        f,
×
436
                        "COPY_GIT --include-dirty {} {} {}",
×
437
                        quote_arg(rev),
×
438
                        quote_arg(from),
×
439
                        quote_arg(to)
×
440
                    )
441
                } else {
442
                    write!(
16✔
443
                        f,
16✔
444
                        "COPY_GIT {} {} {}",
16✔
445
                        quote_arg(rev),
16✔
446
                        quote_arg(from),
16✔
447
                        quote_arg(to)
16✔
448
                    )
449
                }
450
            }
451
            StepKind::HashSha256 { path } => write!(f, "HASH_SHA256 {}", quote_arg(path)),
17✔
452
            StepKind::Exit(code) => write!(f, "EXIT {}", code),
15✔
453
        }
454
    }
300✔
455
}
456

457
impl fmt::Display for Step {
458
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274✔
459
        for group in &self.guards {
406✔
460
            write!(f, "[")?;
132✔
461
            for (i, guard) in group.iter().enumerate() {
194✔
462
                if i > 0 {
194✔
463
                    write!(f, ", ")?
62✔
464
                }
132✔
465
                write!(f, "{}", guard)?;
194✔
466
            }
467
            write!(f, "] ")?;
132✔
468
        }
469
        write!(f, "{}", self.kind)
274✔
470
    }
274✔
471
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc