1use crate::{
9 CommandPolicy, CommandSpec, EnumDef, ExternalNames, FieldDef, OperationKind, ParamSpec,
10 RecordDef, Retry, Scope, Type, TypeTable, VariantDef,
11};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum SessionCommand {
16 Inspect,
17 Activate,
18 Edit,
19 Submit,
20 SetBool,
21 SetNumber,
22 Select,
23 Back,
24 WindowOpen,
25 WindowClose,
26 WindowActivate,
27 WindowConfirmClose,
28 WindowList,
29 WindowMinimize,
30 WindowRestore,
31 WindowMaximize,
32 WindowMove,
33 WindowResize,
34 Language,
35 SetLanguage,
36 Focus,
37 SetFile,
38 TraceStart,
39 TraceStop,
40 TraceReport,
41}
42
43impl SessionCommand {
44 pub const ALL: [SessionCommand; 25] = [
46 SessionCommand::Inspect,
47 SessionCommand::Activate,
48 SessionCommand::Edit,
49 SessionCommand::Submit,
50 SessionCommand::SetBool,
51 SessionCommand::SetNumber,
52 SessionCommand::Select,
53 SessionCommand::Back,
54 SessionCommand::WindowOpen,
55 SessionCommand::WindowClose,
56 SessionCommand::WindowActivate,
57 SessionCommand::WindowConfirmClose,
58 SessionCommand::WindowList,
59 SessionCommand::WindowMinimize,
60 SessionCommand::WindowRestore,
61 SessionCommand::WindowMaximize,
62 SessionCommand::WindowMove,
63 SessionCommand::WindowResize,
64 SessionCommand::Language,
65 SessionCommand::SetLanguage,
66 SessionCommand::Focus,
67 SessionCommand::SetFile,
68 SessionCommand::TraceStart,
69 SessionCommand::TraceStop,
70 SessionCommand::TraceReport,
71 ];
72
73 pub fn id(self) -> &'static str {
74 match self {
75 SessionCommand::Inspect => "ui.inspect",
76 SessionCommand::Activate => "ui.activate",
77 SessionCommand::Edit => "ui.edit",
78 SessionCommand::Submit => "ui.submit",
79 SessionCommand::SetBool => "ui.set_bool",
80 SessionCommand::SetNumber => "ui.set_number",
81 SessionCommand::Select => "ui.select",
82 SessionCommand::Back => "ui.back",
83 SessionCommand::WindowOpen => "window.open",
84 SessionCommand::WindowClose => "window.close",
85 SessionCommand::WindowActivate => "window.activate",
86 SessionCommand::WindowConfirmClose => "window.confirm_close",
87 SessionCommand::WindowList => "window.list",
88 SessionCommand::WindowMinimize => "window.minimize",
89 SessionCommand::WindowRestore => "window.restore",
90 SessionCommand::WindowMaximize => "window.maximize",
91 SessionCommand::WindowMove => "window.move",
92 SessionCommand::WindowResize => "window.resize",
93 SessionCommand::Language => "ui.language",
94 SessionCommand::SetLanguage => "ui.set_language",
95 SessionCommand::Focus => "ui.focus",
96 SessionCommand::SetFile => "ui.set_file",
97 SessionCommand::TraceStart => "trace.start",
98 SessionCommand::TraceStop => "trace.stop",
99 SessionCommand::TraceReport => "trace.report",
100 }
101 }
102
103 pub fn from_id(id: &str) -> Option<Self> {
104 Self::ALL.into_iter().find(|command| command.id() == id)
105 }
106
107 pub fn is_trace(self) -> bool {
110 matches!(
111 self,
112 SessionCommand::TraceStart | SessionCommand::TraceStop | SessionCommand::TraceReport
113 )
114 }
115
116 pub fn is_presentation(self) -> bool {
119 matches!(
120 self,
121 SessionCommand::WindowMinimize
122 | SessionCommand::WindowRestore
123 | SessionCommand::WindowMaximize
124 | SessionCommand::WindowMove
125 | SessionCommand::WindowResize
126 )
127 }
128}
129
130fn param(name: &str, ty: Type, doc: &str) -> ParamSpec {
131 ParamSpec {
132 name: name.into(),
133 ty,
134 doc: doc.into(),
135 }
136}
137
138fn spec(
139 command: SessionCommand,
140 kind: OperationKind,
141 doc: &str,
142 params: Vec<ParamSpec>,
143 result: Type,
144 retry: Retry,
145) -> CommandSpec {
146 let (namespace, name) = command.id().split_once('.').expect("namespaced id");
147 CommandSpec {
148 id: command.id().into(),
149 name: name.into(),
150 doc: doc.into(),
151 kind,
152 scope: Scope::Session,
153 params,
154 result,
155 errors: vec![
156 "requires_ui_session".into(),
157 "target_not_found".into(),
158 "target_unavailable".into(),
159 "target_blocked".into(),
160 "stale_window".into(),
161 "stale_confirmation".into(),
162 ],
163 policy: CommandPolicy {
164 retry,
165 ..CommandPolicy::default()
166 },
167 external: ExternalNames::derive(namespace, name),
168 reads: vec![],
169 on: None,
170 }
171}
172
173fn with_errors(mut spec: CommandSpec, errors: &[&str]) -> CommandSpec {
174 spec.errors
175 .extend(errors.iter().map(|error| (*error).to_string()));
176 spec
177}
178
179fn named(name: &str) -> Type {
180 Type::Record { name: name.into() }
181}
182
183fn distribution() -> Type {
184 named("TraceDistribution")
185}
186
187fn segment() -> Type {
188 named("TraceSegment")
189}
190
191fn trace_record(name: &str, fields: &[(&str, Type, &str)]) -> RecordDef {
192 RecordDef {
193 name: name.into(),
194 fields: fields
195 .iter()
196 .map(|(name, ty, doc)| FieldDef {
197 name: (*name).into(),
198 ty: ty.clone(),
199 doc: (*doc).into(),
200 content: false,
201 })
202 .collect(),
203 }
204}
205
206pub fn session_catalog() -> (TypeTable, Vec<CommandSpec>) {
208 let control = Type::Record {
209 name: "ControlInfo".into(),
210 };
211 let field = |name: &str, ty: Type, doc: &str| FieldDef {
212 name: name.into(),
213 ty,
214 doc: doc.into(),
215 content: false,
216 };
217 let unit_enum = |name: &str, variants: &[&str]| EnumDef {
218 name: name.into(),
219 variants: variants
220 .iter()
221 .map(|variant| VariantDef {
222 name: (*variant).into(),
223 fields: vec![],
224 label: None,
225 })
226 .collect(),
227 };
228 let decision = || Type::Enum {
229 name: "CloseDecision".into(),
230 };
231 let close_result = Type::Record {
232 name: "CloseResult".into(),
233 };
234 let named_enum = |name: &str| Type::Enum { name: name.into() };
235 let window_info = Type::Record {
236 name: "WindowInfo".into(),
237 };
238 let types = TypeTable {
239 enums: vec![
240 unit_enum(
241 "CloseOutcome",
242 &["Closed", "Rejected", "ConfirmationRequired"],
243 ),
244 unit_enum("CloseDecision", &["Save", "Discard", "Cancel"]),
245 unit_enum("WindowPresentation", &["Native", "Virtual"]),
246 unit_enum("WindowLifecycle", &["Opening", "Open", "Closing"]),
247 unit_enum("WindowVisibility", &["Visible", "Minimized", "Hidden"]),
248 unit_enum("WindowSize", &["Normal", "Maximized", "FullScreen"]),
249 unit_enum(
250 "WindowAction",
251 &[
252 "Activate", "Close", "Minimize", "Restore", "Maximize", "Move", "Resize",
253 ],
254 ),
255 unit_enum("WindowChangeSource", &["Command", "System"]),
256 ],
257 records: vec![
258 RecordDef {
259 name: "ControlInfo".into(),
260 fields: vec![
261 field("target", Type::Text, "Stable control identity"),
262 field("window", Type::Text, "Owning window instance"),
263 field("role", Type::Text, "Semantic role"),
264 field("label", Type::Text, "Resolved accessible name"),
265 field("enabled", Type::Bool, "Whether the control can be operated"),
266 field(
267 "value",
268 Type::optional(Type::Text),
269 "Current value, when the control has one",
270 ),
271 field(
272 "reason",
273 Type::optional(Type::Text),
274 "Why the control cannot be operated: `disabled` or `blocked_by_modal`",
275 ),
276 ],
277 },
278 RecordDef {
279 name: "CloseResult".into(),
280 fields: vec![
281 field(
282 "outcome",
283 Type::Enum {
284 name: "CloseOutcome".into(),
285 },
286 "Closed, rejected, or waiting for a confirmation decision",
287 ),
288 field("window", Type::Text, "Window instance"),
289 field(
290 "confirmation",
291 Type::optional(Type::Text),
292 "Confirmation to decide with window.confirm_close",
293 ),
294 field(
295 "draft_revision",
296 Type::Int,
297 "Draft revision the confirmation is bound to",
298 ),
299 field(
300 "decisions",
301 Type::list(decision()),
302 "Decisions the confirmation accepts",
303 ),
304 field(
305 "message",
306 Type::optional(Type::Text),
307 "The declared confirmation message, as shown in the dialog",
308 ),
309 field(
310 "reason",
311 Type::optional(Type::Text),
312 "Why closing was rejected",
313 ),
314 ],
315 },
316 RecordDef {
317 name: "WindowFrame".into(),
318 fields: vec![
319 field(
320 "x",
321 Type::Float,
322 "Left edge in points from the main display's left edge",
323 ),
324 field(
325 "y",
326 Type::Float,
327 "Top edge in points below the main display's top edge",
328 ),
329 field("width", Type::Float, "Outer width in points"),
330 field("height", Type::Float, "Outer height in points"),
331 ],
332 },
333 trace_record(
334 "TraceStarted",
335 &[
336 ("rules", Type::Text, "The rules in their normal spelling"),
337 (
338 "file",
339 Type::optional(Type::Text),
340 "Where the timeline goes, when it is written to a file",
341 ),
342 ],
343 ),
344 trace_record(
345 "TraceStopped",
346 &[
347 (
348 "files",
349 Type::list(Type::Text),
350 "The timeline files written, for ui.perfetto.dev",
351 ),
352 ("events", Type::Int, "Records read"),
353 (
354 "dropped",
355 Type::Int,
356 "Records lost because a ring was full; raise `buffer:`",
357 ),
358 (
359 "timeline",
360 Type::optional(Type::Text),
361 "The timeline itself, when asked for",
362 ),
363 ],
364 ),
365 trace_record(
366 "TraceDistribution",
367 &[
368 ("count", Type::Int, "Samples in all"),
369 ("p50", Type::Int, "Median of the recent samples"),
370 ("p95", Type::Int, "95th percentile of the recent samples"),
371 ("max", Type::Int, "Largest sample"),
372 ],
373 ),
374 trace_record(
375 "TraceCommand",
376 &[
377 ("id", Type::Text, "Command id"),
378 ("entry", Type::Text, "ui, cli, json, mcp or test"),
379 (
380 "total_us",
381 distribution(),
382 "From dispatch to on screen, or to the engine's answer",
383 ),
384 ("queue_us", distribution(), "Waiting for the engine"),
385 ("exec_us", distribution(), "Running the command"),
386 (
387 "publish_us",
388 distribution(),
389 "Snapshot, comparison and encoding of the state",
390 ),
391 (
392 "apply_us",
393 distribution(),
394 "Decoding and writing the state on the UI thread",
395 ),
396 ("bytes", distribution(), "State bytes sent to the UI"),
397 ],
398 ),
399 trace_record(
400 "TraceQuery",
401 &[
402 ("id", Type::Text, "Query id"),
403 ("cancelled", Type::Int, "Answers no one waited for any more"),
404 ("queue_us", distribution(), "Waiting for the engine"),
405 (
406 "provider_us",
407 distribution(),
408 "From the provider's start to its answer",
409 ),
410 (
411 "round_trip_us",
412 distribution(),
413 "From the request to the UI showing the answer",
414 ),
415 ],
416 ),
417 trace_record(
418 "TraceField",
419 &[
420 ("name", Type::Text, "State field"),
421 (
422 "sent",
423 Type::Int,
424 "Times it changed, and was sent to a UI if one runs",
425 ),
426 ("bytes_max", Type::Int, "Largest encoding sent"),
427 (
428 "items_max",
429 Type::optional(Type::Int),
430 "Most items, for a list",
431 ),
432 ("snapshot_us", distribution(), "Taking it from the engine"),
433 (
434 "compare_us",
435 distribution(),
436 "Comparing it with the last state",
437 ),
438 ("encode_us", distribution(), "Encoding it for the UI"),
439 (
440 "apply_us",
441 distribution(),
442 "Decoding and writing it on the UI thread",
443 ),
444 ],
445 ),
446 trace_record(
447 "TraceSegment",
448 &[
449 ("name", Type::Text, "Part of the work"),
450 ("us", Type::Int, "Microseconds"),
451 ],
452 ),
453 trace_record(
454 "TraceInteraction",
455 &[
456 (
457 "control",
458 Type::optional(Type::Text),
459 "The control operated; absent for commands from other entries",
460 ),
461 ("entry", Type::Text, "Where it came from"),
462 ("commands", Type::list(Type::Text), "Commands it ran"),
463 ("at_ms", Type::Int, "Milliseconds after tracing started"),
464 (
465 "total_us",
466 Type::Int,
467 "From the action to the frame showing its result",
468 ),
469 (
470 "revision",
471 Type::optional(Type::Int),
472 "The state revision it produced",
473 ),
474 ("segments", Type::list(segment()), "Where the time went"),
475 ],
476 ),
477 trace_record(
478 "TraceFrames",
479 &[
480 ("spacing_us", distribution(), "Time between frame ticks"),
481 ("long", Type::Int, "Frames over the long-frame threshold"),
482 ],
483 ),
484 trace_record(
485 "TraceFrame",
486 &[
487 ("at_ms", Type::Int, "Milliseconds after tracing started"),
488 ("us", Type::Int, "Time since the frame before"),
489 (
490 "refresh_us",
491 Type::Int,
492 "The display's refresh interval, 0 when unknown",
493 ),
494 ("bern_us", Type::Int, "Bern's own work on the UI thread"),
495 (
496 "outside_us",
497 Type::Int,
498 "The rest: generated views and the platform's layout and drawing",
499 ),
500 (
501 "segments",
502 Type::list(segment()),
503 "Bern's longest work on the UI thread",
504 ),
505 (
506 "engine",
507 Type::list(segment()),
508 "What the engine did meanwhile",
509 ),
510 ],
511 ),
512 trace_record(
513 "TraceSpan",
514 &[
515 ("name", Type::Text, "Span name"),
516 ("us", distribution(), "Durations"),
517 ],
518 ),
519 trace_record(
520 "TraceReport",
521 &[
522 ("running", Type::Bool, "Whether tracing is still on"),
523 ("window_ms", Type::Int, "Milliseconds the statistics cover"),
524 ("events", Type::Int, "Records read"),
525 ("dropped", Type::Int, "Records lost because a ring was full"),
526 (
527 "commands",
528 Type::list(named("TraceCommand")),
529 "Per command, slowest first",
530 ),
531 ("queries", Type::list(named("TraceQuery")), "Per query"),
532 (
533 "fields",
534 Type::list(named("TraceField")),
535 "State fields sent, largest first",
536 ),
537 (
538 "interactions",
539 Type::list(named("TraceInteraction")),
540 "The slowest interactions and commands",
541 ),
542 ("frames", named("TraceFrames"), "Frame ticks"),
543 (
544 "long_frames",
545 Type::list(named("TraceFrame")),
546 "The latest long frames",
547 ),
548 ("spans", Type::list(named("TraceSpan")), "Spans by name"),
549 ],
550 ),
551 RecordDef {
552 name: "WindowInfo".into(),
553 fields: vec![
554 field("window", Type::Text, "Window instance"),
555 field("kind", Type::Text, "Window kind id"),
556 field(
557 "presentation",
558 named_enum("WindowPresentation"),
559 "Native system window or virtual window",
560 ),
561 field(
562 "lifecycle",
563 named_enum("WindowLifecycle"),
564 "Opening until the platform reports it; Closing while a close confirmation waits",
565 ),
566 field(
567 "visibility",
568 named_enum("WindowVisibility"),
569 "Visible, minimized or hidden, as the platform reports it",
570 ),
571 field(
572 "size",
573 named_enum("WindowSize"),
574 "Normal, maximized or full screen",
575 ),
576 field(
577 "frame",
578 Type::optional(Type::Record {
579 name: "WindowFrame".into(),
580 }),
581 "Outer frame; absent while opening",
582 ),
583 field(
584 "focused",
585 Type::Bool,
586 "Whether the window has keyboard focus",
587 ),
588 field(
589 "actions",
590 Type::list(named_enum("WindowAction")),
591 "Window actions available now",
592 ),
593 field(
594 "version",
595 Type::Int,
596 "Bumped by every change of this instance's presentation",
597 ),
598 field(
599 "changed_by",
600 Type::optional(named_enum("WindowChangeSource")),
601 "Whether the latest change came from a command or from the user and system",
602 ),
603 ],
604 },
605 ],
606 };
607 let target = || param("target", Type::Text, "Control identity from ui.inspect");
608 let window = || param("window", Type::Text, "Window instance id");
609 let commands = vec![
610 spec(
611 SessionCommand::Inspect,
612 OperationKind::Query,
613 "List the mounted controls of the session",
614 vec![],
615 Type::list(control),
616 Retry::Idempotent,
617 ),
618 spec(
619 SessionCommand::Activate,
620 OperationKind::Command,
621 "Activate a button or link through its real control binding",
622 vec![target()],
623 Type::Unit,
624 Retry::Never,
625 ),
626 spec(
627 SessionCommand::Edit,
628 OperationKind::Command,
629 "Replace the draft text of a text input",
630 vec![target(), param("value", Type::Text, "New text")],
631 Type::Unit,
632 Retry::Idempotent,
633 ),
634 spec(
635 SessionCommand::Submit,
636 OperationKind::Command,
637 "Submit a text input",
638 vec![target()],
639 Type::Unit,
640 Retry::Never,
641 ),
642 spec(
643 SessionCommand::SetBool,
644 OperationKind::Command,
645 "Set a switch or checkbox",
646 vec![target(), param("value", Type::Bool, "New state")],
647 Type::Unit,
648 Retry::Idempotent,
649 ),
650 spec(
651 SessionCommand::SetNumber,
652 OperationKind::Command,
653 "Set and commit a slider",
654 vec![target(), param("value", Type::Float, "New value")],
655 Type::Unit,
656 Retry::Never,
657 ),
658 spec(
659 SessionCommand::Select,
660 OperationKind::Command,
661 "Select an option of a picker, sidebar or tab view by variant name",
662 vec![target(), param("option", Type::Text, "Option name")],
663 Type::Unit,
664 Retry::Idempotent,
665 ),
666 spec(
667 SessionCommand::Back,
668 OperationKind::Command,
669 "Go back one step in a navigation container",
670 vec![target()],
671 Type::Unit,
672 Retry::Never,
673 ),
674 spec(
675 SessionCommand::Focus,
676 OperationKind::Command,
677 "Give a control the keyboard focus, such as a search field, whose results then show",
678 vec![target()],
679 Type::Unit,
680 Retry::Idempotent,
681 ),
682 with_errors(
683 spec(
684 SessionCommand::SetFile,
685 OperationKind::Command,
686 "Give a file to a control that asks for one or takes dropped files, as if the user chose or dropped it; its action then runs",
687 vec![
688 target(),
689 param(
690 "url",
691 Type::Text,
692 "A `file://` or http(s) URL; it must be of a kind the control accepts",
693 ),
694 ],
695 Type::Unit,
696 Retry::Never,
697 ),
698 &["invalid_url", "unsupported_file"],
699 ),
700 spec(
701 SessionCommand::Language,
702 OperationKind::Query,
703 "The session's interface language tag",
704 vec![],
705 Type::Text,
706 Retry::Idempotent,
707 ),
708 with_errors(
709 spec(
710 SessionCommand::SetLanguage,
711 OperationKind::Command,
712 "Switch the interface language; every localized text updates while drafts, navigation and windows stay",
713 vec![param(
714 "language",
715 Type::Text,
716 "A supported language tag, such as `en`",
717 )],
718 Type::Unit,
719 Retry::Idempotent,
720 ),
721 &["unsupported_language"],
722 ),
723 with_errors(
724 spec(
725 SessionCommand::TraceStart,
726 OperationKind::Command,
727 "Start the performance trace with rules such as `engine,ui,slow:16ms` or `command:library.*`; a running trace ends first",
728 vec![param(
729 "rules",
730 Type::Text,
731 "Categories (engine, ui, frame, span, motion, style, all), filters (command:, query:, control:, field:, feature:), slow:, long:, file:, quiet, buffer:",
732 )],
733 named("TraceStarted"),
734 Retry::Idempotent,
735 ),
736 &["invalid_rules"],
737 ),
738 spec(
739 SessionCommand::TraceStop,
740 OperationKind::Command,
741 "Stop the performance trace and write its timeline",
742 vec![param(
743 "timeline",
744 Type::optional(Type::Bool),
745 "Also return the timeline itself, for an app whose files are on another device",
746 )],
747 named("TraceStopped"),
748 Retry::Idempotent,
749 ),
750 spec(
751 SessionCommand::TraceReport,
752 OperationKind::Query,
753 "What the performance trace measured: p50, p95 and maximum per command, the slowest interactions and the long frames",
754 vec![
755 param(
756 "top",
757 Type::optional(Type::Int),
758 "How many of the slowest interactions to list (10)",
759 ),
760 param(
761 "reset",
762 Type::optional(Type::Bool),
763 "Start the statistics over after this report",
764 ),
765 ],
766 named("TraceReport"),
767 Retry::Idempotent,
768 ),
769 spec(
770 SessionCommand::WindowOpen,
771 OperationKind::Command,
772 "Open a window kind or activate its existing instance",
773 vec![param("kind", Type::Text, "Window kind id")],
774 Type::Text,
775 Retry::Idempotent,
776 ),
777 spec(
778 SessionCommand::WindowClose,
779 OperationKind::Command,
780 "Request closing a window instance; its declared close check may ask for a confirmation",
781 vec![window()],
782 close_result.clone(),
783 Retry::Idempotent,
784 ),
785 spec(
786 SessionCommand::WindowActivate,
787 OperationKind::Command,
788 "Bring a window instance to the front",
789 vec![window()],
790 Type::Unit,
791 Retry::Idempotent,
792 ),
793 spec(
794 SessionCommand::WindowConfirmClose,
795 OperationKind::Command,
796 "Decide a pending close confirmation, as its dialog would",
797 vec![
798 window(),
799 param(
800 "confirmation",
801 Type::Text,
802 "Confirmation id from window.close",
803 ),
804 param("decision", decision(), "Save, Discard or Cancel"),
805 ],
806 close_result,
807 Retry::Never,
808 ),
809 spec(
810 SessionCommand::WindowList,
811 OperationKind::Query,
812 "List the open window instances with their actual presentation and available actions",
813 vec![],
814 Type::list(window_info.clone()),
815 Retry::Idempotent,
816 ),
817 spec(
818 SessionCommand::WindowMinimize,
819 OperationKind::Command,
820 "Minimize a window instance; its drafts and navigation stay",
821 vec![window()],
822 window_info.clone(),
823 Retry::Idempotent,
824 ),
825 spec(
826 SessionCommand::WindowRestore,
827 OperationKind::Command,
828 "Undo one step: bring a minimized window back, or leave full screen or the maximized size",
829 vec![window()],
830 window_info.clone(),
831 Retry::Never,
832 ),
833 spec(
834 SessionCommand::WindowMaximize,
835 OperationKind::Command,
836 "Grow a window instance to fill its display, as the platform zooms windows",
837 vec![window()],
838 window_info.clone(),
839 Retry::Idempotent,
840 ),
841 with_errors(
842 spec(
843 SessionCommand::WindowMove,
844 OperationKind::Command,
845 "Move a window instance so its top-left corner is at x, y; its title bar must stay on a display",
846 vec![
847 window(),
848 param("x", Type::Float, "Left edge in points"),
849 param("y", Type::Float, "Top edge in points"),
850 ],
851 window_info.clone(),
852 Retry::Idempotent,
853 ),
854 &["out_of_range"],
855 ),
856 with_errors(
857 spec(
858 SessionCommand::WindowResize,
859 OperationKind::Command,
860 "Resize a window instance, keeping its top-left corner; the size must respect the window's minimum",
861 vec![
862 window(),
863 param("width", Type::Float, "Outer width in points"),
864 param("height", Type::Float, "Outer height in points"),
865 ],
866 window_info,
867 Retry::Idempotent,
868 ),
869 &["out_of_range"],
870 ),
871 ];
872 (types, commands)
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878 use crate::descriptor::check_external_names;
879
880 #[test]
881 fn session_catalog_is_consistent() {
882 let (types, commands) = session_catalog();
883 types.validate().unwrap();
884 check_external_names(commands.iter()).unwrap();
885 assert_eq!(commands.len(), SessionCommand::ALL.len());
886 for command in SessionCommand::ALL {
887 assert_eq!(SessionCommand::from_id(command.id()), Some(command));
888 assert!(commands.iter().any(|spec| spec.id == command.id()));
889 }
890 }
891}