use super::super::{Envelope, LineOutcome, LineReply, Response, SignalOutcome, SignalReply};
use super::*;
use crate::protocol::MIN_SUPPORTED;
fn envelope(id: u64, body: Request) -> Envelope {
Envelope {
id,
deadline_ms: None,
body,
}
}
#[test]
fn a_signal_request_and_its_reply_round_trip() {
let request = Request::Signal {
selector: SelectorSpec::Name("web".to_string()),
signal: "SIGHUP".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
let reply = Response::Signalled(vec![
SignalReply {
id: 1,
name: "web".to_string(),
outcome: SignalOutcome::Delivered,
},
SignalReply {
id: 2,
name: "web".to_string(),
outcome: SignalOutcome::NotRunning,
},
SignalReply {
id: 3,
name: "api".to_string(),
outcome: SignalOutcome::Failed {
reason: "no such process".to_string(),
},
},
]);
let json = serde_json::to_string(&reply).unwrap();
assert_eq!(serde_json::from_str::<Response>(&json).unwrap(), reply);
assert!(json.contains(r#""kind":"delivered""#), "{json}");
assert!(json.contains(r#""kind":"not_running""#), "{json}");
assert!(json.contains(r#""kind":"failed""#), "{json}");
}
#[test]
fn a_scale_request_names_one_app_and_a_count() {
let request = Request::Scale {
name: "web".to_string(),
count: 4,
};
let json = serde_json::to_string(&request).unwrap();
assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
assert!(json.contains(r#""kind":"scale""#), "{json}");
assert!(json.contains(r#""name":"web""#), "{json}");
assert!(!json.contains("selector"), "{json}");
}
#[test]
fn a_scaled_reply_carries_its_own_tag() {
let json = serde_json::to_string(&Response::Scaled(vec![])).unwrap();
assert_eq!(json, r#"{"kind":"scaled","data":[]}"#);
}
#[test]
fn an_add_request_and_its_reply_round_trip() {
let request = Request::Add {
apps: vec![AppConfig::minimal("web", "./srv")],
};
let json = serde_json::to_string(&request).unwrap();
assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
assert!(json.contains(r#""kind":"add""#), "{json}");
let reply = Response::Added(vec![]);
let json = serde_json::to_string(&reply).unwrap();
assert_eq!(serde_json::from_str::<Response>(&json).unwrap(), reply);
assert!(json.contains(r#""kind":"added""#), "{json}");
}
#[test]
fn a_send_line_request_and_its_reply_round_trip() {
let request = Request::SendLine {
selector: SelectorSpec::Name("repl".to_string()),
line: "reload-config".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
let reply = Response::SentLine(vec![
LineReply {
id: 1,
name: "repl".to_string(),
outcome: LineOutcome::Sent,
},
LineReply {
id: 2,
name: "web".to_string(),
outcome: LineOutcome::NoStdin,
},
LineReply {
id: 3,
name: "stuck".to_string(),
outcome: LineOutcome::NotWritten {
reason: "the app did not read its stdin within 2s".to_string(),
},
},
]);
let json = serde_json::to_string(&reply).unwrap();
assert_eq!(serde_json::from_str::<Response>(&json).unwrap(), reply);
assert!(json.contains(r#""kind":"sent""#), "{json}");
assert!(json.contains(r#""kind":"no_stdin""#), "{json}");
assert!(json.contains("did not read its stdin"), "{json}");
}
#[test]
fn a_line_carrying_a_newline_is_still_one_field_on_the_wire() {
let request = Request::SendLine {
selector: SelectorSpec::All,
line: "a\nb".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains(r#""line":"a\nb""#), "{json}");
assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
}
#[test]
fn set_sheep_env_batch_wire_v8() {
let request = Request::SetSheepEnvBatch {
name: "web".to_string(),
entries: BTreeMap::from([("A".to_string(), EnvValue::from("1".to_string()))]),
force: true,
dry_run: false,
};
let json = serde_json::to_string(&request).unwrap();
assert_eq!(
json,
r#"{"kind":"set_sheep_env_batch","name":"web","entries":{"A":"1"},"force":true,"dry_run":false}"#
);
assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
}
#[test]
fn the_batch_variant_did_not_raise_the_floor() {
assert_eq!(MIN_SUPPORTED, 8);
}
#[test]
fn request_wire_snapshots() {
let requests = vec![
Envelope {
id: 1,
deadline_ms: Some(5000),
body: Request::Ping,
},
envelope(2, Request::ListFlock),
envelope(
3,
Request::Stop {
selector: SelectorSpec::Name("web".to_string()),
},
),
envelope(
4,
Request::Start {
apps: vec![AppConfig::minimal("web", "./srv")],
},
),
envelope(
5,
Request::Reopen {
selector: SelectorSpec::All,
},
),
envelope(
6,
Request::Flush {
selector: SelectorSpec::All,
},
),
envelope(
7,
Request::Reload {
selector: SelectorSpec::Name("web".to_string()),
},
),
envelope(
8,
Request::Trigger {
selector: SelectorSpec::Name("web".to_string()),
action: "set-log-level".to_string(),
params: Some("debug".to_string()),
},
),
envelope(9, Request::SaveRoll),
envelope(10, Request::Muster),
envelope(
11,
Request::DogConfig {
name: "bark".to_string(),
},
),
envelope(
12,
Request::EnableDog {
name: "metrics".to_string(),
source: DogSource::BuiltIn,
},
),
envelope(
13,
Request::DisableDog {
name: "metrics".to_string(),
},
),
envelope(
14,
Request::Describe {
selector: SelectorSpec::Id(7),
},
),
envelope(
15,
Request::Describe {
selector: SelectorSpec::Regex("^web-".to_string()),
},
),
envelope(
16,
Request::Describe {
selector: SelectorSpec::Fold("api".to_string()),
},
),
envelope(
17,
Request::Signal {
selector: SelectorSpec::Name("web".to_string()),
signal: "SIGHUP".to_string(),
},
),
envelope(
18,
Request::Scale {
name: "web".to_string(),
count: 4,
},
),
envelope(
19,
Request::SendLine {
selector: SelectorSpec::All,
line: "reload-config".to_string(),
},
),
envelope(
20,
Request::SetSmit {
sheep: "web".to_string(),
smit: Some(
"\u{25b2} main@a1b2c3"
.parse()
.expect("the reference smit is valid"),
),
},
),
envelope(
21,
Request::SetSmit {
sheep: "web".to_string(),
smit: None,
},
),
envelope(22, Request::ConfigDrift { apps: Vec::new() }),
envelope(
23,
Request::Restart {
selector: SelectorSpec::Instance {
name: "web".to_string(),
slot: 2,
},
},
),
envelope(24, Request::HandoverFitness),
envelope(25, Request::DogStaleness),
envelope(
26,
Request::ApplyConfig {
apps: vec![DeclaredApp {
config: AppConfig::minimal("web", "./srv"),
declared: ["name", "script"]
.iter()
.map(|k| (*k).to_string())
.collect(),
declared_env: ["DATABASE_URL"].iter().map(|k| (*k).to_string()).collect(),
}],
reset: ResetDepth::Policy,
},
),
envelope(
27,
Request::Add {
apps: vec![AppConfig::minimal("web", "./srv")],
},
),
envelope(
28,
Request::SheepConfig {
name: "web".to_string(),
},
),
envelope(
29,
Request::SetSheepEnv {
name: "web".to_string(),
key: "DATABASE_URL".to_string(),
value: Some("postgres://localhost/app".to_string().into()),
},
),
envelope(
30,
Request::SetSheepField {
name: "web".to_string(),
key: "max_restarts".to_string(),
value: serde_json::json!(32),
},
),
envelope(
31,
Request::SetDogConfig {
name: "bark".to_string(),
toml: "debounce = \"30s\"\n".to_string().into(),
},
),
envelope(
32,
Request::PutSecrets {
namespace: "vercel".to_string(),
environment: "production".to_string(),
entries: BTreeMap::from([(
"API_KEY".to_string(),
EnvValue::from("sk_live_placeholder".to_string()),
)]),
},
),
envelope(
33,
Request::SetSheepEnvBatch {
name: "web".to_string(),
entries: BTreeMap::from([
(
"DATABASE_URL".to_string(),
EnvValue::from("postgres://localhost/app".to_string()),
),
(
"API_KEY".to_string(),
EnvValue::from("sk_live_placeholder".to_string()),
),
]),
force: true,
dry_run: true,
},
),
envelope(34, Request::HostUsage),
envelope(
35,
Request::DogSheepSettings {
dog: "jobs".to_string(),
},
),
envelope(
36,
Request::SetSheepDogSettings {
name: "web".to_string(),
dog: "jobs".to_string(),
table: Some(DogTable::from(serde_json::Map::from_iter([
("concurrency".to_string(), serde_json::json!(2)),
(
"hours".to_string(),
serde_json::json!({ "start": "09:00:00" }),
),
]))),
},
),
envelope(
37,
Request::SetSheepDogSettings {
name: "web".to_string(),
dog: "jobs".to_string(),
table: None,
},
),
envelope(
38,
Request::DogConfigResolved {
name: "bark".to_string(),
},
),
];
insta::assert_json_snapshot!("request_wire_v11", requests);
}
#[test]
fn dog_config_resolved_serializes_as_its_own_kind() {
assert_eq!(
serde_json::to_string(&Request::DogConfigResolved {
name: "bark".to_string()
})
.unwrap(),
r#"{"kind":"dog_config_resolved","name":"bark"}"#
);
assert_eq!(
serde_json::to_string(&Request::DogConfig {
name: "bark".to_string()
})
.unwrap(),
r#"{"kind":"dog_config","name":"bark"}"#
);
}
#[test]
fn the_dog_verbs_serialize_snake_case_with_their_payloads_under_data() {
assert_eq!(
serde_json::to_string(&Request::DogConfig {
name: "bark".to_string()
})
.unwrap(),
r#"{"kind":"dog_config","name":"bark"}"#
);
assert_eq!(
serde_json::to_string(&Request::DisableDog {
name: "bark".to_string()
})
.unwrap(),
r#"{"kind":"disable_dog","name":"bark"}"#
);
let section = Response::DogSection {
toml: "port = 9615\n".to_string().into(),
};
let wire = r#"{"kind":"dog_section","data":{"toml":"port = 9615\n"}}"#;
assert_eq!(serde_json::to_string(§ion).unwrap(), wire);
assert_eq!(serde_json::from_str::<Response>(wire).unwrap(), section);
}
#[test]
fn the_enable_and_disable_requests_and_their_replies_have_their_own_tags() {
for (request, wire) in [
(
Request::Disable {
selector: SelectorSpec::Name("api".to_string()),
},
r#"{"kind":"disable","selector":{"kind":"name","value":"api"}}"#,
),
(
Request::Enable {
selector: SelectorSpec::Name("api".to_string()),
},
r#"{"kind":"enable","selector":{"kind":"name","value":"api"}}"#,
),
] {
assert_eq!(serde_json::to_string(&request).unwrap(), wire);
assert_eq!(serde_json::from_str::<Request>(wire).unwrap(), request);
}
assert_eq!(
serde_json::to_string(&Response::Disabled(vec![])).unwrap(),
r#"{"kind":"disabled","data":[]}"#
);
assert_eq!(
serde_json::to_string(&Response::Enabled(vec![])).unwrap(),
r#"{"kind":"enabled","data":[]}"#
);
}