Refactor capabilities to be version instead

pull/189/head
Chip Senkbeil 1 year ago
parent 5dbd4fbec8
commit 93657af270
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

@ -64,13 +64,13 @@ the available features and which backend supports each feature:
| Feature | distant | ssh |
| --------------------- | --------| ----|
| Capabilities | ✅ | ✅ |
| Filesystem I/O | ✅ | ✅ |
| Filesystem Watching | ✅ | ✅ |
| Process Execution | ✅ | ✅ |
| Reconnect | ✅ | ❌ |
| Search | ✅ | ❌ |
| System Information | ✅ | ⚠ |
| System Information | ✅ | ⚠ |
| Version | ✅ | ✅ |
* ✅ means full support
* ⚠ means partial support
@ -78,7 +78,6 @@ the available features and which backend supports each feature:
### Feature Details
* `Capabilities` - able to report back what it is capable of performing
* `Filesystem I/O` - able to read from and write to the filesystem
* `Filesystem Watching` - able to receive notifications when changes to the
filesystem occur
@ -86,6 +85,7 @@ the available features and which backend supports each feature:
* `Reconnect` - able to reconnect after network outages
* `Search` - able to search the filesystem
* `System Information` - able to retrieve information about the system
* `Version` - able to report back version information
## Example

@ -8,8 +8,8 @@ use distant_net::server::{ConnectionCtx, Reply, ServerCtx, ServerHandler};
use log::*;
use crate::protocol::{
self, Capabilities, ChangeKind, DirEntry, Environment, Error, Metadata, Permissions, ProcessId,
PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo,
self, ChangeKind, DirEntry, Environment, Error, Metadata, Permissions, ProcessId, PtySize,
SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, Version,
};
mod local;
@ -76,8 +76,8 @@ pub trait DistantApi {
///
/// *Override this, otherwise it will return "unsupported" as an error.*
#[allow(unused_variables)]
async fn capabilities(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Capabilities> {
unsupported("capabilities")
async fn version(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Version> {
unsupported("version")
}
/// Reads bytes from a file.
@ -536,11 +536,11 @@ where
D: Send + Sync,
{
match request {
protocol::Request::Capabilities {} => server
protocol::Request::Version {} => server
.api
.capabilities(ctx)
.version(ctx)
.await
.map(|supported| protocol::Response::Capabilities { supported })
.map(protocol::Response::Version)
.unwrap_or_else(protocol::Response::from),
protocol::Request::FileRead { path } => server
.api

@ -11,6 +11,7 @@ use walkdir::WalkDir;
use crate::protocol::{
Capabilities, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata,
Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo,
Version, PROTOCOL_VERSION,
};
use crate::{DistantApi, DistantCtx};
@ -40,12 +41,6 @@ impl LocalDistantApi {
impl DistantApi for LocalDistantApi {
type LocalData = ();
async fn capabilities(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Capabilities> {
debug!("[Conn {}] Querying capabilities", ctx.connection_id);
Ok(Capabilities::all())
}
async fn read_file(
&self,
ctx: DistantCtx<Self::LocalData>,
@ -689,6 +684,16 @@ impl DistantApi for LocalDistantApi {
},
})
}
async fn version(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Version> {
debug!("[Conn {}] Querying version", ctx.connection_id);
Ok(Version {
server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
protocol_version: PROTOCOL_VERSION,
capabilities: Capabilities::all(),
})
}
}
#[cfg(test)]

@ -11,8 +11,8 @@ use crate::client::{
Watcher,
};
use crate::protocol::{
self, Capabilities, ChangeKindSet, DirEntry, Environment, Error as Failure, Metadata,
Permissions, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo,
self, ChangeKindSet, DirEntry, Environment, Error as Failure, Metadata, Permissions, PtySize,
SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, Version,
};
pub type AsyncReturn<'a, T, E = io::Error> =
@ -38,9 +38,6 @@ pub trait DistantChannelExt {
data: impl Into<String>,
) -> AsyncReturn<'_, ()>;
/// Retrieves server capabilities
fn capabilities(&mut self) -> AsyncReturn<'_, Capabilities>;
/// Copies a remote file or directory from src to dst
fn copy(&mut self, src: impl Into<PathBuf>, dst: impl Into<PathBuf>) -> AsyncReturn<'_, ()>;
@ -136,6 +133,9 @@ pub trait DistantChannelExt {
/// Retrieves information about the remote system
fn system_info(&mut self) -> AsyncReturn<'_, SystemInfo>;
/// Retrieves server version information
fn version(&mut self) -> AsyncReturn<'_, Version>;
/// Writes a remote file with the data from a collection of bytes
fn write_file(
&mut self,
@ -204,18 +204,6 @@ impl DistantChannelExt
)
}
fn capabilities(&mut self) -> AsyncReturn<'_, Capabilities> {
make_body!(
self,
protocol::Request::Capabilities {},
|data| match data {
protocol::Response::Capabilities { supported } => Ok(supported),
protocol::Response::Error(x) => Err(io::Error::from(x)),
_ => Err(mismatched_response()),
}
)
}
fn copy(&mut self, src: impl Into<PathBuf>, dst: impl Into<PathBuf>) -> AsyncReturn<'_, ()> {
make_body!(
self,
@ -457,6 +445,14 @@ impl DistantChannelExt
})
}
fn version(&mut self) -> AsyncReturn<'_, Version> {
make_body!(self, protocol::Request::Version {}, |data| match data {
protocol::Response::Version(x) => Ok(x),
protocol::Response::Error(x) => Err(io::Error::from(x)),
_ => Err(mismatched_response()),
})
}
fn write_file(
&mut self,
path: impl Into<PathBuf>,

@ -8,6 +8,7 @@ mod permissions;
mod pty;
mod search;
mod system;
mod version;
pub use capabilities::*;
pub use change::*;
@ -19,6 +20,10 @@ pub use permissions::*;
pub use pty::*;
pub use search::*;
pub use system::*;
pub use version::*;
/// Id for a remote process
pub type ProcessId = u32;
/// Version indicated by the tuple of (major, minor, patch).
pub type SemVer = (u8, u8, u8);

@ -1,7 +1,7 @@
use std::cmp::Ordering;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::ops::{BitAnd, BitOr, BitXor};
use std::ops::{BitAnd, BitOr, BitXor, Deref, DerefMut};
use std::str::FromStr;
use derive_more::{From, Into, IntoIterator};
@ -72,6 +72,32 @@ impl Capabilities {
}
}
impl AsRef<HashSet<Capability>> for Capabilities {
fn as_ref(&self) -> &HashSet<Capability> {
&self.0
}
}
impl AsMut<HashSet<Capability>> for Capabilities {
fn as_mut(&mut self) -> &mut HashSet<Capability> {
&mut self.0
}
}
impl Deref for Capabilities {
type Target = HashSet<Capability>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Capabilities {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl BitAnd for &Capabilities {
type Output = Capabilities;

@ -0,0 +1,130 @@
use serde::{Deserialize, Serialize};
use crate::common::{Capabilities, SemVer};
/// Represents version information.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Version {
/// General version of server (arbitrary format)
pub server_version: String,
/// Protocol version
pub protocol_version: SemVer,
/// Capabilities of the server
pub capabilities: Capabilities,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::Capability;
#[test]
fn should_be_able_to_serialize_to_json() {
let version = Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: [Capability {
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
};
let value = serde_json::to_value(version).unwrap();
assert_eq!(
value,
serde_json::json!({
"server_version": "some version",
"protocol_version": [1, 2, 3],
"capabilities": [{
"kind": "some kind",
"description": "some description",
}]
})
);
}
#[test]
fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!({
"server_version": "some version",
"protocol_version": [1, 2, 3],
"capabilities": [{
"kind": "some kind",
"description": "some description",
}]
});
let version: Version = serde_json::from_value(value).unwrap();
assert_eq!(
version,
Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: [Capability {
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
}
);
}
#[test]
fn should_be_able_to_serialize_to_msgpack() {
let version = Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: [Capability {
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
};
// NOTE: We don't actually check the output here because it's an implementation detail
// and could change as we change how serialization is done. This is merely to verify
// that we can serialize since there are times when serde fails to serialize at
// runtime.
let _ = rmp_serde::encode::to_vec_named(&version).unwrap();
}
#[test]
fn should_be_able_to_deserialize_from_msgpack() {
// NOTE: It may seem odd that we are serializing just to deserialize, but this is to
// verify that we are not corrupting or causing issues when serializing on a
// client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(&Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: [Capability {
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
})
.unwrap();
let version: Version = rmp_serde::decode::from_slice(&buf).unwrap();
assert_eq!(
version,
Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: [Capability {
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
}
);
}
}

@ -14,4 +14,4 @@ pub use response::*;
/// This is different from the crate version, which matches that of the complete suite of distant
/// crates. Rather, this verison is used to provide stability indicators when the protocol itself
/// changes across crate versions.
pub const VERSION: (u8, u8, u8) = (0, 1, 0);
pub const PROTOCOL_VERSION: SemVer = (0, 1, 0);

@ -32,10 +32,6 @@ pub type Environment = HashMap<String, String>;
#[strum_discriminants(strum(serialize_all = "snake_case"))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
pub enum Request {
/// Retrieve information about the server's capabilities
#[strum_discriminants(strum(message = "Supports retrieving capabilities"))]
Capabilities {},
/// Reads a file from the specified path on the remote machine
#[strum_discriminants(strum(message = "Supports reading binary file"))]
FileRead {
@ -307,62 +303,16 @@ pub enum Request {
/// Retrieve information about the server and the system it is on
#[strum_discriminants(strum(message = "Supports retrieving system information"))]
SystemInfo {},
/// Retrieve information about the server's protocol version
#[strum_discriminants(strum(message = "Supports retrieving version"))]
Version {},
}
#[cfg(test)]
mod tests {
use super::*;
mod capabilities {
use super::*;
#[test]
fn should_be_able_to_serialize_to_json() {
let payload = Request::Capabilities {};
let value = serde_json::to_value(payload).unwrap();
assert_eq!(
value,
serde_json::json!({
"type": "capabilities",
})
);
}
#[test]
fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!({
"type": "capabilities",
});
let payload: Request = serde_json::from_value(value).unwrap();
assert_eq!(payload, Request::Capabilities {});
}
#[test]
fn should_be_able_to_serialize_to_msgpack() {
let payload = Request::Capabilities {};
// NOTE: We don't actually check the output here because it's an implementation detail
// and could change as we change how serialization is done. This is merely to verify
// that we can serialize since there are times when serde fails to serialize at
// runtime.
let _ = rmp_serde::encode::to_vec_named(&payload).unwrap();
}
#[test]
fn should_be_able_to_deserialize_from_msgpack() {
// NOTE: It may seem odd that we are serializing just to deserialize, but this is to
// verify that we are not corrupting or causing issues when serializing on a
// client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(&Request::Capabilities {}).unwrap();
let payload: Request = rmp_serde::decode::from_slice(&buf).unwrap();
assert_eq!(payload, Request::Capabilities {});
}
}
mod file_read {
use super::*;
@ -2978,4 +2928,54 @@ mod tests {
assert_eq!(payload, Request::SystemInfo {});
}
}
mod version {
use super::*;
#[test]
fn should_be_able_to_serialize_to_json() {
let payload = Request::Version {};
let value = serde_json::to_value(payload).unwrap();
assert_eq!(
value,
serde_json::json!({
"type": "version",
})
);
}
#[test]
fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!({
"type": "version",
});
let payload: Request = serde_json::from_value(value).unwrap();
assert_eq!(payload, Request::Version {});
}
#[test]
fn should_be_able_to_serialize_to_msgpack() {
let payload = Request::Version {};
// NOTE: We don't actually check the output here because it's an implementation detail
// and could change as we change how serialization is done. This is merely to verify
// that we can serialize since there are times when serde fails to serialize at
// runtime.
let _ = rmp_serde::encode::to_vec_named(&payload).unwrap();
}
#[test]
fn should_be_able_to_deserialize_from_msgpack() {
// NOTE: It may seem odd that we are serializing just to deserialize, but this is to
// verify that we are not corrupting or causing issues when serializing on a
// client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(&Request::Version {}).unwrap();
let payload: Request = rmp_serde::decode::from_slice(&buf).unwrap();
assert_eq!(payload, Request::Version {});
}
}
}

@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize};
use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString};
use crate::common::{
Capabilities, Change, DirEntry, Error, Metadata, ProcessId, SearchId, SearchQueryMatch,
SystemInfo,
Change, DirEntry, Error, Metadata, ProcessId, SearchId, SearchQueryMatch, SystemInfo, Version,
};
/// Represents the payload of a successful response
@ -133,8 +132,8 @@ pub enum Response {
/// Response to retrieving information about the server and the system it is on
SystemInfo(SystemInfo),
/// Response to retrieving information about the server's capabilities
Capabilities { supported: Capabilities },
/// Response to retrieving information about the server's version
Version(Version),
}
impl From<io::Error> for Response {
@ -1880,27 +1879,31 @@ mod tests {
}
}
mod capabilities {
mod version {
use super::*;
use crate::common::Capability;
use crate::common::{Capabilities, Capability};
#[test]
fn should_be_able_to_serialize_to_json() {
let payload = Response::Capabilities {
supported: [Capability {
let payload = Response::Version(Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: [Capability {
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
};
});
let value = serde_json::to_value(payload).unwrap();
assert_eq!(
value,
serde_json::json!({
"type": "capabilities",
"supported": [{
"type": "version",
"server_version": "some version",
"protocol_version": [1, 2, 3],
"capabilities": [{
"kind": "some kind",
"description": "some description",
}],
@ -1911,24 +1914,30 @@ mod tests {
#[test]
fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!({
"type": "capabilities",
"supported": Capabilities::all(),
"type": "version",
"server_version": "some version",
"protocol_version": [1, 2, 3],
"capabilities": Capabilities::all(),
});
let payload: Response = serde_json::from_value(value).unwrap();
assert_eq!(
payload,
Response::Capabilities {
supported: Capabilities::all(),
}
Response::Version(Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: Capabilities::all(),
})
);
}
#[test]
fn should_be_able_to_serialize_to_msgpack() {
let payload = Response::Capabilities {
supported: Capabilities::all(),
};
let payload = Response::Version(Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: Capabilities::all(),
});
// NOTE: We don't actually check the errput here because it's an implementation detail
// and could change as we change how serialization is done. This is merely to verify
@ -1943,17 +1952,21 @@ mod tests {
// verify that we are not corrupting or causing issues when serializing on a
// client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(&Response::Capabilities {
supported: Capabilities::all(),
})
let buf = rmp_serde::encode::to_vec_named(&Response::Version(Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: Capabilities::all(),
}))
.unwrap();
let payload: Response = rmp_serde::decode::from_slice(&buf).unwrap();
assert_eq!(
payload,
Response::Capabilities {
supported: Capabilities::all(),
}
Response::Version(Version {
server_version: String::from("some version"),
protocol_version: (1, 2, 3),
capabilities: Capabilities::all(),
})
);
}
}

@ -10,7 +10,7 @@ use async_trait::async_trait;
use distant_core::net::server::ConnectionCtx;
use distant_core::protocol::{
Capabilities, CapabilityKind, DirEntry, Environment, FileType, Metadata, Permissions,
ProcessId, PtySize, SetPermissionsOptions, SystemInfo, UnixMetadata,
ProcessId, PtySize, SetPermissionsOptions, SystemInfo, UnixMetadata, Version, PROTOCOL_VERSION,
};
use distant_core::{DistantApi, DistantCtx};
use log::*;
@ -79,22 +79,6 @@ impl DistantApi for SshDistantApi {
Ok(())
}
async fn capabilities(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Capabilities> {
debug!("[Conn {}] Querying capabilities", ctx.connection_id);
let mut capabilities = Capabilities::all();
// Searching is not supported by ssh implementation
// TODO: Could we have external search using ripgrep's JSON lines API?
capabilities.take(CapabilityKind::Search);
capabilities.take(CapabilityKind::CancelSearch);
// Broken via wezterm-ssh, so not supported right now
capabilities.take(CapabilityKind::SetPermissions);
Ok(capabilities)
}
async fn read_file(
&self,
ctx: DistantCtx<Self::LocalData>,
@ -1013,4 +997,24 @@ impl DistantApi for SshDistantApi {
shell,
})
}
async fn version(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Version> {
debug!("[Conn {}] Querying capabilities", ctx.connection_id);
let mut capabilities = Capabilities::all();
// Searching is not supported by ssh implementation
// TODO: Could we have external search using ripgrep's JSON lines API?
capabilities.take(CapabilityKind::Search);
capabilities.take(CapabilityKind::CancelSearch);
// Broken via wezterm-ssh, so not supported right now
capabilities.take(CapabilityKind::SetPermissions);
Ok(Version {
server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
protocol_version: PROTOCOL_VERSION,
capabilities,
})
}
}

@ -7,7 +7,8 @@ use anyhow::Context;
use distant_core::net::common::{ConnectionId, Host, Map, Request, Response};
use distant_core::net::manager::ManagerClient;
use distant_core::protocol::{
self, ChangeKindSet, FileType, Permissions, SearchQuery, SetPermissionsOptions, SystemInfo,
self, Capabilities, ChangeKindSet, FileType, Permissions, SearchQuery, SetPermissionsOptions,
SystemInfo,
};
use distant_core::{DistantChannel, DistantChannelExt, RemoteCommand, Searcher, Watcher};
use log::*;
@ -48,7 +49,7 @@ async fn read_cache(path: &Path) -> Cache {
async fn async_run(cmd: ClientSubcommand) -> CliResult {
match cmd {
ClientSubcommand::Capabilities {
ClientSubcommand::Version {
cache,
connection,
format,
@ -69,37 +70,83 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
format!("Failed to open raw channel to connection {connection_id}")
})?;
debug!("Retrieving capabilities");
let capabilities = channel
debug!("Retrieving version information");
let version = channel
.into_client()
.into_channel()
.capabilities()
.version()
.await
.with_context(|| {
format!("Failed to retrieve capabilities using connection {connection_id}")
format!("Failed to retrieve version using connection {connection_id}")
})?;
match format {
Format::Shell => {
#[derive(Tabled)]
struct EntryRow {
kind: String,
description: String,
}
let table = Table::new(capabilities.into_sorted_vec().into_iter().map(|cap| {
EntryRow {
kind: cap.kind,
description: cap.description,
println!("Server version: {}", version.server_version);
let (major, minor, patch) = version.protocol_version;
println!("Protocol version: {major}.{minor}.{patch}");
// Build a complete set of capabilities to show which ones we support
let client_capabilities = Capabilities::all();
let server_capabilities = version.capabilities;
let mut capabilities: Vec<String> = client_capabilities
.union(server_capabilities.as_ref())
.map(|cap| {
let kind = &cap.kind;
if client_capabilities.contains(kind)
&& server_capabilities.contains(kind)
{
format!("+{kind}")
} else {
format!("-{kind}")
}
})
.collect();
capabilities.sort_unstable();
// Figure out the text length of the longest capability
let max_len = capabilities.iter().map(|x| x.len()).max().unwrap_or(0);
if max_len > 0 {
const MAX_COLS: usize = 4;
// Determine how wide we have available to determine how many columns
// to use; if we don't have a terminal width, default to something
//
// Maximum columns we want to support is 4
let cols = match terminal_size::terminal_size() {
// If we have a tty, see how many we can fit including space char
//
// Ensure that we at least return 1 as cols
Some((width, _)) => std::cmp::max(width.0 as usize / (max_len + 1), 1),
// If we have no tty, default to 4 columns
None => MAX_COLS,
};
println!("Capabilities supported (+) or not (-):");
for chunk in capabilities.chunks(std::cmp::min(cols, MAX_COLS)) {
let cnt = chunk.len();
match cnt {
1 => println!("{:max_len$}", chunk[0]),
2 => println!("{:max_len$} {:max_len$}", chunk[0], chunk[1]),
3 => println!(
"{:max_len$} {:max_len$} {:max_len$}",
chunk[0], chunk[1], chunk[2]
),
4 => println!(
"{:max_len$} {:max_len$} {:max_len$} {:max_len$}",
chunk[0], chunk[1], chunk[2], chunk[3]
),
_ => unreachable!("Chunk of size {cnt} is not 1 > i <= {MAX_COLS}"),
}
}
}))
.with(Style::ascii())
.with(Modify::new(Rows::new(..)).with(Alignment::left()))
.to_string();
println!("{table}");
}
}
Format::Json => {
println!("{}", serde_json::to_string(&version).unwrap())
}
Format::Json => println!("{}", serde_json::to_string(&capabilities).unwrap()),
}
}
ClientSubcommand::Connect {

@ -375,17 +375,23 @@ fn format_shell(state: &mut FormatterState, data: protocol::Response) -> Output
)
.into_bytes(),
),
protocol::Response::Capabilities { supported } => {
protocol::Response::Version(version) => {
#[derive(Tabled)]
struct EntryRow {
kind: String,
description: String,
}
let table = Table::new(supported.into_sorted_vec().into_iter().map(|cap| EntryRow {
kind: cap.kind,
description: cap.description,
}))
let table = Table::new(
version
.capabilities
.into_sorted_vec()
.into_iter()
.map(|cap| EntryRow {
kind: cap.kind,
description: cap.description,
}),
)
.with(Style::ascii())
.with(Modify::new(Rows::new(..)).with(Alignment::left()))
.to_string()

@ -103,7 +103,7 @@ impl Options {
network.merge(config.client.network);
*timeout = timeout.take().or(config.client.api.timeout);
}
ClientSubcommand::Capabilities { network, .. } => {
ClientSubcommand::Version { network, .. } => {
network.merge(config.client.network);
}
ClientSubcommand::Connect {
@ -264,7 +264,7 @@ pub enum ClientSubcommand {
},
/// Retrieves capabilities of the remote server
Capabilities {
Version {
/// Location to store cached data
#[clap(
long,
@ -463,7 +463,7 @@ pub enum ClientSubcommand {
impl ClientSubcommand {
pub fn cache_path(&self) -> &Path {
match self {
Self::Capabilities { cache, .. } => cache.as_path(),
Self::Version { cache, .. } => cache.as_path(),
Self::Connect { cache, .. } => cache.as_path(),
Self::FileSystem(fs) => fs.cache_path(),
Self::Launch { cache, .. } => cache.as_path(),
@ -476,7 +476,7 @@ impl ClientSubcommand {
pub fn network_settings(&self) -> &NetworkSettings {
match self {
Self::Capabilities { network, .. } => network,
Self::Version { network, .. } => network,
Self::Connect { network, .. } => network,
Self::FileSystem(fs) => fs.network_settings(),
Self::Launch { network, .. } => network,
@ -1265,7 +1265,7 @@ mod tests {
log_file: None,
log_level: None,
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
command: DistantSubcommand::Client(ClientSubcommand::Version {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {
@ -1302,7 +1302,7 @@ mod tests {
log_file: Some(PathBuf::from("config-log-file")),
log_level: Some(LogLevel::Trace),
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
command: DistantSubcommand::Client(ClientSubcommand::Version {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {
@ -1323,7 +1323,7 @@ mod tests {
log_file: Some(PathBuf::from("cli-log-file")),
log_level: Some(LogLevel::Info),
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
command: DistantSubcommand::Client(ClientSubcommand::Version {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {
@ -1360,7 +1360,7 @@ mod tests {
log_file: Some(PathBuf::from("cli-log-file")),
log_level: Some(LogLevel::Info),
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
command: DistantSubcommand::Client(ClientSubcommand::Version {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {

@ -1,4 +1,3 @@
mod capabilities;
mod copy;
mod dir_create;
mod dir_read;
@ -15,4 +14,5 @@ mod remove;
mod rename;
mod search;
mod system_info;
mod version;
mod watch;

@ -1,4 +1,4 @@
use distant_core::protocol::{Capabilities, Capability};
use distant_core::protocol::{Capabilities, Capability, SemVer, PROTOCOL_VERSION};
use rstest::*;
use serde_json::json;
use test_log::test;
@ -13,15 +13,19 @@ async fn should_support_json_capabilities(mut api_process: CtxCommand<ApiProcess
let id = rand::random::<u64>().to_string();
let req = json!({
"id": id,
"payload": { "type": "capabilities" },
"payload": { "type": "version" },
});
let res = api_process.write_and_read_json(req).await.unwrap().unwrap();
assert_eq!(res["origin_id"], id, "JSON: {res}");
assert_eq!(res["payload"]["type"], "capabilities", "JSON: {res}");
assert_eq!(res["payload"]["type"], "version", "JSON: {res}");
let supported: Capabilities = res["payload"]["supported"]
let protocol_version: SemVer =
serde_json::from_value(res["payload"]["protocol_version"].clone()).unwrap();
assert_eq!(protocol_version, PROTOCOL_VERSION);
let capabilities: Capabilities = res["payload"]["capabilities"]
.as_array()
.expect("Field 'supported' was not an array")
.iter()
@ -33,5 +37,5 @@ async fn should_support_json_capabilities(mut api_process: CtxCommand<ApiProcess
// NOTE: Our local server api should always support all capabilities since it is the reference
// implementation for our api
assert_eq!(supported, Capabilities::all());
assert_eq!(capabilities, Capabilities::all());
}

@ -1,68 +0,0 @@
use indoc::indoc;
use rstest::*;
use crate::cli::fixtures::*;
const EXPECTED_TABLE: &str = indoc! {"
+------------------+------------------------------------------------------------------+
| kind | description |
+------------------+------------------------------------------------------------------+
| cancel_search | Supports canceling an active search against the filesystem |
+------------------+------------------------------------------------------------------+
| capabilities | Supports retrieving capabilities |
+------------------+------------------------------------------------------------------+
| copy | Supports copying files, directories, and symlinks |
+------------------+------------------------------------------------------------------+
| dir_create | Supports creating directory |
+------------------+------------------------------------------------------------------+
| dir_read | Supports reading directory |
+------------------+------------------------------------------------------------------+
| exists | Supports checking if a path exists |
+------------------+------------------------------------------------------------------+
| file_append | Supports appending to binary file |
+------------------+------------------------------------------------------------------+
| file_append_text | Supports appending to text file |
+------------------+------------------------------------------------------------------+
| file_read | Supports reading binary file |
+------------------+------------------------------------------------------------------+
| file_read_text | Supports reading text file |
+------------------+------------------------------------------------------------------+
| file_write | Supports writing binary file |
+------------------+------------------------------------------------------------------+
| file_write_text | Supports writing text file |
+------------------+------------------------------------------------------------------+
| metadata | Supports retrieving metadata about a file, directory, or symlink |
+------------------+------------------------------------------------------------------+
| proc_kill | Supports killing a spawned process |
+------------------+------------------------------------------------------------------+
| proc_resize_pty | Supports resizing the pty of a spawned process |
+------------------+------------------------------------------------------------------+
| proc_spawn | Supports spawning a process |
+------------------+------------------------------------------------------------------+
| proc_stdin | Supports sending stdin to a spawned process |
+------------------+------------------------------------------------------------------+
| remove | Supports removing files, directories, and symlinks |
+------------------+------------------------------------------------------------------+
| rename | Supports renaming files, directories, and symlinks |
+------------------+------------------------------------------------------------------+
| search | Supports searching filesystem using queries |
+------------------+------------------------------------------------------------------+
| set_permissions | Supports setting permissions on a file, directory, or symlink |
+------------------+------------------------------------------------------------------+
| system_info | Supports retrieving system information |
+------------------+------------------------------------------------------------------+
| unwatch | Supports unwatching filesystem for changes |
+------------------+------------------------------------------------------------------+
| watch | Supports watching filesystem for changes |
+------------------+------------------------------------------------------------------+
"};
#[rstest]
#[test_log::test]
fn should_output_capabilities(ctx: DistantManagerCtx) {
ctx.cmd("capabilities")
.assert()
.success()
.stdout(EXPECTED_TABLE)
.stderr("");
}

@ -1,4 +1,3 @@
mod capabilities;
mod fs_copy;
mod fs_exists;
mod fs_make_dir;
@ -12,3 +11,5 @@ mod fs_watch;
mod fs_write;
mod spawn;
mod system_info;
mod version;

@ -0,0 +1,34 @@
use distant_core::protocol::PROTOCOL_VERSION;
use rstest::*;
use crate::cli::fixtures::*;
use crate::cli::utils::TrimmedLinesMatchPredicate;
#[rstest]
#[test_log::test]
fn should_output_capabilities(ctx: DistantManagerCtx) {
// Because all of our crates have the same version, we can expect it to match
let package_name = "distant-core";
let package_version = env!("CARGO_PKG_VERSION");
let (major, minor, patch) = PROTOCOL_VERSION;
// Since our client and server are built the same, all capabilities should be listed with +
// and using 4 columns since we are not using a tty
let expected = indoc::formatdoc! {"
Server version: {package_name} {package_version}
Protocol version: {major}.{minor}.{patch}
Capabilities supported (+) or not (-):
+cancel_search +copy +dir_create +dir_read
+exists +file_append +file_append_text +file_read
+file_read_text +file_write +file_write_text +metadata
+proc_kill +proc_resize_pty +proc_spawn +proc_stdin
+remove +rename +search +set_permissions
+system_info +unwatch +version +watch
"};
ctx.cmd("version")
.assert()
.success()
.stdout(TrimmedLinesMatchPredicate::new(expected))
.stderr("");
}

@ -1,9 +1,12 @@
use predicates::prelude::*;
use ::predicates::prelude::*;
mod predicates;
mod reader;
pub use self::predicates::TrimmedLinesMatchPredicate;
pub use reader::ThreadedReader;
/// Produces a regex predicate using the given string
pub fn regex_pred(s: &str) -> predicates::str::RegexPredicate {
pub fn regex_pred(s: &str) -> ::predicates::str::RegexPredicate {
predicate::str::is_match(s).unwrap()
}

@ -0,0 +1,50 @@
use predicates::reflection::PredicateReflection;
use predicates::Predicate;
use std::fmt;
/// Checks if lines of text match the provided, trimming each line
/// of both before comparing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrimmedLinesMatchPredicate {
pattern: String,
}
impl TrimmedLinesMatchPredicate {
pub fn new(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
}
}
}
impl fmt::Display for TrimmedLinesMatchPredicate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "trimmed_lines expects {}", self.pattern)
}
}
impl Predicate<str> for TrimmedLinesMatchPredicate {
fn eval(&self, variable: &str) -> bool {
let mut expected = self.pattern.lines();
let mut actual = variable.lines();
// Fail if we don't have the same number of lines
// or of the trimmed result of lines don't match
//
// Otherwise if we finish processing all lines,
// we are a success
loop {
match (expected.next(), actual.next()) {
(Some(expected), Some(actual)) => {
if expected.trim() != actual.trim() {
return false;
}
}
(None, None) => return true,
_ => return false,
}
}
}
}
impl PredicateReflection for TrimmedLinesMatchPredicate {}
Loading…
Cancel
Save