nmbrs_workload/report/vocab.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Single source of truth for the report grammar's lexical
5//! vocabulary — directives, their CLI flag mapping, and the
6//! valid value space for each.
7//!
8//! See [SRD-64 §4.3](../../../../docs/SRD/64_report_cli.md)
9//! for the design intent. This module is consumed by:
10//!
11//! - the YAML parser in [`super`] (validates input
12//! directives against [`KIND_DIRECTIVES`]),
13//! - `nmbrs::report_cmd` (builds [`ReportItem`] from CLI
14//! flags by walking [`directives_for`]),
15//! - `nmbrs::completion::report_node` (offers per-flag
16//! value providers from [`ValueProvider`]),
17//! - `super::ReportItem::to_yaml_directive_string` (the
18//! round-trip emitter — uses canonical directive ordering
19//! from [`directives_for`]).
20//!
21//! Adding a new directive means adding an entry to one of
22//! the per-kind tables in this module. The parser, CLI,
23//! completion, and emitter all pick it up automatically.
24//!
25//! ## Closed sets
26//!
27//! Directives whose value space is a closed set ship the
28//! enumeration here. Tab completion offers exactly these
29//! values; the parser rejects anything else.
30//!
31//! ## Open sets
32//!
33//! Directives whose value space depends on the active
34//! session db (metric names, observed label keys / values)
35//! ship a [`ValueProvider::Db`] tag. Completion plumbing in
36//! `nmbrs::completion` resolves these against the active
37//! session at tab time. The parser does not validate
38//! open-set values — they're free-form strings until the
39//! renderer queries the db.
40
41use super::Kind;
42
43/// Enumeration of value-space kinds for one directive's
44/// argument. Drives both completion suggestions and
45/// (for closed sets) parser-side validation.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum ValueProvider {
48 /// Free text — no completion, no validation.
49 /// Examples: `--label`, `--xlabel`, `--ylabel`.
50 Text,
51 /// Closed set of strings. Completion offers exactly
52 /// these; the parser rejects anything else.
53 Closed(&'static [&'static str]),
54 /// Numeric value (`f32` / `u32`). Completion offers
55 /// no suggestions; parser asserts numeric.
56 Number,
57 /// Hex color (`#RRGGBB` / `#RRGGBBAA`). Completion
58 /// offers no suggestions; parser asserts the shape.
59 HexColor,
60 /// File-system path (with `*.yaml` / `*.yml` filter
61 /// for workload paths). Completion plumbing decides.
62 Path { glob: &'static str },
63 /// Metric names from the active session db.
64 /// Completion queries the db at tab time.
65 DbMetricNames,
66 /// Distinct label keys observed in the active session
67 /// db. Completion queries the db at tab time.
68 DbLabelKeys,
69 /// `key=value` pairs where `key` completes from
70 /// [`Self::DbLabelKeys`] and `value` completes from the
71 /// distinct values observed for that key. Multi-valued
72 /// (comma-separated). Used for `--where`.
73 DbLabelKeyValuePairs,
74 /// Strict-JSON object literal. No completion.
75 /// Used for `--series` sub-blocks.
76 Json,
77}
78
79/// One directive: its CLI flag, its YAML directive name, the
80/// kinds it applies to, and the value-space it consumes.
81///
82/// `cli_flag` is always `--<name>`. `yaml_directive` is the
83/// keyword the YAML parser recognises; for some directives
84/// the YAML form is `<name> <value>` (no `=`) and for others
85/// it's `<name>=<value>`. [`yaml_form`] captures that.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Directive {
88 /// CLI long flag (e.g. `--over`, `--palette`).
89 pub cli_flag: &'static str,
90 /// YAML directive keyword (e.g. `over`, `palette`).
91 pub yaml_directive: &'static str,
92 /// Whether the YAML form uses `=` or whitespace
93 /// between keyword and value.
94 pub yaml_form: YamlForm,
95 /// Kinds the directive applies to. Per-item
96 /// declarations on a non-applicable kind warn at parse
97 /// time (SRD-46 §"Style and metadata directives").
98 pub applies_to: KindMask,
99 /// Where the directive's value lands in the AST.
100 pub target: DirectiveTarget,
101 /// Value-space for completion + parser validation.
102 pub value: ValueProvider,
103 /// Whether the flag may appear more than once in one
104 /// command-line invocation (e.g. `--series` is
105 /// repeatable; `--label` is not).
106 pub repeatable: bool,
107}
108
109/// `<keyword> <value>` (whitespace-separated) vs
110/// `<keyword>=<value>` (assignment). The parser accepts
111/// both for some directives by historical accident; this
112/// type captures the **canonical** form the round-trip
113/// emitter uses.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum YamlForm {
116 /// `over cycle`, `by profile`, `where dataset=example`,
117 /// `label "Demo"`, `as plot_demo`.
118 Whitespace,
119 /// `palette=wong`, `agg=mean`, `width=4`, `xscale=log`.
120 Equals,
121}
122
123/// Where a directive's value lives in the parsed AST. The
124/// CLI builder uses this to decide which field to populate;
125/// the round-trip emitter walks targets in canonical order.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum DirectiveTarget {
128 /// `ReportItem::label`.
129 ItemLabel,
130 /// `ReportItem::as_stem`.
131 ItemAsStem,
132 /// One of the [`super::Style`] scalar fields, named by
133 /// `yaml_directive`.
134 StyleField,
135 /// `ReportItem::style.series` (one entry per `--series`).
136 StyleSeries,
137 /// [`super::Style::destinations`] — the output routing set
138 /// (`to stdout, sessiondir`). Rides the style cascade so a
139 /// `defaults:` mapping can route a whole report or group at
140 /// once, but it is routing, not cosmetics.
141 Destinations,
142 /// `ReportItem::body` line — the renderer-consumed
143 /// directives (`over`, `by`, `where`, `agg`, `xlabel`,
144 /// `ylabel`, `xscale`, `yscale`, `metric`).
145 Body,
146}
147
148/// Bitmask of [`Kind`] applicability. A directive may apply
149/// to one kind, several, or all.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct KindMask(pub u8);
152
153impl KindMask {
154 pub const PLOT: KindMask = KindMask(1 << 0);
155 pub const TABLE: KindMask = KindMask(1 << 1);
156 pub const TEXT: KindMask = KindMask(1 << 2);
157 pub const FILE: KindMask = KindMask(1 << 3);
158 pub const DETAILS: KindMask = KindMask(1 << 4);
159
160 /// `Plot ∪ Table` — the figure kinds.
161 pub const FIGURES: KindMask = KindMask(0b00011);
162 /// Every kind.
163 pub const ALL: KindMask = KindMask(0b11111);
164
165 pub const fn contains(self, kind: Kind) -> bool {
166 (self.0 & kind_bit(kind)) != 0
167 }
168
169 pub const fn or(self, other: KindMask) -> KindMask {
170 KindMask(self.0 | other.0)
171 }
172}
173
174const fn kind_bit(kind: Kind) -> u8 {
175 match kind {
176 Kind::Plot => 1 << 0,
177 Kind::Table => 1 << 1,
178 Kind::Text => 1 << 2,
179 Kind::File => 1 << 3,
180 Kind::Details => 1 << 4,
181 }
182}
183
184// ---------------------------------------------------------------------------
185// Closed-set vocabularies
186// ---------------------------------------------------------------------------
187
188/// Aggregation function names accepted by `--agg` /
189/// `agg=<fn>`.
190pub const AGG_FNS: &[&str] = &["mean", "min", "max", "p50", "p99", "sum", "count"];
191
192/// Palette names. Numeric indices (`0`..`7`) are also
193/// accepted by the parser; completion only offers the
194/// named forms — numeric indices are a discoverability
195/// dead-end.
196pub const PALETTE_NAMES: &[&str] = &[
197 "wong",
198 "cividis_5",
199 "ibm",
200 "tol_bright",
201 "tol_high_contrast",
202 "tol_light",
203 "tol_muted",
204 "viridis_5",
205];
206
207/// Line-dash styles.
208pub const LINE_STYLES: &[&str] = &["solid", "dashed", "dotted", "dashdot", "none"];
209
210/// Marker shapes. `auto` cycles a distinct shape per series
211/// (literature-style); the rest are explicit shapes.
212pub const MARKER_SHAPES: &[&str] = &[
213 "auto", "none", "circle", "square", "triangle", "diamond", "plus", "cross",
214];
215
216/// Axis scale modes.
217/// Axis-scale mode keywords accepted by `xscale=` / `yscale=`.
218///
219/// - `linear` — default linear axis, range derived from data.
220/// - `log` — logarithmic axis (base 10). Data ≤ 0 becomes a
221/// plot error. Tick marks land on decade boundaries
222/// automatically.
223/// - `dec` — linear axis but the range is *snapped* to the
224/// nearest decade boundary on each side (powers of 10),
225/// producing tick marks at "round" decimal values.
226/// - `bin` — linear axis snapped to power-of-2 boundaries
227/// (1, 2, 4, 8, 16, …). Useful for plots whose x-axis is a
228/// `limit=` or `concurrency=` swept by doubling.
229pub const AXIS_SCALES: &[&str] = &["linear", "log", "dec", "bin"];
230
231/// SRD-46 output destinations — where a rendered item is
232/// delivered (`to stdout, sessiondir`).
233///
234/// - `sessiondir` — the session directory: the standalone
235/// artifact plus the markdown upsert into `summary.md` (or
236/// the item's `file` target). The default when nothing is
237/// declared, so a run's report is always durable.
238/// - `stdout` / `stderr` — the console form on that stream.
239/// NEVER implied: end-of-run rendering writes to a terminal
240/// only when the workload asks, because stdout is a
241/// workload's op output and a report carries wall-clock
242/// values that make it non-reproducible.
243/// - `none` — render nothing. Suppresses the item without
244/// deleting its declaration.
245pub const DESTINATION_NAMES: &[&str] = &["sessiondir", "stdout", "stderr", "none"];
246
247// ---------------------------------------------------------------------------
248// The directive table — one entry per CLI flag / YAML
249// directive pair. Adding a new directive starts here.
250// ---------------------------------------------------------------------------
251
252/// All directives, in canonical declaration order. The
253/// round-trip emitter walks this in order, picking out
254/// entries applicable to the item's kind.
255///
256/// Ordering matters for the emitter — directives appear in
257/// this order in the emitted YAML so `parse → emit → parse`
258/// is identity. Roughly: identity (label / as) → style →
259/// data shape (over / by / where / agg) → axis (x* / y*) →
260/// per-series sub-blocks.
261pub const ALL_DIRECTIVES: &[Directive] = &[
262 // ── Item identity ────────────────────────────────────
263 Directive {
264 cli_flag: "--label",
265 yaml_directive: "label",
266 yaml_form: YamlForm::Whitespace,
267 applies_to: KindMask::ALL,
268 target: DirectiveTarget::ItemLabel,
269 value: ValueProvider::Text,
270 repeatable: false,
271 },
272 Directive {
273 cli_flag: "--as",
274 yaml_directive: "as",
275 yaml_form: YamlForm::Whitespace,
276 applies_to: KindMask::FIGURES,
277 target: DirectiveTarget::ItemAsStem,
278 value: ValueProvider::Text,
279 repeatable: false,
280 },
281 // ── Output routing ───────────────────────────────────
282 // `to <dest>[, <dest>…]` — where a rendered item is
283 // delivered. One directive carrying the whole comma list
284 // (not repeatable) so the declaration reads as a set and
285 // an inner scope REPLACES an outer one rather than
286 // accumulating destinations the operator can't take back.
287 Directive {
288 cli_flag: "--to",
289 yaml_directive: "to",
290 yaml_form: YamlForm::Whitespace,
291 applies_to: KindMask::ALL,
292 target: DirectiveTarget::Destinations,
293 value: ValueProvider::Closed(DESTINATION_NAMES),
294 repeatable: false,
295 },
296 // ── Style / cosmetics ────────────────────────────────
297 Directive {
298 cli_flag: "--palette",
299 yaml_directive: "palette",
300 yaml_form: YamlForm::Equals,
301 applies_to: KindMask::FIGURES,
302 target: DirectiveTarget::StyleField,
303 value: ValueProvider::Closed(PALETTE_NAMES),
304 repeatable: false,
305 },
306 Directive {
307 cli_flag: "--line",
308 yaml_directive: "line",
309 yaml_form: YamlForm::Equals,
310 applies_to: KindMask::PLOT,
311 target: DirectiveTarget::StyleField,
312 value: ValueProvider::Closed(LINE_STYLES),
313 repeatable: false,
314 },
315 Directive {
316 cli_flag: "--width",
317 yaml_directive: "width",
318 yaml_form: YamlForm::Equals,
319 applies_to: KindMask::PLOT,
320 target: DirectiveTarget::StyleField,
321 value: ValueProvider::Number,
322 repeatable: false,
323 },
324 Directive {
325 cli_flag: "--scale",
326 yaml_directive: "scale",
327 yaml_form: YamlForm::Equals,
328 applies_to: KindMask::PLOT,
329 target: DirectiveTarget::StyleField,
330 value: ValueProvider::Number,
331 repeatable: false,
332 },
333 Directive {
334 cli_flag: "--marker",
335 yaml_directive: "marker",
336 yaml_form: YamlForm::Equals,
337 applies_to: KindMask::PLOT,
338 target: DirectiveTarget::StyleField,
339 value: ValueProvider::Closed(MARKER_SHAPES),
340 repeatable: false,
341 },
342 Directive {
343 cli_flag: "--size",
344 yaml_directive: "size",
345 yaml_form: YamlForm::Equals,
346 applies_to: KindMask::PLOT,
347 target: DirectiveTarget::StyleField,
348 value: ValueProvider::Number,
349 repeatable: false,
350 },
351 Directive {
352 cli_flag: "--color",
353 yaml_directive: "color",
354 yaml_form: YamlForm::Equals,
355 applies_to: KindMask::FIGURES,
356 target: DirectiveTarget::StyleField,
357 value: ValueProvider::HexColor,
358 repeatable: false,
359 },
360 Directive {
361 cli_flag: "--figure-width",
362 yaml_directive: "figure_width",
363 yaml_form: YamlForm::Equals,
364 applies_to: KindMask::FIGURES,
365 target: DirectiveTarget::StyleField,
366 value: ValueProvider::Number,
367 repeatable: false,
368 },
369 Directive {
370 cli_flag: "--figure-height",
371 yaml_directive: "figure_height",
372 yaml_form: YamlForm::Equals,
373 applies_to: KindMask::FIGURES,
374 target: DirectiveTarget::StyleField,
375 value: ValueProvider::Number,
376 repeatable: false,
377 },
378 // ── Data shape (body directives) ─────────────────────
379 Directive {
380 cli_flag: "--metric",
381 yaml_directive: "metric",
382 yaml_form: YamlForm::Whitespace,
383 applies_to: KindMask::FIGURES,
384 target: DirectiveTarget::Body,
385 value: ValueProvider::DbMetricNames,
386 repeatable: false,
387 },
388 Directive {
389 cli_flag: "--over",
390 yaml_directive: "over",
391 yaml_form: YamlForm::Whitespace,
392 applies_to: KindMask::FIGURES,
393 target: DirectiveTarget::Body,
394 value: ValueProvider::DbLabelKeys,
395 repeatable: false,
396 },
397 Directive {
398 cli_flag: "--by",
399 yaml_directive: "by",
400 yaml_form: YamlForm::Whitespace,
401 applies_to: KindMask::FIGURES,
402 target: DirectiveTarget::Body,
403 value: ValueProvider::DbLabelKeys,
404 repeatable: false,
405 },
406 Directive {
407 cli_flag: "--where",
408 yaml_directive: "where",
409 yaml_form: YamlForm::Whitespace,
410 applies_to: KindMask::FIGURES,
411 target: DirectiveTarget::Body,
412 value: ValueProvider::DbLabelKeyValuePairs,
413 repeatable: false,
414 },
415 Directive {
416 cli_flag: "--agg",
417 yaml_directive: "agg",
418 yaml_form: YamlForm::Equals,
419 applies_to: KindMask::FIGURES,
420 target: DirectiveTarget::Body,
421 value: ValueProvider::Closed(AGG_FNS),
422 repeatable: false,
423 },
424 // ── Axis labels / scales (plot only) ─────────────────
425 Directive {
426 cli_flag: "--xlabel",
427 yaml_directive: "xlabel",
428 yaml_form: YamlForm::Equals,
429 applies_to: KindMask::PLOT,
430 target: DirectiveTarget::Body,
431 value: ValueProvider::Text,
432 repeatable: false,
433 },
434 Directive {
435 cli_flag: "--ylabel",
436 yaml_directive: "ylabel",
437 yaml_form: YamlForm::Equals,
438 applies_to: KindMask::PLOT,
439 target: DirectiveTarget::Body,
440 value: ValueProvider::Text,
441 repeatable: false,
442 },
443 Directive {
444 cli_flag: "--x-scale",
445 yaml_directive: "x-scale",
446 yaml_form: YamlForm::Equals,
447 applies_to: KindMask::PLOT,
448 target: DirectiveTarget::Body,
449 value: ValueProvider::Closed(AXIS_SCALES),
450 repeatable: false,
451 },
452 Directive {
453 cli_flag: "--y-scale",
454 yaml_directive: "y-scale",
455 yaml_form: YamlForm::Equals,
456 applies_to: KindMask::PLOT,
457 target: DirectiveTarget::Body,
458 value: ValueProvider::Closed(AXIS_SCALES),
459 repeatable: false,
460 },
461 // ── Per-series style override (repeatable) ───────────
462 //
463 // `style <key>=<value>:<directives>` overrides scalar
464 // style fields (line / width / marker / size / color /
465 // palette) for the series whose discriminator matches
466 // `<key>=<value>`. Series-partition selection is no
467 // longer a plot directive — `query: avg(…) by (k, profile)`
468 // with `x: <one-of-those>` is the canonical declaration,
469 // and the renderer auto-derives partition labels from the
470 // result-set labels minus the X axis. That makes "what
471 // dimensions split the series" a property of the
472 // MetricsQL aggregation surface rather than something the
473 // plot grammar duplicates.
474 Directive {
475 cli_flag: "--style",
476 yaml_directive: "style",
477 yaml_form: YamlForm::Whitespace,
478 applies_to: KindMask::FIGURES,
479 target: DirectiveTarget::StyleSeries,
480 value: ValueProvider::Json,
481 repeatable: true,
482 },
483];
484
485/// Directives applicable to one [`Kind`], in canonical
486/// emit order. Filters [`ALL_DIRECTIVES`] by
487/// [`Directive::applies_to`].
488pub fn directives_for(kind: Kind) -> impl Iterator<Item = &'static Directive> {
489 ALL_DIRECTIVES
490 .iter()
491 .filter(move |d| d.applies_to.contains(kind))
492}
493
494/// Look up the directive for a given CLI flag (without
495/// the leading `--`). Returns `None` for unknown flags.
496pub fn directive_by_cli_flag(flag: &str) -> Option<&'static Directive> {
497 ALL_DIRECTIVES.iter().find(|d| {
498 // Accept both `--foo` and `foo` for ergonomics.
499 d.cli_flag == flag
500 || d.cli_flag
501 .strip_prefix("--")
502 .is_some_and(|stripped| stripped == flag)
503 })
504}
505
506/// Look up the directive for a given YAML directive
507/// keyword. Returns `None` for unknown keywords.
508pub fn directive_by_yaml_keyword(keyword: &str) -> Option<&'static Directive> {
509 ALL_DIRECTIVES.iter().find(|d| d.yaml_directive == keyword)
510}
511
512/// All CLI flags applicable to one kind, suitable for
513/// passing to `StrictNode::leaf_with_flags`. Stable
514/// order, matches [`ALL_DIRECTIVES`].
515pub fn cli_flags_for(kind: Kind) -> Vec<&'static str> {
516 directives_for(kind).map(|d| d.cli_flag).collect()
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn every_directive_has_a_unique_cli_flag() {
525 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
526 for d in ALL_DIRECTIVES {
527 assert!(
528 seen.insert(d.cli_flag),
529 "duplicate cli_flag '{}'",
530 d.cli_flag
531 );
532 }
533 }
534
535 #[test]
536 fn every_directive_has_a_unique_yaml_keyword() {
537 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
538 for d in ALL_DIRECTIVES {
539 assert!(
540 seen.insert(d.yaml_directive),
541 "duplicate yaml_directive '{}'",
542 d.yaml_directive
543 );
544 }
545 }
546
547 #[test]
548 fn every_cli_flag_starts_with_double_dash() {
549 for d in ALL_DIRECTIVES {
550 assert!(
551 d.cli_flag.starts_with("--"),
552 "cli_flag '{}' missing -- prefix",
553 d.cli_flag
554 );
555 }
556 }
557
558 #[test]
559 fn directives_for_plot_includes_axis_directives() {
560 let plot: Vec<&str> = directives_for(Kind::Plot).map(|d| d.cli_flag).collect();
561 assert!(plot.contains(&"--xlabel"));
562 assert!(plot.contains(&"--ylabel"));
563 assert!(plot.contains(&"--x-scale"));
564 assert!(plot.contains(&"--y-scale"));
565 assert!(plot.contains(&"--marker"));
566 assert!(plot.contains(&"--line"));
567 }
568
569 #[test]
570 fn directives_for_table_excludes_axis_and_marker_directives() {
571 let table: Vec<&str> = directives_for(Kind::Table).map(|d| d.cli_flag).collect();
572 assert!(!table.contains(&"--xlabel"));
573 assert!(!table.contains(&"--ylabel"));
574 assert!(!table.contains(&"--x-scale"));
575 assert!(!table.contains(&"--y-scale"));
576 assert!(!table.contains(&"--marker"));
577 assert!(!table.contains(&"--line"));
578 assert!(!table.contains(&"--width"));
579 assert!(!table.contains(&"--size"));
580 }
581
582 #[test]
583 fn directives_for_figures_share_data_shape_flags() {
584 // over / by / where / agg / metric apply to both
585 // plot and table — the data-shape group.
586 for kind in [Kind::Plot, Kind::Table] {
587 let flags: Vec<&str> = directives_for(kind).map(|d| d.cli_flag).collect();
588 for required in ["--over", "--by", "--where", "--agg", "--metric"] {
589 assert!(
590 flags.contains(&required),
591 "{kind:?} missing data-shape flag {required}"
592 );
593 }
594 }
595 }
596
597 #[test]
598 fn directives_for_text_excludes_figure_directives() {
599 let text: Vec<&str> = directives_for(Kind::Text).map(|d| d.cli_flag).collect();
600 assert!(!text.contains(&"--over"));
601 assert!(!text.contains(&"--metric"));
602 assert!(!text.contains(&"--agg"));
603 // But `--label` should apply (text items can carry
604 // a heading title).
605 assert!(text.contains(&"--label"));
606 }
607
608 #[test]
609 fn directive_by_cli_flag_round_trips() {
610 for d in ALL_DIRECTIVES {
611 let found = directive_by_cli_flag(d.cli_flag)
612 .unwrap_or_else(|| panic!("missing {}", d.cli_flag));
613 assert_eq!(found, d);
614 // Stripped form (`foo` vs `--foo`) also resolves.
615 let stripped = d.cli_flag.trim_start_matches("--");
616 let found_stripped = directive_by_cli_flag(stripped)
617 .unwrap_or_else(|| panic!("missing stripped {}", stripped));
618 assert_eq!(found_stripped, d);
619 }
620 }
621
622 #[test]
623 fn directive_by_yaml_keyword_round_trips() {
624 for d in ALL_DIRECTIVES {
625 assert_eq!(directive_by_yaml_keyword(d.yaml_directive), Some(d));
626 }
627 assert_eq!(directive_by_yaml_keyword("nonexistent"), None);
628 }
629
630 #[test]
631 fn closed_sets_are_non_empty_and_sorted_or_explicit() {
632 // PALETTE_NAMES is sorted alphabetically (per
633 // SRD-46 §"Palettes" — stable indexing rule).
634 let mut sorted = PALETTE_NAMES.to_vec();
635 sorted.sort();
636 // PALETTE_NAMES first lists the default `wong`,
637 // then alphabetic — verify the alphabetic suffix.
638 // The SRD explicitly notes "sorted alphabetically
639 // for stable indexing"; we keep `wong` in slot 0
640 // because it's the documented default. The numeric
641 // index = enumerated position.
642 assert!(!PALETTE_NAMES.is_empty());
643 for set in [AGG_FNS, LINE_STYLES, MARKER_SHAPES, AXIS_SCALES] {
644 assert!(!set.is_empty());
645 }
646 }
647
648 #[test]
649 fn style_is_repeatable_other_directives_are_not() {
650 let style = directive_by_cli_flag("--style").unwrap();
651 assert!(style.repeatable, "--style must be repeatable");
652 for d in ALL_DIRECTIVES {
653 if d.cli_flag != "--style" {
654 assert!(!d.repeatable, "{} should not be repeatable", d.cli_flag);
655 }
656 }
657 }
658
659 #[test]
660 fn kind_mask_figures_covers_plot_and_table_only() {
661 let m = KindMask::FIGURES;
662 assert!(m.contains(Kind::Plot));
663 assert!(m.contains(Kind::Table));
664 assert!(!m.contains(Kind::Text));
665 assert!(!m.contains(Kind::File));
666 assert!(!m.contains(Kind::Details));
667 }
668}