use std::fs;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
use assert_cmd::Command;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use tempfile::tempdir;
use tiny_http::{Header, Response, Server, StatusCode};
fn write_file(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("mkdir");
}
fs::write(path, content).expect("write");
}
fn create_single_crate_workspace(root: &Path) {
write_file(
&root.join("Cargo.toml"),
r#"
[workspace]
members = ["demo"]
resolver = "2"
"#,
);
write_file(
&root.join("demo/Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
"#,
);
write_file(&root.join("demo/src/lib.rs"), "pub fn demo() {}\n");
}
fn create_multi_crate_workspace(root: &Path) {
write_file(
&root.join("Cargo.toml"),
r#"
[workspace]
members = ["core", "utils"]
resolver = "2"
"#,
);
write_file(
&root.join("core/Cargo.toml"),
r#"
[package]
name = "core"
version = "0.1.0"
edition = "2021"
"#,
);
write_file(&root.join("core/src/lib.rs"), "pub fn core() {}\n");
write_file(
&root.join("utils/Cargo.toml"),
r#"
[package]
name = "utils"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" }
"#,
);
write_file(&root.join("utils/src/lib.rs"), "pub fn utils() {}\n");
}
fn shipper_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("shipper-cli"))
}
fn path_sep() -> &'static str {
if cfg!(windows) { ";" } else { ":" }
}
fn create_fake_cargo_with_stderr(bin_dir: &Path) -> PathBuf {
#[cfg(windows)]
{
let path = bin_dir.join("cargo.cmd");
fs::write(
&path,
"@echo off\r\n\
if \"%1\"==\"publish\" (\r\n\
echo %SHIPPER_FAKE_STDERR% 1>&2\r\n\
if \"%SHIPPER_FAKE_PUBLISH_EXIT%\"==\"\" (exit /b 1) else (exit /b %SHIPPER_FAKE_PUBLISH_EXIT%)\r\n\
)\r\n\
\"%REAL_CARGO%\" %*\r\n\
exit /b %ERRORLEVEL%\r\n",
)
.expect("write fake cargo");
path
}
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
let path = bin_dir.join("cargo");
fs::write(
&path,
"#!/usr/bin/env sh\n\
if [ \"$1\" = \"publish\" ]; then\n\
echo \"$SHIPPER_FAKE_STDERR\" >&2\n\
exit \"${SHIPPER_FAKE_PUBLISH_EXIT:-1}\"\n\
fi\n\
\"$REAL_CARGO\" \"$@\"\n",
)
.expect("write fake cargo");
let mut perms = fs::metadata(&path).expect("meta").permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).expect("chmod");
path
}
}
fn create_fake_cargo_partial(bin_dir: &Path) -> PathBuf {
#[cfg(windows)]
{
let path = bin_dir.join("cargo.cmd");
fs::write(
&path,
"@echo off\r\n\
if not \"%1\"==\"publish\" goto passthrough\r\n\
set /a _cnt=0\r\n\
if exist \"%SHIPPER_FAKE_COUNTER_FILE%\" set /p _cnt=<\"%SHIPPER_FAKE_COUNTER_FILE%\"\r\n\
set /a _cnt=%_cnt%+1\r\n\
>\"%SHIPPER_FAKE_COUNTER_FILE%\" echo %_cnt%\r\n\
if %_cnt% LEQ %SHIPPER_FAKE_SUCCEED_COUNT% exit /b 0\r\n\
echo %SHIPPER_FAKE_STDERR% 1>&2\r\n\
exit /b 1\r\n\
:passthrough\r\n\
\"%REAL_CARGO%\" %*\r\n\
exit /b %ERRORLEVEL%\r\n",
)
.expect("write fake cargo");
path
}
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
let path = bin_dir.join("cargo");
fs::write(
&path,
"#!/usr/bin/env sh\n\
if [ \"$1\" = \"publish\" ]; then\n\
count=0\n\
if [ -f \"$SHIPPER_FAKE_COUNTER_FILE\" ]; then\n\
count=$(cat \"$SHIPPER_FAKE_COUNTER_FILE\")\n\
fi\n\
count=$((count + 1))\n\
echo \"$count\" > \"$SHIPPER_FAKE_COUNTER_FILE\"\n\
if [ \"$count\" -le \"$SHIPPER_FAKE_SUCCEED_COUNT\" ]; then\n\
exit 0\n\
else\n\
echo \"$SHIPPER_FAKE_STDERR\" >&2\n\
exit 1\n\
fi\n\
fi\n\
\"$REAL_CARGO\" \"$@\"\n",
)
.expect("write fake cargo");
let mut perms = fs::metadata(&path).expect("meta").permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).expect("chmod");
path
}
}
fn prepend_fake_bin(bin_dir: &Path) -> (String, String) {
let old_path = std::env::var("PATH").unwrap_or_default();
let mut new_path = bin_dir.display().to_string();
if !old_path.is_empty() {
new_path.push_str(path_sep());
new_path.push_str(&old_path);
}
let real_cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
(new_path, real_cargo)
}
struct TestRegistry {
base_url: String,
handle: thread::JoinHandle<()>,
}
impl TestRegistry {
fn join(self) {
self.handle.join().expect("join server");
}
}
fn spawn_registry(statuses: Vec<u16>, expected_requests: usize) -> TestRegistry {
let server = Server::http("127.0.0.1:0").expect("server");
let base_url = format!("http://{}", server.server_addr());
let handle = thread::spawn(move || {
for idx in 0..expected_requests {
let req = match server.recv_timeout(Duration::from_secs(30)) {
Ok(Some(r)) => r,
_ => break,
};
let status = statuses
.get(idx)
.copied()
.or_else(|| statuses.last().copied())
.unwrap_or(404);
let resp = Response::from_string("{}")
.with_status_code(StatusCode(status))
.with_header(
Header::from_bytes("Content-Type", "application/json").expect("header"),
);
req.respond(resp).expect("respond");
}
});
TestRegistry { base_url, handle }
}
mod auth_failure {
use super::*;
#[test]
fn given_401_unauthorized_when_classifying_then_permanent() {
let outcome = shipper_core::cargo_failure::classify_publish_failure("401 unauthorized", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Permanent
);
}
#[test]
fn given_invalid_token_when_classifying_then_permanent() {
let outcome = shipper_core::cargo_failure::classify_publish_failure("token is invalid", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Permanent
);
}
#[test]
fn given_not_authorized_when_classifying_then_permanent() {
let outcome =
shipper_core::cargo_failure::classify_publish_failure("not authorized to publish", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Permanent
);
}
#[test]
fn given_auth_failure_stderr_when_publish_then_cli_reports_error() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
let fake_cargo = create_fake_cargo_with_stderr(&bin_dir);
let (new_path, real_cargo) = prepend_fake_bin(&bin_dir);
let registry = spawn_registry(vec![404], 4);
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--no-readiness")
.arg("--max-attempts")
.arg("1")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "1")
.env(
"SHIPPER_FAKE_STDERR",
"error: 401 unauthorized: token is invalid",
)
.assert()
.failure()
.stderr(contains("permanent").or(contains("unauthorized").or(contains("failed"))));
registry.join();
}
}
mod rate_limiting {
use super::*;
#[test]
fn given_429_when_classifying_then_retryable() {
let outcome =
shipper_core::cargo_failure::classify_publish_failure("HTTP 429 too many requests", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_too_many_requests_when_classifying_then_retryable() {
let outcome = shipper_core::cargo_failure::classify_publish_failure(
"the remote server responded with 429 Too Many Requests",
"",
);
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_rate_limit_stderr_when_publish_then_retries_before_failing() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
let fake_cargo = create_fake_cargo_with_stderr(&bin_dir);
let (new_path, real_cargo) = prepend_fake_bin(&bin_dir);
let registry = spawn_registry(vec![404], 20);
let output = shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--no-readiness")
.arg("--max-attempts")
.arg("2")
.arg("--base-delay")
.arg("0ms")
.arg("--max-delay")
.arg("0ms")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "1")
.env("SHIPPER_FAKE_STDERR", "error: 429 too many requests")
.output()
.expect("run");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("attempt") || stderr.contains("retryable") || stderr.contains("failed"),
"expected retry-related message in stderr, got:\n{stderr}"
);
registry.join();
}
}
mod network_timeout {
use super::*;
#[test]
fn given_timeout_when_classifying_then_retryable() {
let outcome = shipper_core::cargo_failure::classify_publish_failure(
"operation timed out after 30s",
"",
);
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_dns_error_when_classifying_then_retryable() {
let outcome =
shipper_core::cargo_failure::classify_publish_failure("dns error: lookup failed", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_connection_reset_when_classifying_then_retryable() {
let outcome =
shipper_core::cargo_failure::classify_publish_failure("connection reset by peer", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_502_when_classifying_then_retryable() {
let outcome = shipper_core::cargo_failure::classify_publish_failure("502 bad gateway", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_timeout_stderr_when_publish_then_retries() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
let fake_cargo = create_fake_cargo_with_stderr(&bin_dir);
let (new_path, real_cargo) = prepend_fake_bin(&bin_dir);
let registry = spawn_registry(vec![404], 20);
let output = shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--no-readiness")
.arg("--max-attempts")
.arg("2")
.arg("--base-delay")
.arg("0ms")
.arg("--max-delay")
.arg("0ms")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "1")
.env("SHIPPER_FAKE_STDERR", "error: operation timed out")
.output()
.expect("run");
assert!(!output.status.success());
registry.join();
}
}
mod invalid_manifest {
use super::*;
#[test]
fn given_compilation_failed_when_classifying_then_permanent() {
let outcome =
shipper_core::cargo_failure::classify_publish_failure("compilation failed", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Permanent
);
}
#[test]
fn given_missing_manifest_when_plan_then_cli_errors() {
let td = tempdir().expect("tempdir");
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("nonexistent").join("Cargo.toml"))
.arg("plan")
.assert()
.failure();
}
#[test]
fn given_malformed_cargo_toml_when_plan_then_cli_errors() {
let td = tempdir().expect("tempdir");
write_file(&td.path().join("Cargo.toml"), "this is not valid TOML {{{");
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("plan")
.assert()
.failure();
}
#[test]
fn given_workspace_with_no_members_when_plan_then_cli_errors() {
let td = tempdir().expect("tempdir");
write_file(
&td.path().join("Cargo.toml"),
r#"
[workspace]
members = []
resolver = "2"
"#,
);
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("plan")
.assert()
.failure();
}
#[test]
fn given_parse_manifest_error_when_classifying_then_permanent() {
let outcome = shipper_core::cargo_failure::classify_publish_failure(
"failed to parse manifest at Cargo.toml",
"",
);
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Permanent
);
}
}
mod registry_unreachable {
use super::*;
#[test]
fn given_connection_refused_when_classifying_then_retryable() {
let outcome =
shipper_core::cargo_failure::classify_publish_failure("connection refused", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_network_unreachable_when_classifying_then_retryable() {
let outcome =
shipper_core::cargo_failure::classify_publish_failure("network unreachable", "");
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Retryable
);
}
#[test]
fn given_unreachable_registry_when_preflight_then_fails() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg("http://127.0.0.1:1")
.arg("--allow-dirty")
.arg("--policy")
.arg("fast")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.failure();
}
#[test]
fn given_unrecognized_error_when_classifying_then_ambiguous() {
let outcome = shipper_core::cargo_failure::classify_publish_failure(
"unexpected registry response: xyz",
"",
);
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Ambiguous
);
}
}
mod mixed_success_failure_state {
use super::*;
#[test]
fn given_first_succeeds_second_fails_when_publish_then_state_preserves_success() {
let td = tempdir().expect("tempdir");
create_multi_crate_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
let fake_cargo = create_fake_cargo_partial(&bin_dir);
let (new_path, real_cargo) = prepend_fake_bin(&bin_dir);
let counter_file = td.path().join("publish_counter.txt");
let registry = spawn_registry(vec![404, 200, 404], 20);
let output = shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--no-readiness")
.arg("--max-attempts")
.arg("1")
.arg("--base-delay")
.arg("0ms")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo)
.env("SHIPPER_FAKE_SUCCEED_COUNT", "1")
.env("SHIPPER_FAKE_COUNTER_FILE", &counter_file)
.env("SHIPPER_FAKE_STDERR", "error: simulated permanent failure")
.output()
.expect("run");
assert!(!output.status.success());
let state_path = td.path().join(".shipper").join("state.json");
if state_path.exists() {
let state_json = fs::read_to_string(&state_path).expect("read state");
let state: serde_json::Value = serde_json::from_str(&state_json).expect("parse state");
let packages = state["packages"].as_object().expect("packages object");
let core_entry = packages.iter().find(|(k, _)| k.starts_with("core"));
if let Some((_key, core_state)) = core_entry {
let pkg_state = core_state["state"]["state"].as_str().unwrap_or("");
assert!(
pkg_state == "published" || pkg_state == "pending",
"expected core to be 'published' or 'pending', got: {pkg_state}"
);
}
}
registry.join();
}
#[test]
fn given_already_exists_when_classifying_then_permanent() {
let outcome = shipper_core::cargo_failure::classify_publish_failure(
"version already exists: 0.1.0",
"",
);
assert_eq!(
outcome.class,
shipper_core::cargo_failure::CargoFailureClass::Permanent
);
}
#[test]
fn given_retryable_failure_when_max_attempts_exhausted_then_nonzero_exit() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
let fake_cargo = create_fake_cargo_with_stderr(&bin_dir);
let (new_path, real_cargo) = prepend_fake_bin(&bin_dir);
let registry = spawn_registry(vec![404], 20);
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--no-readiness")
.arg("--max-attempts")
.arg("3")
.arg("--base-delay")
.arg("0ms")
.arg("--max-delay")
.arg("0ms")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "1")
.env("SHIPPER_FAKE_STDERR", "error: connection reset by peer")
.assert()
.failure();
registry.join();
}
#[test]
fn given_backoff_config_when_computing_delay_then_capped() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--base-delay")
.arg("2s")
.arg("--max-delay")
.arg("30s")
.arg("--max-attempts")
.arg("10")
.arg("plan")
.assert()
.success();
}
#[test]
fn given_retryable_exhausted_then_receipt_shows_failed() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
let fake_cargo = create_fake_cargo_with_stderr(&bin_dir);
let (new_path, real_cargo) = prepend_fake_bin(&bin_dir);
let state_dir = td.path().join(".shipper");
let registry = spawn_registry(vec![404], 20);
let output = shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--no-readiness")
.arg("--max-attempts")
.arg("3")
.arg("--base-delay")
.arg("0ms")
.arg("--max-delay")
.arg("0ms")
.arg("--state-dir")
.arg(&state_dir)
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "1")
.env("SHIPPER_FAKE_STDERR", "error: connection reset by peer")
.output()
.expect("run");
assert!(!output.status.success());
let receipt_path = state_dir.join("receipt.json");
if receipt_path.exists() {
let receipt_json = fs::read_to_string(&receipt_path).expect("read receipt");
let receipt: serde_json::Value =
serde_json::from_str(&receipt_json).expect("parse receipt");
let packages = receipt["packages"].as_array().expect("packages array");
let demo = packages
.iter()
.find(|p| p["name"].as_str() == Some("demo"))
.expect("demo in receipt");
let state = demo["state"]["state"].as_str().unwrap_or("");
assert_eq!(
state, "failed",
"expected demo@0.1.0 in 'failed' state, got: {state}"
);
}
registry.join();
}
}
mod ambiguous_resolves_via_registry {
use super::*;
#[test]
fn given_ambiguous_failure_when_registry_shows_published_then_marked_published() {
let td = tempdir().expect("tempdir");
create_single_crate_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
let fake_cargo = create_fake_cargo_with_stderr(&bin_dir);
let (new_path, real_cargo) = prepend_fake_bin(&bin_dir);
let state_dir = td.path().join(".shipper");
let registry = spawn_registry(vec![404, 200], 10);
let output = shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--no-readiness")
.arg("--max-attempts")
.arg("1")
.arg("--state-dir")
.arg(&state_dir)
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "1")
.env(
"SHIPPER_FAKE_STDERR",
"error: unexpected registry response: xyz",
)
.output()
.expect("run");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success()
|| stderr.contains("treating as published")
|| stderr.contains("present on registry"),
"expected success or registry-resolved publish in stderr, got:\n{stderr}"
);
let receipt_path = state_dir.join("receipt.json");
if receipt_path.exists() {
let receipt_json = fs::read_to_string(&receipt_path).expect("read receipt");
let receipt: serde_json::Value =
serde_json::from_str(&receipt_json).expect("parse receipt");
let packages = receipt["packages"].as_array().expect("packages array");
let demo = packages
.iter()
.find(|p| p["name"].as_str() == Some("demo"))
.expect("demo in receipt");
let state = demo["state"]["state"].as_str().unwrap_or("");
assert!(
state == "published" || state == "skipped",
"expected demo@0.1.0 to be 'published' or 'skipped', got: {state}"
);
}
registry.join();
}
}