| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- //! Tests for [`crate::cluster_state`] (separate file — the main
- //! module sits at the 500-line cap).
- use crate::cluster_state::*;
- #[cfg(test)]
- use crate::config::{Config, NcclConfig, Node};
- use std::collections::HashMap;
- fn test_node(name: &str) -> Node {
- Node {
- name: name.to_string(),
- ssh_ip: format!("10.0.0.{}", name.chars().last().unwrap_or('0')),
- ib_ip: Some("10.0.1.1".to_string()),
- rank: 0,
- host_key: None,
- entity: None,
- }
- }
- fn test_cluster(name: &str, node_names: &[&str]) -> Cluster {
- Cluster {
- name: name.to_string(),
- master_ib_ip: Some("10.0.1.1".to_string()),
- nodes: node_names.iter().map(|n| test_node(n)).collect(),
- extra_entities: vec![],
- }
- }
- fn test_nccl() -> NcclConfig {
- NcclConfig {
- ib_hca: "r".to_string(),
- socket_ifname: "e".to_string(),
- ib_gid_index: Some(3),
- ib_disable: 0,
- gloo_socket_ifname: None,
- ucx_net_devices: None,
- nccl_cross_nic: None,
- nccl_proto: None,
- nccl_p2p_level: None,
- }
- }
- #[test]
- fn verdict_all_up() {
- let v = verdict(&[(true, false), (true, true)], false);
- assert!(v.up);
- assert!(!v.switch_off);
- assert_eq!(v.free, 1);
- }
- #[test]
- fn verdict_one_offline_node_takes_cluster_down() {
- let v = verdict(&[(true, false), (false, false)], false);
- assert!(!v.up);
- assert_eq!(v.free, 0);
- }
- #[test]
- fn verdict_switch_off_takes_cluster_down() {
- let v = verdict(&[(true, false), (true, false)], true);
- assert!(!v.up);
- assert!(v.switch_off);
- assert_eq!(v.free, 0);
- }
- #[test]
- fn free_count_all_reachable() {
- let mut state = ClusterState::default();
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- state.nodes.insert(
- "n2".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- let c = test_cluster("c", &["n1", "n2"]);
- assert_eq!(state.free_count(&c), 2);
- }
- #[test]
- fn free_count_occupied_excluded() {
- let mut state = ClusterState::default();
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: true,
- occupied: true,
- },
- );
- state.nodes.insert(
- "n2".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- let c = test_cluster("c", &["n1", "n2"]);
- assert_eq!(state.free_count(&c), 1);
- }
- #[test]
- fn free_count_unreachable_excluded() {
- let mut state = ClusterState::default();
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: false,
- occupied: false,
- },
- );
- state.nodes.insert(
- "n2".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- let c = test_cluster("c", &["n1", "n2"]);
- assert_eq!(state.free_count(&c), 1);
- }
- #[test]
- fn free_count_missing_node_not_free() {
- let mut state = ClusterState::default();
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- // n2 not probed — absent is treated as not free
- let c = test_cluster("c", &["n1", "n2"]);
- assert_eq!(state.free_count(&c), 1);
- }
- #[test]
- fn select_smallest_with_enough_free() {
- let mut state = ClusterState::default();
- // big cluster: 4 nodes, 2 free
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: true,
- occupied: true,
- },
- );
- state.nodes.insert(
- "n2".into(),
- NodeState {
- reachable: true,
- occupied: true,
- },
- );
- state.nodes.insert(
- "n3".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- state.nodes.insert(
- "n4".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- // small cluster: 2 nodes, 1 free
- state.nodes.insert(
- "n5".into(),
- NodeState {
- reachable: true,
- occupied: true,
- },
- );
- state.nodes.insert(
- "n6".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- let cfg = Config {
- ssh_user: "u".to_string(),
- ssh_key_path: "/k".into(),
- clusters: vec![
- test_cluster("big", &["n1", "n2", "n3", "n4"]),
- test_cluster("small", &["n5", "n6"]),
- ],
- nccl: {
- let mut m = HashMap::new();
- m.insert("big".into(), test_nccl());
- m.insert("small".into(), test_nccl());
- m
- },
- hf_cache_dir: None,
- init_port: 25000,
- before_serve: HashMap::new(),
- exec_aliases: HashMap::new(),
- home_assistant: None,
- mikrotik: None,
- drop_caches: true,
- mount_aliases: HashMap::new(),
- recipe_sources: Vec::new(),
- recipe_repos: Vec::new(),
- docker_image_sources: Vec::new(),
- cluster_sources: Vec::new(),
- registry: None,
- global_mods_dir: None,
- };
- // Request 1 node — small cluster is smallest with free >= 1
- let (c, _) = select_cluster(&cfg, "auto", Some(1), &state).unwrap();
- assert_eq!(c.name, "small");
- // Request 2 nodes — only big cluster has 2 free
- let (c, _) = select_cluster(&cfg, "auto", Some(2), &state).unwrap();
- assert_eq!(c.name, "big");
- }
- #[test]
- fn select_cluster_not_found_errors() {
- let state = ClusterState::default();
- let cfg = Config {
- ssh_user: "u".to_string(),
- ssh_key_path: "/k".into(),
- clusters: vec![],
- nccl: HashMap::new(),
- hf_cache_dir: None,
- init_port: 25000,
- before_serve: HashMap::new(),
- exec_aliases: HashMap::new(),
- home_assistant: None,
- mikrotik: None,
- drop_caches: true,
- mount_aliases: HashMap::new(),
- recipe_sources: Vec::new(),
- recipe_repos: Vec::new(),
- docker_image_sources: Vec::new(),
- cluster_sources: Vec::new(),
- registry: None,
- global_mods_dir: None,
- };
- assert!(select_cluster(&cfg, "nonexistent", None, &state).is_err());
- }
- #[test]
- fn no_cluster_has_enough_free_errors() {
- let mut state = ClusterState::default();
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: true,
- occupied: true,
- },
- );
- let cfg = Config {
- ssh_user: "u".to_string(),
- ssh_key_path: "/k".into(),
- clusters: vec![test_cluster("c", &["n1"])],
- nccl: {
- let mut m = HashMap::new();
- m.insert("c".into(), test_nccl());
- m
- },
- hf_cache_dir: None,
- init_port: 25000,
- before_serve: HashMap::new(),
- exec_aliases: HashMap::new(),
- home_assistant: None,
- mikrotik: None,
- drop_caches: true,
- mount_aliases: HashMap::new(),
- recipe_sources: Vec::new(),
- recipe_repos: Vec::new(),
- docker_image_sources: Vec::new(),
- cluster_sources: Vec::new(),
- registry: None,
- global_mods_dir: None,
- };
- assert!(select_cluster(&cfg, "auto", Some(1), &state).is_err());
- }
- #[test]
- fn single_node_allows_cluster_without_nccl() {
- let mut state = ClusterState::default();
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- let cfg = Config {
- ssh_user: "u".to_string(),
- ssh_key_path: "/k".into(),
- clusters: vec![test_cluster("c", &["n1"])],
- nccl: HashMap::new(), // no NCCL config
- hf_cache_dir: None,
- init_port: 25000,
- before_serve: HashMap::new(),
- exec_aliases: HashMap::new(),
- home_assistant: None,
- mikrotik: None,
- drop_caches: true,
- mount_aliases: HashMap::new(),
- recipe_sources: Vec::new(),
- recipe_repos: Vec::new(),
- docker_image_sources: Vec::new(),
- cluster_sources: Vec::new(),
- registry: None,
- global_mods_dir: None,
- };
- // Single-node recipe on a cluster without NCCL config → OK
- let (c, nccl) = select_cluster(&cfg, "auto", Some(1), &state).unwrap();
- assert_eq!(c.name, "c");
- assert!(nccl.is_none());
- }
- #[test]
- fn multi_node_requires_nccl() {
- let mut state = ClusterState::default();
- state.nodes.insert(
- "n1".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- state.nodes.insert(
- "n2".into(),
- NodeState {
- reachable: true,
- occupied: false,
- },
- );
- let cfg = Config {
- ssh_user: "u".to_string(),
- ssh_key_path: "/k".into(),
- clusters: vec![test_cluster("c", &["n1", "n2"])],
- nccl: HashMap::new(), // no NCCL config
- hf_cache_dir: None,
- init_port: 25000,
- before_serve: HashMap::new(),
- exec_aliases: HashMap::new(),
- home_assistant: None,
- mikrotik: None,
- drop_caches: true,
- mount_aliases: HashMap::new(),
- recipe_sources: Vec::new(),
- recipe_repos: Vec::new(),
- docker_image_sources: Vec::new(),
- cluster_sources: Vec::new(),
- registry: None,
- global_mods_dir: None,
- };
- // Multi-node recipe on a cluster without NCCL config → error
- assert!(select_cluster(&cfg, "auto", Some(2), &state).is_err());
- }
|