From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id C92F31FF179 for ; Wed, 29 Oct 2025 15:49:19 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8B5DDE910; Wed, 29 Oct 2025 15:49:52 +0100 (CET) From: Lukas Wagner To: pdm-devel@lists.proxmox.com Date: Wed, 29 Oct 2025 15:48:55 +0100 Message-ID: <20251029144902.446852-7-l.wagner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251029144902.446852-1-l.wagner@proxmox.com> References: <20251029144902.446852-1-l.wagner@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1761749338311 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH datacenter-manager 06/13] views: add tests for view filter implementation X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" Signed-off-by: Lukas Wagner --- server/src/views/mod.rs | 3 + server/src/views/tests.rs | 486 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 server/src/views/tests.rs diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs index 9a2856a4..ea9e6de7 100644 --- a/server/src/views/mod.rs +++ b/server/src/views/mod.rs @@ -1 +1,4 @@ pub mod view_filter; + +#[cfg(test)] +mod tests; diff --git a/server/src/views/tests.rs b/server/src/views/tests.rs new file mode 100644 index 00000000..e2608c27 --- /dev/null +++ b/server/src/views/tests.rs @@ -0,0 +1,486 @@ +use pdm_api_types::{ + resource::{PveLxcResource, PveQemuResource, PveStorageResource, Resource, ResourceType}, + views::ViewFilterConfig, +}; + +use super::view_filter::ViewFilter; + +fn make_storage_resource(remote: &str, node: &str, storage_name: &str) -> Resource { + Resource::PveStorage(PveStorageResource { + disk: 1000, + maxdisk: 2000, + id: format!("remote/{remote}/storage/{node}/{storage_name}"), + storage: storage_name.into(), + node: node.into(), + status: "available".into(), + }) +} + +fn make_qemu_resource( + remote: &str, + node: &str, + vmid: u32, + pool: Option<&str>, + tags: &[&str], +) -> Resource { + Resource::PveQemu(PveQemuResource { + disk: 1000, + maxdisk: 2000, + id: format!("remote/{remote}/guest/{vmid}"), + node: node.into(), + status: "available".into(), + cpu: 0.0, + maxcpu: 0.0, + maxmem: 1024, + mem: 512, + name: format!("vm-{vmid}"), + // TODO: Check the API type - i guess it should be an option? + pool: pool.map_or_else(String::new, |a| a.into()), + tags: tags.iter().map(|tag| String::from(*tag)).collect(), + template: false, + uptime: 1337, + vmid, + }) +} + +fn make_lxc_resource( + remote: &str, + node: &str, + vmid: u32, + pool: Option<&str>, + tags: &[&str], +) -> Resource { + Resource::PveLxc(PveLxcResource { + disk: 1000, + maxdisk: 2000, + id: format!("remote/{remote}/guest/{vmid}"), + node: node.into(), + status: "available".into(), + cpu: 0.0, + maxcpu: 0.0, + maxmem: 1024, + mem: 512, + name: format!("vm-{vmid}"), + // TODO: Check the API type - i guess it should be an option? + pool: pool.map_or_else(String::new, |a| a.into()), + tags: tags.iter().map(|tag| String::from(*tag)).collect(), + template: false, + uptime: 1337, + vmid, + }) +} + +fn run_test(config: ViewFilterConfig, tests: &[((&str, &Resource), bool)]) { + let filter = ViewFilter::new(config); + + for ((remote_name, resource), expected) in tests { + eprintln!("remote: {remote_name}, resource: {resource:?}"); + assert_eq!(filter.resource_matches(remote_name, resource), *expected); + } +} + +const NODE: &str = "somenode"; +const STORAGE: &str = "somestorage"; +const REMOTE: &str = "someremote"; + +#[test] +fn include_remotes() { + let config = ViewFilterConfig { + id: "only-includes".into(), + include_remote: vec!["remote-a".into(), "remote-b".into()], + ..Default::default() + }; + run_test( + config.clone(), + &[ + ( + ( + "remote-a", + &make_storage_resource("remote-a", NODE, STORAGE), + ), + true, + ), + ( + ( + "remote-b", + &make_storage_resource("remote-b", NODE, STORAGE), + ), + true, + ), + ( + ( + "remote-c", + &make_storage_resource("remote-c", NODE, STORAGE), + ), + false, + ), + ], + ); + + let filter = ViewFilter::new(config); + + assert!(!filter.can_skip_remote("remote-a")); + assert!(!filter.can_skip_remote("remote-b")); + assert!(filter.can_skip_remote("remote-c")); +} + +#[test] +fn exclude_remotes() { + let config = ViewFilterConfig { + id: "only-excludes".into(), + exclude_remote: vec!["remote-a".into(), "remote-b".into()], + ..Default::default() + }; + + run_test( + config.clone(), + &[ + ( + ( + "remote-a", + &make_storage_resource("remote-a", NODE, STORAGE), + ), + false, + ), + ( + ( + "remote-b", + &make_storage_resource("remote-b", NODE, STORAGE), + ), + false, + ), + ( + ( + "remote-c", + &make_storage_resource("remote-c", NODE, STORAGE), + ), + true, + ), + ], + ); + + let filter = ViewFilter::new(config); + + assert!(filter.can_skip_remote("remote-a")); + assert!(filter.can_skip_remote("remote-b")); + assert!(!filter.can_skip_remote("remote-c")); +} + +#[test] +fn include_exclude_remotes() { + let config = ViewFilterConfig { + id: "both".into(), + include_remote: vec!["remote-a".into(), "remote-b".into()], + exclude_remote: vec!["remote-b".into(), "remote-c".into()], + ..Default::default() + }; + run_test( + config.clone(), + &[ + ( + ( + "remote-a", + &make_storage_resource("remote-a", NODE, STORAGE), + ), + true, + ), + ( + ( + "remote-b", + &make_storage_resource("remote-b", NODE, STORAGE), + ), + false, + ), + ( + ( + "remote-c", + &make_storage_resource("remote-c", NODE, STORAGE), + ), + false, + ), + ], + ); + + let filter = ViewFilter::new(config); + + assert!(!filter.can_skip_remote("remote-a")); + assert!(filter.can_skip_remote("remote-b")); + assert!(filter.can_skip_remote("remote-c")); + assert!(filter.can_skip_remote("remote-d")); +} + +#[test] +fn empty_config() { + let config = ViewFilterConfig { + id: "empty".into(), + ..Default::default() + }; + run_test( + config.clone(), + &[ + ( + ( + "remote-a", + &make_storage_resource("remote-a", NODE, STORAGE), + ), + true, + ), + ( + ( + "remote-b", + &make_storage_resource("remote-b", NODE, STORAGE), + ), + true, + ), + ( + ( + "remote-c", + &make_storage_resource("remote-c", NODE, STORAGE), + ), + true, + ), + ( + (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])), + true, + ), + ], + ); + + let filter = ViewFilter::new(config); + + assert!(!filter.can_skip_remote("remote-a")); + assert!(!filter.can_skip_remote("remote-b")); + assert!(!filter.can_skip_remote("remote-c")); +} + +#[test] +fn include_type() { + run_test( + ViewFilterConfig { + id: "include-resource-type".into(), + include_resource_type: vec![ResourceType::PveStorage, ResourceType::PveQemu], + ..Default::default() + }, + &[ + ( + (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)), + true, + ), + ( + (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])), + true, + ), + ( + (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])), + false, + ), + ], + ); +} + +#[test] +fn exclude_type() { + run_test( + ViewFilterConfig { + id: "exclude-resource-type".into(), + exclude_resource_type: vec![ResourceType::PveStorage, ResourceType::PveQemu], + ..Default::default() + }, + &[ + ( + (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)), + false, + ), + ( + (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])), + false, + ), + ( + (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])), + true, + ), + ], + ); +} + +#[test] +fn include_exclude_type() { + run_test( + ViewFilterConfig { + id: "exclude-resource-type".into(), + include_resource_type: vec![ResourceType::PveQemu], + exclude_resource_type: vec![ResourceType::PveStorage], + ..Default::default() + }, + &[ + ( + (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)), + false, + ), + ( + (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])), + true, + ), + ( + (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])), + false, + ), + ], + ); +} + +#[test] +fn include_exclude_tags() { + run_test( + ViewFilterConfig { + id: "include-tags".into(), + include_tag: vec!["tag1".into(), "tag2".into()], + exclude_tag: vec!["tag3".into()], + ..Default::default() + }, + &[ + ( + (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)), + // only qemu/lxc can match tags for now + false, + ), + ( + ( + REMOTE, + &make_qemu_resource(REMOTE, NODE, 100, None, &["tag1", "tag3"]), + ), + // because tag3 is excluded + false, + ), + ( + ( + REMOTE, + &make_lxc_resource(REMOTE, NODE, 101, None, &["tag1"]), + ), + // matches since it's in the includes + true, + ), + ( + ( + REMOTE, + &make_lxc_resource(REMOTE, NODE, 102, None, &["tag4"]), + ), + // Not in includes, can never match + false, + ), + ], + ); +} + +#[test] +fn include_exclude_resource_pool() { + run_test( + ViewFilterConfig { + id: "pools".into(), + include_resource_pool: vec!["pool1".into(), "pool2".into()], + exclude_resource_pool: vec!["pool2".into()], + ..Default::default() + }, + &[ + ( + (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)), + // only qemu/lxc can match pools for now + false, + ), + ( + ( + REMOTE, + &make_qemu_resource(REMOTE, NODE, 100, Some("pool2"), &[]), + ), + // because pool2 is excluded (takes precedence over includes) + false, + ), + ( + ( + REMOTE, + &make_lxc_resource(REMOTE, NODE, 101, Some("pool1"), &[]), + ), + // matches since it's in the includes + true, + ), + ( + ( + REMOTE, + &make_lxc_resource(REMOTE, NODE, 102, Some("pool4"), &[]), + ), + // Not in includes, can never match + false, + ), + ], + ); +} + +#[test] +fn include_exclude_resource_id() { + run_test( + ViewFilterConfig { + id: "resource-id".into(), + include_resource_id: vec![ + format!("remote/{REMOTE}/guest/100"), + format!("remote/{REMOTE}/storage/{NODE}/{STORAGE}"), + ], + exclude_resource_id: vec![ + format!("remote/{REMOTE}/guest/101"), + format!("remote/otherremote/guest/101"), + format!("remote/{REMOTE}/storage/{NODE}/otherstorage"), + ], + ..Default::default() + }, + &[ + ( + (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)), + true, + ), + ( + (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])), + true, + ), + ( + (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])), + false, + ), + ( + (REMOTE, &make_lxc_resource(REMOTE, NODE, 102, None, &[])), + false, + ), + ( + ( + "otherremote", + &make_lxc_resource("otherremote", NODE, 101, None, &[]), + ), + false, + ), + ( + ( + "yetanoterremote", + &make_lxc_resource("yetanotherremote", NODE, 104, None, &[]), + ), + false, + ), + ], + ); +} + +#[test] +fn node_included() { + let filter = ViewFilter::new(ViewFilterConfig { + id: "both".into(), + include_remote: vec!["remote-a".into()], + exclude_remote: vec!["remote-b".into()], + include_resource_id: vec!["remote/someremote/node/test".into()], + ..Default::default() + }); + + assert!(filter.is_node_included("remote-a", "somenode")); + assert!(filter.is_node_included("remote-a", "somenode2")); + assert!(!filter.is_node_included("remote-b", "somenode")); + assert!(!filter.is_node_included("remote-b", "somenode2")); + assert!(filter.is_node_included("someremote", "test")); + + assert_eq!(filter.name(), "both"); +} -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel