From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 176481FF183 for ; Wed, 5 Nov 2025 11:07:29 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5B2C51FE39; Wed, 5 Nov 2025 11:08:09 +0100 (CET) Message-ID: Date: Wed, 5 Nov 2025 11:08:03 +0100 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Beta To: Proxmox Datacenter Manager development discussion , Lukas Wagner References: <20251103123521.266258-1-l.wagner@proxmox.com> <20251103123521.266258-6-l.wagner@proxmox.com> Content-Language: en-US From: Dominik Csapak In-Reply-To: <20251103123521.266258-6-l.wagner@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1762337265613 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.029 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: Re: [pdm-devel] [PATCH datacenter-manager v2 05/12] 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-Transfer-Encoding: 7bit Content-Type: text/plain; charset="us-ascii"; Format="flowed" Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" IMHO this should be melded into the last commit On 11/3/25 1:35 PM, Lukas Wagner wrote: > Signed-off-by: Lukas Wagner > --- > server/src/views/mod.rs | 3 + > server/src/views/tests.rs | 585 ++++++++++++++++++++++++++++++++++++++ > 2 files changed, 588 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..013b301f > --- /dev/null > +++ b/server/src/views/tests.rs > @@ -0,0 +1,585 @@ > +use pdm_api_types::{ > + resource::{PveLxcResource, PveQemuResource, PveStorageResource, Resource, ResourceType}, > + views::{FilterRule, 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: vec![ > + FilterRule::Remote("remote-a".into()), > + FilterRule::Remote("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: vec![ > + FilterRule::Remote("remote-a".into()), > + FilterRule::Remote("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: vec![ > + FilterRule::Remote("remote-a".into()), > + FilterRule::Remote("remote-b".into()), > + ], > + exclude: vec![ > + FilterRule::Remote("remote-b".into()), > + FilterRule::Remote("remote-c".into()), > + ], > + }; > + 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: vec![ > + FilterRule::ResourceType(ResourceType::PveStorage), > + FilterRule::ResourceType(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: vec![ > + FilterRule::ResourceType(ResourceType::PveStorage), > + FilterRule::ResourceType(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: vec![FilterRule::ResourceType(ResourceType::PveQemu)], > + exclude: vec![FilterRule::ResourceType(ResourceType::PveStorage)], > + }, > + &[ > + ( > + (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: vec![ > + FilterRule::Tag("tag1".to_string()), > + FilterRule::Tag("tag2".to_string()), > + ], > + exclude: vec![FilterRule::Tag("tag3".to_string())], > + }, > + &[ > + ( > + (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: vec![ > + FilterRule::ResourcePool("pool1".to_string()), > + FilterRule::ResourcePool("pool2".to_string()), > + ], > + exclude: vec![FilterRule::ResourcePool("pool2".to_string())], > + }, > + &[ > + ( > + (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: vec![ > + FilterRule::ResourceId(format!("remote/{REMOTE}/guest/100")), > + FilterRule::ResourceId(format!("remote/{REMOTE}/storage/{NODE}/{STORAGE}")), > + ], > + exclude: vec![ > + FilterRule::ResourceId(format!("remote/{REMOTE}/guest/101")), > + FilterRule::ResourceId("remote/otherremote/guest/101".to_string()), > + FilterRule::ResourceId(format!("remote/{REMOTE}/storage/{NODE}/otherstorage")), > + ], > + }, > + &[ > + ( > + (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: vec![ > + FilterRule::Remote("remote-a".to_string()), > + FilterRule::ResourceId("remote/someremote/node/test".to_string()), > + ], > + exclude: vec![FilterRule::Remote("remote-b".to_string())], > + }); > + > + 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"); > +} > + > +#[test] > +fn can_skip_remote_if_excluded() { > + let filter = ViewFilter::new(ViewFilterConfig { > + id: "abc".into(), > + include: vec![], > + exclude: vec![FilterRule::Remote("remote-b".to_string())], > + }); > + > + assert!(!filter.can_skip_remote("remote-a")); > + assert!(filter.can_skip_remote("remote-b")); > +} > + > +#[test] > +fn can_skip_remote_if_included() { > + let filter = ViewFilter::new(ViewFilterConfig { > + id: "abc".into(), > + include: vec![FilterRule::Remote("remote-b".to_string())], > + exclude: vec![], > + }); > + > + assert!(!filter.can_skip_remote("remote-b")); > + assert!(filter.can_skip_remote("remote-a")); > +} > + > +#[test] > +fn can_skip_remote_cannot_skip_if_any_other_include() { > + let filter = ViewFilter::new(ViewFilterConfig { > + id: "abc".into(), > + include: vec![ > + FilterRule::Remote("remote-b".to_string()), > + FilterRule::ResourceId("resource/remote-a/guest/100".to_string()), > + ], > + exclude: vec![], > + }); > + > + assert!(!filter.can_skip_remote("remote-b")); > + assert!(!filter.can_skip_remote("remote-a")); > +} > + > +#[test] > +fn can_skip_remote_explicit_remote_exclude() { > + let filter = ViewFilter::new(ViewFilterConfig { > + id: "abc".into(), > + include: vec![FilterRule::ResourceId( > + "resource/remote-a/guest/100".to_string(), > + )], > + exclude: vec![FilterRule::Remote("remote-a".to_string())], > + }); > + > + assert!(filter.can_skip_remote("remote-a")); > +} > + > +#[test] > +fn can_skip_remote_with_empty_config() { > + let filter = ViewFilter::new(ViewFilterConfig { > + id: "abc".into(), > + include: vec![], > + exclude: vec![], > + }); > + > + assert!(!filter.can_skip_remote("remote-a")); > + assert!(!filter.can_skip_remote("remote-b")); > +} > + > +#[test] > +fn can_skip_remote_with_no_remote_includes() { > + let filter = ViewFilter::new(ViewFilterConfig { > + id: "abc".into(), > + include: vec![FilterRule::ResourceId( > + "resource/remote-a/guest/100".to_string(), > + )], > + exclude: vec![], > + }); > + > + assert!(!filter.can_skip_remote("remote-a")); > + assert!(!filter.can_skip_remote("remote-b")); > +} _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel