use std::process::Command;
use rmcp::ErrorData;
use crate::{
Tool, execute_command,
serde_utils::{deserialize_string, deserialize_string_vec, output_verbosity_to_cli_flags},
};
#[derive(Debug, ::serde::Deserialize, schemars::JsonSchema)]
pub struct CargoInstaUpdateSnapshotsRequest {
#[serde(default)]
force: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
manifest_path: Option<String>,
#[serde(default, deserialize_with = "deserialize_string_vec")]
package: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_string_vec")]
exclude: Option<Vec<String>>,
#[serde(default)]
lib: Option<bool>,
#[serde(default)]
tests: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
test: Option<String>,
#[serde(default)]
all_targets: Option<bool>,
#[serde(default)]
workspace: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string_vec")]
features: Option<Vec<String>>,
#[serde(default)]
all_features: Option<bool>,
#[serde(default)]
no_default_features: Option<bool>,
#[serde(default)]
jobs: Option<u32>,
#[serde(default, deserialize_with = "deserialize_string")]
output_verbosity: Option<String>,
}
impl CargoInstaUpdateSnapshotsRequest {
pub fn build_cmd(&self) -> Result<Command, ErrorData> {
let mut cmd = Command::new("cargo");
cmd.arg("test");
let insta_update = if self.force.unwrap_or(false) {
"force"
} else {
"always"
};
cmd.env("INSTA_UPDATE", insta_update);
if let Some(manifest_path) = &self.manifest_path {
cmd.arg("--manifest-path").arg(manifest_path);
}
if let Some(packages) = &self.package {
for p in packages {
cmd.arg("--package").arg(p);
}
}
if self.workspace.unwrap_or(false) {
cmd.arg("--workspace");
}
if let Some(excludes) = &self.exclude {
for e in excludes {
cmd.arg("--exclude").arg(e);
}
}
if self.lib.unwrap_or(false) {
cmd.arg("--lib");
}
if self.tests.unwrap_or(false) {
cmd.arg("--tests");
}
if let Some(test) = &self.test {
cmd.arg("--test").arg(test);
}
if self.all_targets.unwrap_or(false) {
cmd.arg("--all-targets");
}
if let Some(features) = &self.features
&& !features.is_empty()
{
cmd.arg("--features").arg(features.join(","));
}
if self.all_features.unwrap_or(false) {
cmd.arg("--all-features");
}
if self.no_default_features.unwrap_or(false) {
cmd.arg("--no-default-features");
}
if let Some(jobs) = self.jobs {
cmd.arg("--jobs").arg(jobs.to_string());
}
let output_flags = output_verbosity_to_cli_flags(self.output_verbosity.as_deref())?;
cmd.args(output_flags);
Ok(cmd)
}
}
pub struct CargoInstaUpdateSnapshotsRmcpTool;
impl Tool for CargoInstaUpdateSnapshotsRmcpTool {
const NAME: &'static str = "cargo-insta-update-snapshots";
const TITLE: &'static str = "Update insta snapshots";
const DESCRIPTION: &'static str = "Runs `cargo test` with the `INSTA_UPDATE` environment variable to update insta snapshot files.";
type RequestArgs = CargoInstaUpdateSnapshotsRequest;
fn call_rmcp_tool(&self, request: Self::RequestArgs) -> Result<crate::Response, ErrorData> {
execute_command(request.build_cmd()?, Self::NAME).map(Into::into)
}
}
#[cfg(test)]
mod tests {
use super::CargoInstaUpdateSnapshotsRequest;
use insta::assert_debug_snapshot;
use serde_json::json;
fn cmd_args(request: &CargoInstaUpdateSnapshotsRequest) -> Vec<String> {
request
.build_cmd()
.expect("Should build command")
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect()
}
fn cmd_env_insta_update(request: &CargoInstaUpdateSnapshotsRequest) -> String {
request
.build_cmd()
.expect("Should build command")
.get_envs()
.find(|(k, _)| k == &"INSTA_UPDATE")
.and_then(|(_, v)| v)
.map(|v| v.to_string_lossy().to_string())
.unwrap_or_default()
}
#[test]
fn test_update_snapshots_default_args() {
let request: CargoInstaUpdateSnapshotsRequest =
serde_json::from_value(json!({})).expect("Should deserialize empty request");
let args = cmd_args(&request);
assert_debug_snapshot!("cargo_insta_update_snapshots_default_args", args);
assert_eq!(cmd_env_insta_update(&request), "always");
}
#[test]
fn test_update_snapshots_with_manifest_path() {
let request: CargoInstaUpdateSnapshotsRequest = serde_json::from_value(json!({
"manifest_path": "Cargo.toml"
}))
.expect("Should deserialize request");
let args = cmd_args(&request);
assert_debug_snapshot!("cargo_insta_update_snapshots_manifest_path_args", args);
}
#[test]
fn test_update_snapshots_all_features() {
let request: CargoInstaUpdateSnapshotsRequest = serde_json::from_value(json!({
"all_features": true
}))
.expect("Should deserialize request");
let args = cmd_args(&request);
assert_debug_snapshot!("cargo_insta_update_snapshots_all_features_args", args);
}
#[test]
fn test_update_snapshots_features_and_targets() {
let request: CargoInstaUpdateSnapshotsRequest = serde_json::from_value(json!({
"features": ["serde", "async"],
"no_default_features": true,
"lib": true,
"jobs": 4
}))
.expect("Should deserialize request");
let args = cmd_args(&request);
assert_debug_snapshot!(
"cargo_insta_update_snapshots_features_and_targets_args",
args
);
}
#[test]
fn test_update_snapshots_force_flag() {
let request: CargoInstaUpdateSnapshotsRequest = serde_json::from_value(json!({
"force": true
}))
.expect("Should deserialize request");
assert_eq!(cmd_env_insta_update(&request), "force");
}
}