exfiltrate 0.3.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Implements the `build_info` command.
use crate::command::{Command, Response};
use exfiltrate_internal::args::{ArgKind, ArgSpec, ParsedArgs};

/// The `build_info` command.
///
/// Answers the first question anyone asks of a program they have just attached
/// to: what is this, and what was it built from. The payload is the same
/// [`BuildInfo`](exfiltrate_internal::build_info::BuildInfo) the connection
/// handshake exchanges, built once in [`Config::build_info`](crate::Config), so
/// the handshake and this command can never disagree.
pub(crate) struct BuildInfoCommand;

static ARGS: &[ArgSpec] = &[ArgSpec::flag(
    "json",
    "return MessagePack-encoded BuildInfo instead of text",
    ArgKind::Bool,
)];

impl Command for BuildInfoCommand {
    fn name(&self) -> &'static str {
        "build_info"
    }

    fn short_description(&self) -> &'static str {
        "Reports crate versions, target triple, profile, and enabled features.  Use this first to learn what you are attached to."
    }

    fn full_description(&self) -> &'static str {
        "Reports what this program is and what it was built from.

Includes the host program's name and version, the linked `exfiltrate` version, the
target triple, the cargo profile, the enabled `exfiltrate` features, and — when the
build recorded them — a git revision and a build timestamp.

This is the same payload the connection handshake exchanges, so `exfiltrate status`
shows it too without a second round trip."
    }

    fn args(&self) -> &'static [ArgSpec] {
        ARGS
    }

    fn execute(&self, args: Vec<String>) -> Result<Response, Response> {
        let parsed = ParsedArgs::parse(self.args(), args).map_err(Response::String)?;
        let info = crate::config_snapshot().build_info();
        if parsed.boolean("json") {
            Response::from_serialize(&info)
        } else {
            Ok(info.to_string().into())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use exfiltrate_internal::build_info::BuildInfo;

    #[test]
    fn the_text_form_names_the_target_and_the_crate_version() {
        let text = BuildInfoCommand.execute(Vec::new()).unwrap().into_string();
        assert!(text.contains(env!("CARGO_PKG_VERSION")), "{text}");
        assert!(text.contains(env!("EXFILTRATE_TARGET")), "{text}");
        assert!(text.contains("protocol version"), "{text}");
    }

    #[test]
    fn the_json_form_decodes_back_into_build_info() {
        let Response::Bytes(bytes) = BuildInfoCommand
            .execute(vec!["--json".to_string()])
            .unwrap()
        else {
            panic!("--json must answer with Bytes");
        };
        let info: BuildInfo = rmp_serde::from_slice(&bytes).unwrap();
        assert_eq!(info.exfiltrate_version, env!("CARGO_PKG_VERSION"));
        assert!(info.is_compatible());
    }

    #[test]
    fn an_unknown_flag_is_refused_rather_than_ignored() {
        let error = BuildInfoCommand
            .execute(vec!["--nope".to_string()])
            .unwrap_err()
            .to_string();
        assert!(error.contains("--nope"), "{error}");
    }
}