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)
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:

View File

@ -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()]

View File

@ -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<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::request::Request;

View File

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

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