podup 1.9.0

Translate and run docker-compose files on rootless Podman
Documentation
use super::*;
use crate::libpod::Client;

fn engine(project: &str) -> Engine {
	Engine::with_base_dir(
		Client::new("/nonexistent.sock"),
		project.into(),
		std::env::temp_dir(),
	)
}

fn scaled_service(replicas: u32) -> Service {
	Service {
		scale: Some(replicas),
		..Service::default()
	}
}

#[test]
fn replica_names_always_index_suffix_default_name() {
	// The #815 contract: an auto-generated container name is ALWAYS
	// index-suffixed, even for a single replica (docker/podman parity).
	let e = engine("proj");
	assert_eq!(
		e.replica_names("web", &Service::default()),
		vec!["proj-web-1".to_string()]
	);
	assert_eq!(
		e.replica_names("web", &scaled_service(3)),
		vec![
			"proj-web-1".to_string(),
			"proj-web-2".to_string(),
			"proj-web-3".to_string(),
		]
	);
}

#[test]
fn replica_names_honour_explicit_container_name_verbatim() {
	// An explicit `container_name:` is the user's exact choice and is never
	// index-suffixed at a single replica.
	let e = engine("proj");
	let svc = Service {
		container_name: Some("my-db".to_string()),
		..Service::default()
	};
	assert_eq!(e.replica_names("db", &svc), vec!["my-db".to_string()]);
	assert_eq!(e.first_replica_name("db", &svc), "my-db");
}

#[test]
fn replica_names_for_zero_scale_is_empty() {
	// `--scale svc=0` resolves to no containers, so the name set is empty.
	let e = engine("proj");
	assert!(e
		.replica_names_for("web", &Service::default(), 0)
		.is_empty());
}

#[test]
fn replica_name_at_index_zero_is_rejected() {
	// `--index` is 1-based; index 0 must be an error, never replica 1.
	let e = engine("proj");
	let svc = scaled_service(3);
	let err = e
		.replica_name_at("web", &svc, Some(0))
		.expect_err("index 0 must be rejected");
	assert!(
		matches!(err, ComposeError::ReplicaIndex { index: 0, ref service } if service == "web"),
		"unexpected error: {err:?}"
	);
	// The index hint renders outside the quoted service name.
	let msg = err.to_string();
	assert!(
		msg.contains("'web'") && msg.contains("1-based"),
		"got {msg:?}"
	);
}

#[test]
fn replica_name_at_index_one_is_first_replica() {
	let e = engine("proj");
	let svc = scaled_service(3);
	assert_eq!(
		e.replica_name_at("web", &svc, Some(1)).unwrap(),
		"proj-web-1"
	);
}

#[test]
fn replica_name_at_index_n_is_nth_replica() {
	let e = engine("proj");
	let svc = scaled_service(3);
	assert_eq!(
		e.replica_name_at("web", &svc, Some(3)).unwrap(),
		"proj-web-3"
	);
}

#[test]
fn replica_name_at_out_of_range_is_rejected() {
	let e = engine("proj");
	let svc = scaled_service(3);
	assert!(e.replica_name_at("web", &svc, Some(4)).is_err());
}

#[test]
fn replica_name_at_none_is_first_replica() {
	let e = engine("proj");
	// Single replica: the first index-suffixed name (always-suffix parity
	// with docker/podman — there is no bare, unnumbered container).
	assert_eq!(
		e.replica_name_at("web", &Service::default(), None).unwrap(),
		"proj-web-1"
	);
	// Multiple replicas: the first suffixed name.
	assert_eq!(
		e.replica_name_at("web", &scaled_service(3), None).unwrap(),
		"proj-web-1"
	);
}

fn names(list: &[&str]) -> Vec<String> {
	list.iter().map(|s| s.to_string()).collect()
}

#[test]
fn resolve_replica_targets_running_scale_not_compose_default() {
	// The regression: a later `cp`/`exec` has no `--scale` (empty overrides),
	// so the static count is the compose default (1). But the service was
	// scaled up earlier and three replicas are running — `--index 2` must
	// address the running `proj-web-2`, not fall back to the base name.
	let live = names(&["proj-web-1", "proj-web-2", "proj-web-3"]);
	assert_eq!(
		resolve_replica_name("web", "proj-web", &live, Some(2)).unwrap(),
		"proj-web-2"
	);
	assert_eq!(
		resolve_replica_name("web", "proj-web", &live, Some(3)).unwrap(),
		"proj-web-3"
	);
}

#[test]
fn resolve_replica_is_order_independent() {
	// Podman does not guarantee a listing order; `--index n` targets replica
	// `n` by name, and `None` picks the lowest-numbered replica regardless.
	let live = names(&["proj-web-3", "proj-web-1", "proj-web-2"]);
	assert_eq!(
		resolve_replica_name("web", "proj-web", &live, Some(1)).unwrap(),
		"proj-web-1"
	);
	assert_eq!(
		resolve_replica_name("web", "proj-web", &live, None).unwrap(),
		"proj-web-1"
	);
}

#[test]
fn resolve_replica_out_of_range_against_running_scale() {
	// Only two replicas running: index 3 is out of range, not a stale base.
	let live = names(&["proj-web-1", "proj-web-2"]);
	assert!(resolve_replica_name("web", "proj-web", &live, Some(3)).is_err());
}

#[test]
fn resolve_replica_index_zero_is_rejected() {
	let live = names(&["proj-web-1", "proj-web-2"]);
	let err = resolve_replica_name("web", "proj-web", &live, Some(0))
		.expect_err("index 0 must be rejected");
	assert!(
		matches!(err, ComposeError::ReplicaIndex { index: 0, ref service } if service == "web"),
		"unexpected error: {err:?}"
	);
}

#[test]
fn resolve_replica_single_unsuffixed_base() {
	// A single, unsuffixed replica answers to index 1 (and None), never index 2.
	let live = names(&["proj-web"]);
	assert_eq!(
		resolve_replica_name("web", "proj-web", &live, None).unwrap(),
		"proj-web"
	);
	assert_eq!(
		resolve_replica_name("web", "proj-web", &live, Some(1)).unwrap(),
		"proj-web"
	);
	assert!(resolve_replica_name("web", "proj-web", &live, Some(2)).is_err());
}

#[test]
fn order_replicas_sorts_by_replica_number() {
	let live = names(&["proj-web-10", "proj-web-2", "proj-web-1"]);
	assert_eq!(
		order_replicas("proj-web", &live),
		names(&["proj-web-1", "proj-web-2", "proj-web-10"])
	);
}