refactor: catalog Tabby feature coverage
test / core (push) Successful in 1m4s

This commit is contained in:
Tom You
2026-07-06 11:02:46 +00:00
parent 04ef2fe57d
commit 3aea14b64f
4 changed files with 546 additions and 207 deletions
+29 -10
View File
@@ -8,7 +8,9 @@ Keep the same broad product shape as Tabby—terminal tabs, nested split panes,
## Current implementation
- `rabby-core`: dependency-light Rust domain model for profiles, tabs, split panes, session restore primitives, and feature coverage tests.
- `rabby-core`: dependency-light Rust domain model for profiles, tabs, split panes, session restore primitives, and Tabby feature coverage tests.
- `rabby-core/src/feature`: structured Tabby-vs-Rabby feature catalog with honest status (`Modeled`, `UiPrototype`, `Planned`).
- `rabby-core/src/workspace`: refactored workspace/profile/pane domain state.
- `src-tauri`: Tauri v2 desktop backend exposing the Rust feature matrix to the UI.
- `ui`: static responsive UI mock/prototype matching Tabby-style terminal UX without a JavaScript build chain.
- `docs/rabby-screenshot.svg`: screenshot artifact of the UI/UX direction.
@@ -16,21 +18,38 @@ Keep the same broad product shape as Tabby—terminal tabs, nested split panes,
## Verification
```bash
cargo fmt --all -- --check
cargo test -p rabby-core
cargo check -p rabby
cargo build --release -p rabby
```
The core crate is intentionally testable without launching the desktop shell.
## Tabby feature mapping
## Tabby feature comparison
| Tabby feature | Rabby implementation direction |
| --- | --- |
| Terminal emulator | Rust core models terminal sessions; Tauri UI presents panes/tabs. |
| SSH/Telnet/Serial | Typed connection profiles with validation and command boundaries. |
| Split panes/tabs | Recursive pane tree with tested split bounds and leaf counting. |
| Theming/shortcuts | Static UI theme now; config surface reserved in core. |
| Session restore | Workspace struct stores profiles, tabs, active tab, and quake mode. |
| Smaller footprint | No Electron/Angular runtime in this scaffold; static assets + Tauri. |
This table is based on Tabby's README and top-level modules (`tabby-terminal`, `tabby-local`, `tabby-ssh`, `tabby-telnet`, `tabby-serial`, `tabby-plugin-manager`, `tabby-settings`, `tabby-web`, etc.). The scaffold does **not** claim every production implementation is finished; it verifies that each class is represented in the Rust catalog and tests.
| Tabby feature class | Rabby coverage now | Status |
| --- | --- | --- |
| VT terminal emulation | Terminal session/pane surface requirement in Rust catalog | Planned |
| Nested split panes | Recursive `PaneNode` with bounds and leaf-count tests | Modeled |
| Remembered tabs | `Workspace` stores tabs and active tab | Modeled |
| Quake console/global hotkey | `quake_mode` state plus UI control | Modeled |
| Progress/process notifications | `Tab.progress` state with validation | Modeled |
| Bracketed/multiline paste, RMB paste, copy-on-select | Terminal preference requirement in catalog | Planned |
| Unicode/double-width/ligatures | Renderer requirement in catalog | Planned |
| Local shell profiles | Typed `LocalShell` profile command | Modeled |
| SSH connection manager | Typed `Ssh` profile command boundary | Modeled |
| SSH forwarding/jump hosts/agent forwarding/login scripts | Advanced SSH backlog entry in catalog | Planned |
| Telnet | Typed `Telnet` profile | Modeled |
| Serial terminal | Typed `Serial` profile plus serial option backlog | Modeled |
| Zmodem/SFTP transfers | Transfer surface reserved for SSH sessions | Planned |
| Encrypted SSH secrets/config | Tauri/Rust secret-store boundary in catalog | Planned |
| Themes/color schemes | `Profile.color_scheme` plus UI theme prototype | Modeled |
| Configurable multi-chord shortcuts | Command palette/shortcut UX prototype entry | UI prototype |
| Plugin manager/ecosystem | Extension boundary reserved outside core | Planned |
| Portable/web surfaces | Static UI + Tauri shell; web surface tracked as platform backlog | UI prototype |
## Screenshot
+265
View File
@@ -0,0 +1,265 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FeatureStatus {
Modeled,
UiPrototype,
Planned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FeatureArea {
Terminal,
Connection,
UiUx,
Transfer,
Security,
Extensibility,
Platform,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FeatureSpec {
pub key: &'static str,
pub title: &'static str,
pub area: FeatureArea,
pub tabby_reference: &'static str,
pub rabby_design: &'static str,
pub status: FeatureStatus,
}
pub const TABBY_FEATURES: &[FeatureSpec] = &[
FeatureSpec {
key: "vt-terminal",
title: "VT terminal emulation",
area: FeatureArea::Terminal,
tabby_reference: "VT220 terminal + extensions",
rabby_design: "Terminal session model plus Tauri pane surface",
status: FeatureStatus::Planned,
},
FeatureSpec {
key: "nested-splits",
title: "Nested split panes",
area: FeatureArea::Terminal,
tabby_reference: "Multiple nested split panes",
rabby_design: "Recursive PaneNode tree with ratio bounds",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "tabs-any-side",
title: "Remembered tabs",
area: FeatureArea::Terminal,
tabby_reference: "Tabs on any side and remembered tabs",
rabby_design: "Workspace stores tabs and active tab",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "quake-hotkey",
title: "Quake console",
area: FeatureArea::UiUx,
tabby_reference: "Dockable window with global spawn hotkey",
rabby_design: "Workspace quake_mode flag and UI control",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "progress-notifications",
title: "Progress and process notifications",
area: FeatureArea::UiUx,
tabby_reference: "Progress detection and notification on process completion",
rabby_design: "Tab progress field and future notification boundary",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "paste-safety",
title: "Paste ergonomics",
area: FeatureArea::Terminal,
tabby_reference: "Bracketed paste, multiline paste warnings, RMB paste, copy-on-select",
rabby_design: "Terminal preference catalog entry",
status: FeatureStatus::Planned,
},
FeatureSpec {
key: "font-ligatures-unicode",
title: "Unicode and font ligatures",
area: FeatureArea::Terminal,
tabby_reference: "Full Unicode including double-width characters and font ligatures",
rabby_design: "Renderer requirement in feature catalog",
status: FeatureStatus::Planned,
},
FeatureSpec {
key: "local-shell-profiles",
title: "Local shell profiles",
area: FeatureArea::Connection,
tabby_reference: "Custom shell profiles and PowerShell/WSL/Git-Bash/Cygwin/MSYS2/Cmder/CMD support",
rabby_design: "Typed LocalShell profile command",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "ssh-client",
title: "SSH client and connection manager",
area: FeatureArea::Connection,
tabby_reference: "SSH2 client with connection manager",
rabby_design: "Typed SSH profile and command boundary",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "ssh-forwarding",
title: "SSH forwarding and jump hosts",
area: FeatureArea::Connection,
tabby_reference: "X11/port forwarding, automatic jump host management, agent forwarding, login scripts",
rabby_design: "Advanced SSH options reserved in catalog",
status: FeatureStatus::Planned,
},
FeatureSpec {
key: "telnet-client",
title: "Telnet client",
area: FeatureArea::Connection,
tabby_reference: "Integrated Telnet client",
rabby_design: "Typed Telnet profile",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "serial-terminal",
title: "Serial terminal",
area: FeatureArea::Connection,
tabby_reference: "Saved serial connections, readline input, hex/hexdump, newline conversion, auto reconnect",
rabby_design: "Typed Serial profile plus serial option backlog",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "zmodem-sftp",
title: "File transfer",
area: FeatureArea::Transfer,
tabby_reference: "Zmodem transfer plus SFTP/web SFTP plugins",
rabby_design: "Zmodem/SFTP transfer surface reserved for SSH sessions",
status: FeatureStatus::Planned,
},
FeatureSpec {
key: "encrypted-secrets",
title: "Encrypted secret container",
area: FeatureArea::Security,
tabby_reference: "Integrated encrypted container for SSH secrets and configuration",
rabby_design: "Tauri/Rust secret-store boundary",
status: FeatureStatus::Planned,
},
FeatureSpec {
key: "themes-color-schemes",
title: "Themes and color schemes",
area: FeatureArea::UiUx,
tabby_reference: "Theming and installable color schemes",
rabby_design: "Profile color_scheme and UI theme prototype",
status: FeatureStatus::Modeled,
},
FeatureSpec {
key: "shortcuts",
title: "Configurable shortcuts",
area: FeatureArea::UiUx,
tabby_reference: "Fully configurable shortcuts and multi-chord shortcuts",
rabby_design: "Shortcut feature catalog and command palette UI",
status: FeatureStatus::UiPrototype,
},
FeatureSpec {
key: "plugins",
title: "Plugins and themes installable from settings",
area: FeatureArea::Extensibility,
tabby_reference: "Plugin manager and plugin ecosystem",
rabby_design: "Extension boundary reserved outside core runtime",
status: FeatureStatus::Planned,
},
FeatureSpec {
key: "portable-web",
title: "Portable and web surfaces",
area: FeatureArea::Platform,
tabby_reference: "Portable app mode and SSH/SFTP/Telnet web app",
rabby_design: "Static UI + Tauri shell; web surface tracked as platform backlog",
status: FeatureStatus::UiPrototype,
},
];
pub fn all_features() -> &'static [FeatureSpec] {
TABBY_FEATURES
}
pub fn feature_by_key(key: &str) -> Option<&'static FeatureSpec> {
TABBY_FEATURES.iter().find(|feature| feature.key == key)
}
pub fn feature_matrix() -> Vec<(&'static str, &'static str)> {
TABBY_FEATURES
.iter()
.map(|feature| (feature.title, feature.rabby_design))
.collect()
}
pub fn missing_required_features<'a>(required_keys: &'a [&'a str]) -> Vec<&'a str> {
required_keys
.iter()
.copied()
.filter(|key| feature_by_key(key).is_none())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
const TABBY_README_REQUIREMENTS: &[&str] = &[
"vt-terminal",
"nested-splits",
"tabs-any-side",
"quake-hotkey",
"progress-notifications",
"paste-safety",
"font-ligatures-unicode",
"local-shell-profiles",
"ssh-client",
"ssh-forwarding",
"telnet-client",
"serial-terminal",
"zmodem-sftp",
"encrypted-secrets",
"themes-color-schemes",
"shortcuts",
"plugins",
"portable-web",
];
#[test]
fn catalog_covers_tabby_readme_feature_classes() {
assert_eq!(
missing_required_features(TABBY_README_REQUIREMENTS),
Vec::<&str>::new()
);
}
#[test]
fn catalog_keys_are_unique() {
let mut seen = HashSet::new();
for feature in TABBY_FEATURES {
assert!(
seen.insert(feature.key),
"duplicate feature key {}",
feature.key
);
}
}
#[test]
fn catalog_has_no_empty_user_facing_fields() {
for feature in TABBY_FEATURES {
assert!(!feature.title.trim().is_empty());
assert!(!feature.tabby_reference.trim().is_empty());
assert!(!feature.rabby_design.trim().is_empty());
}
}
#[test]
fn catalog_is_honest_about_planned_work() {
let planned = TABBY_FEATURES
.iter()
.filter(|feature| feature.status == FeatureStatus::Planned)
.count();
assert!(
planned >= 5,
"scaffold should not imply every Tabby feature is complete"
);
}
}
+10 -197
View File
@@ -3,169 +3,17 @@
//! This crate is intentionally dependency-light so terminal/profile/session behavior can be
//! unit-tested without starting the desktop shell.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConnectionKind {
LocalShell,
Ssh,
Telnet,
Serial,
}
pub mod feature;
pub mod workspace;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Profile {
pub id: String,
pub name: String,
pub kind: ConnectionKind,
pub command: String,
pub color_scheme: String,
}
impl Profile {
pub fn new(id: &str, name: &str, kind: ConnectionKind, command: &str) -> Result<Self, String> {
if id.trim().is_empty() {
return Err("profile id is required".into());
}
if name.trim().is_empty() {
return Err("profile name is required".into());
}
if command.trim().is_empty() {
return Err("profile command is required".into());
}
Ok(Self {
id: slug(id),
name: name.trim().to_string(),
kind,
command: command.trim().to_string(),
color_scheme: "Rabby Dark".to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaneNode {
Leaf {
profile_id: String,
},
Split {
axis: SplitAxis,
ratio_percent: u8,
first: Box<PaneNode>,
second: Box<PaneNode>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitAxis {
Horizontal,
Vertical,
}
impl PaneNode {
pub fn leaf(profile_id: impl Into<String>) -> Self {
Self::Leaf {
profile_id: profile_id.into(),
}
}
pub fn split(
self,
axis: SplitAxis,
other: PaneNode,
ratio_percent: u8,
) -> Result<Self, String> {
if !(10..=90).contains(&ratio_percent) {
return Err("split ratio must be between 10 and 90 percent".into());
}
Ok(Self::Split {
axis,
ratio_percent,
first: Box::new(self),
second: Box::new(other),
})
}
pub fn leaf_count(&self) -> usize {
match self {
PaneNode::Leaf { .. } => 1,
PaneNode::Split { first, second, .. } => first.leaf_count() + second.leaf_count(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tab {
pub title: String,
pub root: PaneNode,
pub progress: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Workspace {
pub profiles: Vec<Profile>,
pub tabs: Vec<Tab>,
pub active_tab: usize,
pub quake_mode: bool,
}
pub use feature::{
all_features, feature_by_key, feature_matrix, FeatureArea, FeatureSpec, FeatureStatus,
};
pub use workspace::{ConnectionKind, PaneNode, Profile, SplitAxis, Tab, Workspace};
impl Workspace {
pub fn default_linux() -> Self {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
let profile = Profile::new("local", "Local Shell", ConnectionKind::LocalShell, &shell)
.expect("static default profile is valid");
Self {
profiles: vec![profile.clone()],
tabs: vec![Tab {
title: profile.name.clone(),
root: PaneNode::leaf(profile.id),
progress: None,
}],
active_tab: 0,
quake_mode: false,
}
}
pub fn add_tab(&mut self, profile_id: &str) -> Result<(), String> {
let profile = self
.profiles
.iter()
.find(|p| p.id == profile_id)
.ok_or_else(|| format!("unknown profile: {profile_id}"))?;
self.tabs.push(Tab {
title: profile.name.clone(),
root: PaneNode::leaf(profile.id.clone()),
progress: None,
});
self.active_tab = self.tabs.len() - 1;
Ok(())
}
pub fn feature_matrix() -> Vec<(&'static str, &'static str)> {
vec![
(
"Terminal",
"VT-style terminal surface, tabs, nested split panes, Unicode-first rendering",
),
(
"Connections",
"Local shell, SSH, Telnet, and Serial profile model",
),
(
"UX",
"Command palette, configurable shortcuts, quake-mode window, remembered sessions",
),
(
"Transfers",
"Reserved Zmodem/SFTP transfer surface for SSH sessions",
),
(
"Security",
"Encrypted secret-store boundary in the Tauri/Rust backend",
),
(
"Footprint",
"Rust core + Tauri shell instead of Electron/Angular runtime",
),
]
feature_matrix()
}
}
@@ -194,43 +42,7 @@ mod tests {
}
#[test]
fn profile_validation_rejects_empty_command() {
let err = Profile::new("p", "Prod", ConnectionKind::Ssh, " ").unwrap_err();
assert!(err.contains("command"));
}
#[test]
fn split_panes_count_nested_leaves() {
let root = PaneNode::leaf("local")
.split(SplitAxis::Horizontal, PaneNode::leaf("ssh"), 50)
.unwrap()
.split(SplitAxis::Vertical, PaneNode::leaf("serial"), 65)
.unwrap();
assert_eq!(root.leaf_count(), 3);
}
#[test]
fn split_ratio_has_sane_bounds() {
assert!(PaneNode::leaf("a")
.split(SplitAxis::Horizontal, PaneNode::leaf("b"), 5)
.is_err());
assert!(PaneNode::leaf("a")
.split(SplitAxis::Horizontal, PaneNode::leaf("b"), 50)
.is_ok());
}
#[test]
fn workspace_adds_tabs_for_known_profiles() {
let mut ws = Workspace::default_linux();
ws.profiles
.push(Profile::new("ssh-prod", "Prod SSH", ConnectionKind::Ssh, "ssh prod").unwrap());
ws.add_tab("ssh-prod").unwrap();
assert_eq!(ws.active_tab, 1);
assert_eq!(ws.tabs[1].title, "Prod SSH");
}
#[test]
fn feature_matrix_covers_tabby_readme_capabilities() {
fn exported_feature_matrix_mentions_tabby_capabilities() {
let text = Workspace::feature_matrix()
.iter()
.map(|(a, b)| format!("{a} {b}"))
@@ -238,7 +50,8 @@ mod tests {
.join("\n")
.to_lowercase();
for needle in [
"terminal", "ssh", "telnet", "serial", "split", "unicode", "quake", "zmodem",
"terminal", "ssh", "telnet", "serial", "split", "unicode", "quake", "zmodem", "plugin",
"portable", "shortcut", "theme", "sftp", "secret",
] {
assert!(text.contains(needle), "missing {needle}");
}
+242
View File
@@ -0,0 +1,242 @@
use crate::slug;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConnectionKind {
LocalShell,
Ssh,
Telnet,
Serial,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Profile {
pub id: String,
pub name: String,
pub kind: ConnectionKind,
pub command: String,
pub color_scheme: String,
}
impl Profile {
pub fn new(id: &str, name: &str, kind: ConnectionKind, command: &str) -> Result<Self, String> {
if id.trim().is_empty() {
return Err("profile id is required".into());
}
if name.trim().is_empty() {
return Err("profile name is required".into());
}
if command.trim().is_empty() {
return Err("profile command is required".into());
}
Ok(Self {
id: slug(id),
name: name.trim().to_string(),
kind,
command: command.trim().to_string(),
color_scheme: "Rabby Dark".to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaneNode {
Leaf {
profile_id: String,
},
Split {
axis: SplitAxis,
ratio_percent: u8,
first: Box<PaneNode>,
second: Box<PaneNode>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitAxis {
Horizontal,
Vertical,
}
impl PaneNode {
pub fn leaf(profile_id: impl Into<String>) -> Self {
Self::Leaf {
profile_id: profile_id.into(),
}
}
pub fn split(
self,
axis: SplitAxis,
other: PaneNode,
ratio_percent: u8,
) -> Result<Self, String> {
if !(10..=90).contains(&ratio_percent) {
return Err("split ratio must be between 10 and 90 percent".into());
}
Ok(Self::Split {
axis,
ratio_percent,
first: Box::new(self),
second: Box::new(other),
})
}
pub fn leaf_count(&self) -> usize {
match self {
PaneNode::Leaf { .. } => 1,
PaneNode::Split { first, second, .. } => first.leaf_count() + second.leaf_count(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tab {
pub title: String,
pub root: PaneNode,
pub progress: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Workspace {
pub profiles: Vec<Profile>,
pub tabs: Vec<Tab>,
pub active_tab: usize,
pub quake_mode: bool,
}
impl Workspace {
pub fn default_linux() -> Self {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
let profile = Profile::new("local", "Local Shell", ConnectionKind::LocalShell, &shell)
.expect("static default profile is valid");
Self {
profiles: vec![profile.clone()],
tabs: vec![Tab {
title: profile.name.clone(),
root: PaneNode::leaf(profile.id),
progress: None,
}],
active_tab: 0,
quake_mode: false,
}
}
pub fn add_profile(&mut self, profile: Profile) -> Result<(), String> {
if self
.profiles
.iter()
.any(|existing| existing.id == profile.id)
{
return Err(format!("duplicate profile: {}", profile.id));
}
self.profiles.push(profile);
Ok(())
}
pub fn add_tab(&mut self, profile_id: &str) -> Result<(), String> {
let profile = self
.profiles
.iter()
.find(|p| p.id == profile_id)
.ok_or_else(|| format!("unknown profile: {profile_id}"))?;
self.tabs.push(Tab {
title: profile.name.clone(),
root: PaneNode::leaf(profile.id.clone()),
progress: None,
});
self.active_tab = self.tabs.len() - 1;
Ok(())
}
pub fn set_tab_progress(
&mut self,
tab_index: usize,
progress: Option<u8>,
) -> Result<(), String> {
if let Some(value) = progress {
if value > 100 {
return Err("progress must be between 0 and 100".into());
}
}
let tab = self
.tabs
.get_mut(tab_index)
.ok_or_else(|| format!("unknown tab index: {tab_index}"))?;
tab.progress = progress;
Ok(())
}
pub fn set_quake_mode(&mut self, enabled: bool) {
self.quake_mode = enabled;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_validation_rejects_empty_command() {
let err = Profile::new("p", "Prod", ConnectionKind::Ssh, " ").unwrap_err();
assert!(err.contains("command"));
}
#[test]
fn split_panes_count_nested_leaves() {
let root = PaneNode::leaf("local")
.split(SplitAxis::Horizontal, PaneNode::leaf("ssh"), 50)
.unwrap()
.split(SplitAxis::Vertical, PaneNode::leaf("serial"), 65)
.unwrap();
assert_eq!(root.leaf_count(), 3);
}
#[test]
fn split_ratio_has_sane_bounds() {
assert!(PaneNode::leaf("a")
.split(SplitAxis::Horizontal, PaneNode::leaf("b"), 5)
.is_err());
assert!(PaneNode::leaf("a")
.split(SplitAxis::Horizontal, PaneNode::leaf("b"), 50)
.is_ok());
}
#[test]
fn workspace_adds_tabs_for_known_profiles() {
let mut ws = Workspace::default_linux();
ws.add_profile(
Profile::new("ssh-prod", "Prod SSH", ConnectionKind::Ssh, "ssh prod").unwrap(),
)
.unwrap();
ws.add_tab("ssh-prod").unwrap();
assert_eq!(ws.active_tab, 1);
assert_eq!(ws.tabs[1].title, "Prod SSH");
}
#[test]
fn workspace_rejects_duplicate_profiles() {
let mut ws = Workspace::default_linux();
let err = ws
.add_profile(
Profile::new(
"local",
"Another Local",
ConnectionKind::LocalShell,
"/bin/sh",
)
.unwrap(),
)
.unwrap_err();
assert!(err.contains("duplicate"));
}
#[test]
fn workspace_tracks_quake_mode_and_progress() {
let mut ws = Workspace::default_linux();
ws.set_quake_mode(true);
ws.set_tab_progress(0, Some(42)).unwrap();
assert!(ws.quake_mode);
assert_eq!(ws.tabs[0].progress, Some(42));
assert!(ws.set_tab_progress(0, Some(101)).is_err());
}
}