From ed4a9055fafd07075fb6ab87077267b736e3b28e Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 12 May 2025 14:40:16 +0100 Subject: [PATCH] fix bug add testcases --- src/python/qubed/set_operations.py | 29 +++++++++- src/python/qubed/value_types.py | 3 + src/rust/lib.rs | 43 +++++++++++++++ tests/test_set_operations.py | 89 ++++++++++++++++++++++++++++++ tests/test_wildcard.py | 13 +++++ 5 files changed, 174 insertions(+), 3 deletions(-) diff --git a/src/python/qubed/set_operations.py b/src/python/qubed/set_operations.py index 037b952..84f0b6e 100644 --- a/src/python/qubed/set_operations.py +++ b/src/python/qubed/set_operations.py @@ -75,7 +75,11 @@ def node_intersection( return QEnum_intersection(A, B) if isinstance(A.values, WildcardGroup) and isinstance(B.values, WildcardGroup): - return A, ValuesMetadata(WildcardGroup(), {}), B + return ( + ValuesMetadata(QEnum([]), {}), + ValuesMetadata(WildcardGroup(), {}), + ValuesMetadata(QEnum([]), {}), + ) # If A is a wildcard matcher then the intersection is everything # just_A is still * @@ -92,7 +96,7 @@ def node_intersection( ) -def operation(A: Qube, B: Qube, operation_type: SetOperation, node_type) -> Qube: +def operation(A: Qube, B: Qube, operation_type: SetOperation, node_type) -> Qube | None: assert A.key == B.key, ( "The two Qube root nodes must have the same key to perform set operations," f"would usually be two root nodes. They have {A.key} and {B.key} respectively" @@ -118,6 +122,18 @@ def operation(A: Qube, B: Qube, operation_type: SetOperation, node_type) -> Qube output = list(_operation(key, A_nodes, B_nodes, operation_type, node_type)) new_children.extend(output) + # print(f"operation {operation_type}: {A}, {B} {new_children = }") + # print(f"{A.children = }") + # print(f"{B.children = }") + # print(f"{new_children = }") + + # If there are now no children as a result of the operation, return nothing. + if (A.children or B.children) and not new_children: + if A.key == "root": + return A.replace(children=()) + else: + return None + # Whenever we modify children we should recompress them # But since `operation` is already recursive, we only need to compress this level not all levels # Hence we use the non-recursive _compress method @@ -161,7 +177,14 @@ def _operation( values=intersection.values, metadata=intersection.metadata, ) - yield operation(new_node_a, new_node_b, operation_type, node_type) + # print(f"{node_a = }") + # print(f"{node_b = }") + # print(f"{intersection.values =}") + result = operation( + new_node_a, new_node_b, operation_type, node_type + ) + if result is not None: + yield result # Now we've removed all the intersections we can yield the just_A and just_B parts if needed if keep_just_A: diff --git a/src/python/qubed/value_types.py b/src/python/qubed/value_types.py index 7eb1fd5..ad38af6 100644 --- a/src/python/qubed/value_types.py +++ b/src/python/qubed/value_types.py @@ -119,6 +119,9 @@ class WildcardGroup(ValueGroup): def __iter__(self): return ["*"] + def __bool__(self): + return True + @classmethod def from_strings(cls, values: Iterable[str]) -> Sequence[ValueGroup]: return [WildcardGroup()] diff --git a/src/rust/lib.rs b/src/rust/lib.rs index 865bacd..442a228 100644 --- a/src/rust/lib.rs +++ b/src/rust/lib.rs @@ -3,6 +3,8 @@ // #![allow(unused_variables)] +use std::collections::HashMap; + use pyo3::prelude::*; use pyo3::wrap_pyfunction; use pyo3::types::{PyDict, PyInt, PyList, PyString}; @@ -18,6 +20,47 @@ fn rust(m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] +struct NodeId(usize); + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] +struct StringId(usize); + +struct Node { + key: StringId, + metadata: HashMap>, + parent: NodeId, + values: Vec, + children: HashMap>, +} + + + +struct Qube { + root: NodeId, + nodes: Vec, + strings: Vec, +} + +use std::ops; + +impl ops::Index for Qube { + type Output = str; + + fn index(&self, index: StringId) -> &str { + &self.strings[index.0] + } + +} + +impl ops::Index for Qube { + type Output = Node; + + fn index(&self, index: NodeId) -> &Node { + &self.nodes[index.0] + } + +} // use rsfdb::listiterator::KeyValueLevel; // use rsfdb::request::Request; diff --git a/tests/test_set_operations.py b/tests/test_set_operations.py index 21b9527..cbd1ddc 100644 --- a/tests/test_set_operations.py +++ b/tests/test_set_operations.py @@ -1,6 +1,95 @@ from qubed import Qube +def set_operation_testcase(testcase): + q1 = Qube.from_tree(testcase["q1"]) + q2 = Qube.from_tree(testcase["q2"]) + assert q1 | q2 == Qube.from_tree(testcase["union"]) + assert q1 & q2 == Qube.from_tree(testcase["intersection"]) + assert q1 - q2 == Qube.from_tree(testcase["q1 - q2"]) + + +# These are a bunch of testcases where q1 and q2 are specified and then their union/intersection/difference are checked +# Generate them with code like: +# q1 = Qube.from_tree("root, frequency=*, levtype=*, param=*, levelist=*, domain=a/b/c/d") +# q2 = Qube.from_tree("root, frequency=*, levtype=*, param=*, domain=a/b/c/d") + +# test = { +# "q1": str(q1), +# "q2": str(q2), +# "union": str(q1 | q2), +# "intersection": str(q1 & q2), +# "q1 - q2": str(q1 - q2), +# } +# BUT MANUALLY CHECK THE OUTPUT BEFORE ADDING IT AS A TEST CASE! + + +testcases = [ + # Simplest case, only leaves differ + { + "q1": "root, a=1, b=1, c=1", + "q2": "root, a=1, b=1, c=2", + "union": "root, a=1, b=1, c=1/2", + "intersection": "root", + "q1 - q2": "root", + }, + # Some overlap but also each tree has unique items + { + "q1": "root, a=1, b=1, c=1/2/3", + "q2": "root, a=1, b=1, c=2/3/4", + "union": "root, a=1, b=1, c=1/2/3/4", + "intersection": "root, a=1, b=1, c=2/3", + "q1 - q2": "root", + }, + # Overlap at two levels + { + "q1": "root, a=1, b=1/2, c=1/2/3", + "q2": "root, a=1, b=2/3, c=2/3/4", + "union": """ + root, a=1 + ├── b=1, c=1/2/3 + ├── b=2, c=1/2/3/4 + └── b=3, c=2/3/4 + """, + "intersection": "root, a=1, b=2, c=2/3", + "q1 - q2": "root", + }, + # Check that we can merge even if the divergence point is higher + { + "q1": "root, a=1, b=1, c=1", + "q2": "root, a=2, b=1, c=1", + "union": "root, a=1/2, b=1, c=1", + "intersection": "root", + "q1 - q2": "root, a=1, b=1, c=1", + }, + # Two equal qubes + { + "q1": "root, a=1, b=1, c=1", + "q2": "root, a=1, b=1, c=1", + "union": "root, a=1, b=1, c=1", + "intersection": "root, a=1, b=1, c=1", + "q1 - q2": "root", + }, + # With wildcards + { + "q1": "root, frequency=*, levtype=*, param=*, levelist=*, domain=a/b/c/d", + "q2": "root, frequency=*, levtype=*, param=*, domain=a/b/c/d", + "union": """ + root, frequency=*, levtype=*, param=* + ├── domain=a/b/c/d + └── levelist=*, domain=a/b/c/d + """, + "intersection": "root", + "q1 - q2": "root", + }, +] + + +def test_cases(): + for case in testcases: + set_operation_testcase(case) + + def test_leaf_conservation(): q = Qube.from_dict( { diff --git a/tests/test_wildcard.py b/tests/test_wildcard.py index f40fa33..a867464 100644 --- a/tests/test_wildcard.py +++ b/tests/test_wildcard.py @@ -34,3 +34,16 @@ def test_intersection(): }, } ) + + +def test_wildcard_union(): + q1 = Qube.from_tree( + "root, frequency=*, levtype=*, param=*, levelist=*, domain=a/b/c/d" + ) + q2 = Qube.from_tree("root, frequency=*, levtype=*, param=*, domain=a/b/c/d") + expected = Qube.from_tree(""" + root, frequency=*, levtype=*, param=* + ├── domain=a/b/c/d + └── levelist=*, domain=a/b/c/d + """) + assert (q1 | q2) == expected