fix bug add testcases

This commit is contained in:
Tom 2025-05-12 14:40:16 +01:00
parent 110046b251
commit ed4a9055fa
5 changed files with 174 additions and 3 deletions

View File

@ -75,7 +75,11 @@ def node_intersection(
return QEnum_intersection(A, B) return QEnum_intersection(A, B)
if isinstance(A.values, WildcardGroup) and isinstance(B.values, WildcardGroup): 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 # If A is a wildcard matcher then the intersection is everything
# just_A is still * # 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, ( assert A.key == B.key, (
"The two Qube root nodes must have the same key to perform set operations," "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" 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)) output = list(_operation(key, A_nodes, B_nodes, operation_type, node_type))
new_children.extend(output) 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 # Whenever we modify children we should recompress them
# But since `operation` is already recursive, we only need to compress this level not all levels # But since `operation` is already recursive, we only need to compress this level not all levels
# Hence we use the non-recursive _compress method # Hence we use the non-recursive _compress method
@ -161,7 +177,14 @@ def _operation(
values=intersection.values, values=intersection.values,
metadata=intersection.metadata, 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 # Now we've removed all the intersections we can yield the just_A and just_B parts if needed
if keep_just_A: if keep_just_A:

View File

@ -119,6 +119,9 @@ class WildcardGroup(ValueGroup):
def __iter__(self): def __iter__(self):
return ["*"] return ["*"]
def __bool__(self):
return True
@classmethod @classmethod
def from_strings(cls, values: Iterable[str]) -> Sequence[ValueGroup]: def from_strings(cls, values: Iterable[str]) -> Sequence[ValueGroup]:
return [WildcardGroup()] return [WildcardGroup()]

View File

@ -3,6 +3,8 @@
// #![allow(unused_variables)] // #![allow(unused_variables)]
use std::collections::HashMap;
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::wrap_pyfunction; use pyo3::wrap_pyfunction;
use pyo3::types::{PyDict, PyInt, PyList, PyString}; use pyo3::types::{PyDict, PyInt, PyList, PyString};
@ -18,6 +20,47 @@ fn rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
Ok(()) 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<StringId, Vec<String>>,
parent: NodeId,
values: Vec<String>,
children: HashMap<StringId, Vec<NodeId>>,
}
struct Qube {
root: NodeId,
nodes: Vec<Node>,
strings: Vec<String>,
}
use std::ops;
impl ops::Index<StringId> for Qube {
type Output = str;
fn index(&self, index: StringId) -> &str {
&self.strings[index.0]
}
}
impl ops::Index<NodeId> for Qube {
type Output = Node;
fn index(&self, index: NodeId) -> &Node {
&self.nodes[index.0]
}
}
// use rsfdb::listiterator::KeyValueLevel; // use rsfdb::listiterator::KeyValueLevel;
// use rsfdb::request::Request; // use rsfdb::request::Request;

View File

@ -1,6 +1,95 @@
from qubed import Qube 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(): def test_leaf_conservation():
q = Qube.from_dict( q = Qube.from_dict(
{ {

View File

@ -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