Robust rule validation and UX (#36)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
* Add minimum severity and hide skipped options - Introduced `minimum_severity` to filter reported results. - Added `hide_skipped` option to control visibility of skipped tests. - Updated rule application logic to respect new parameters. * Improve rule processing and validation - Enhanced rule reading to group rules and handle validation messages. - Added functions to process and validate rule numbers in the spreadsheet. - Improved error handling when reading TSV files. * Improve docstrings and clean up code - Added docstring for `PropertyMatchMode` enum. - Removed unnecessary blank lines in `FunctionInputs`. - Commented out unused `property_match_mode` field with explanation. * Improve rule validation logic Added a new function to validate the structure of rule groups. Key updates include: - Checks for required columns and conditions. - Ensures rules start with "WHERE" and have at most one "CHECK". - Validates that "CHECK" is the last condition if present. - Raises descriptive errors for invalid structures. * Refactor rule processing logic - Added a new function to separate filters and final checks. - Simplified condition evaluation by using the new function. - Improved handling of empty rule groups and speckle objects. - Enhanced clarity in filtering logic for better maintainability. * Refactor number conversion logic in comparison - Updated boolean comparison method call for clarity. - Introduced a helper function to safely convert strings to numbers, handling whitespace and negative values. - Simplified the type-checking process for string-to-float conversion. * Update comparison logic in parameter checks Flipped the comparison logic to match user expectations for parameter value checks. Updated docstrings for better clarity on UX perspective. Added error handling for non-numeric values to improve robustness. * Refactor method names for clarity Updated method names to remove leading underscores for better readability and consistency. Added a docstring to describe the purpose of the rules module. * Add tests for comparison and rule processing - Introduced a new test suite for value comparisons, covering numeric strings, boolean strings, case sensitivity in string comparisons, and float comparison with tolerance. - Created a test suite for parameter handling functionality to validate the existence and retrieval of parameters in v2 and v3 objects. - Added tests for rule processing functionality using both explicit CHECK format and legacy format rules.
This commit is contained in:
committed by
GitHub
parent
e49bf225ec
commit
168a1f517a
+21
-11
@@ -1,12 +1,13 @@
|
||||
# This is the main function that will be executed when the automation is triggered.
|
||||
# It will receive the inputs from the user, and the context of the run.
|
||||
# It will then apply the rules to the objects in the model, and report back the results.
|
||||
"""This is the main function that will be executed when the automation is triggered.
|
||||
|
||||
It will receive the inputs from the user, and the context of the run.
|
||||
It will then apply the rules to the objects in the model, and report back the results.
|
||||
"""
|
||||
|
||||
from pandas import DataFrame
|
||||
from speckle_automate import AutomationContext
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
from src.helpers import flatten_base
|
||||
from src.helpers import flatten_base, speckle_print
|
||||
from src.inputs import FunctionInputs
|
||||
from src.rule_processor import apply_rules_to_objects
|
||||
from src.spreadsheet import read_rules_from_spreadsheet
|
||||
@@ -38,16 +39,25 @@ def automate_function(
|
||||
global VERSION
|
||||
VERSION = getattr(version_root_object, "version", 2) # noqa: F841SION = getattr(version_root_object,"version", 2) # noqa: F841 # noqa: F841
|
||||
|
||||
# read the rules from the spreadsheet
|
||||
rules: DataFrame = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
|
||||
# Read and group rules
|
||||
grouped_rules, messages = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
|
||||
|
||||
if (rules is None) or (len(rules) == 0):
|
||||
automate_context.mark_run_exception("No rules defined")
|
||||
# Handle any validation messages
|
||||
for message in messages:
|
||||
speckle_print(message) # or log them appropriately
|
||||
|
||||
grouped_rules = rules.groupby("Rule Number")
|
||||
if grouped_rules is None:
|
||||
automate_context.mark_run_exception("Failed to process rules")
|
||||
return
|
||||
|
||||
# apply the rules to the objects
|
||||
apply_rules_to_objects(flat_list_of_objects, grouped_rules, automate_context)
|
||||
apply_rules_to_objects(
|
||||
flat_list_of_objects,
|
||||
grouped_rules,
|
||||
automate_context,
|
||||
minimum_severity=function_inputs.minimum_severity,
|
||||
hide_skipped=function_inputs.hide_skipped,
|
||||
)
|
||||
|
||||
# set the automation context view, to the original model / VERSION view
|
||||
automate_context.set_context_view()
|
||||
|
||||
+33
-16
@@ -5,9 +5,20 @@ from speckle_automate import AutomateBase
|
||||
|
||||
|
||||
class PropertyMatchMode(Enum):
|
||||
STRICT = "strict" # Exact parameter path match
|
||||
FUZZY = "fuzzy" # Search all parameters ignoring hierarchy
|
||||
MIXED = "mixed" # Exact match first, fuzzy fallback
|
||||
"""Controls how strictly parameter names must match."""
|
||||
|
||||
STRICT = "strict" # Exact parameter path match
|
||||
FUZZY = "fuzzy" # Search all parameters ignoring hierarchy
|
||||
MIXED = "mixed" # Exact match first, fuzzy fallback
|
||||
|
||||
|
||||
class MinimumSeverity(str, Enum):
|
||||
"""Enum for minimum severity level to report."""
|
||||
|
||||
INFO = "Info"
|
||||
WARNING = "Warning"
|
||||
ERROR = "Error"
|
||||
|
||||
|
||||
class FunctionInputs(AutomateBase):
|
||||
"""These are function author defined values.
|
||||
@@ -17,23 +28,29 @@ class FunctionInputs(AutomateBase):
|
||||
https://docs.pydantic.dev/latest/usage/models/
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# In this exercise, we will move rules to an external source so not to hardcode them.
|
||||
spreadsheet_url: str = Field(
|
||||
title="Spreadsheet URL",
|
||||
description="This is the URL of the spreadsheet to check. It should be a TSV format data source.",
|
||||
)
|
||||
|
||||
property_match_mode: PropertyMatchMode = Field(
|
||||
default=PropertyMatchMode.MIXED,
|
||||
title="Property Match Mode",
|
||||
description='Controls how strictly parameter names must match. ' +
|
||||
'STRICT will only match exact parameter paths, ' +
|
||||
'FUZZY will search all parameters ignoring hierarchy, ' +
|
||||
'MIXED will exact match first, fuzzy fallback.'
|
||||
minimum_severity: MinimumSeverity = Field(
|
||||
default=MinimumSeverity.INFO,
|
||||
title="Minimum Severity Level",
|
||||
description="Only report test results with this severity level or higher. Info will show all results, Warning will show warnings and errors, Error will show only errors.",
|
||||
)
|
||||
|
||||
hide_skipped: bool = Field(
|
||||
default=False,
|
||||
title="Hide Skipped Tests",
|
||||
description="If enabled, tests that were skipped (no matching objects found) will not be reported.",
|
||||
)
|
||||
|
||||
# property_match_mode: PropertyMatchMode = Field(
|
||||
# default=PropertyMatchMode.MIXED,
|
||||
# title="Property Match Mode",
|
||||
# description='Controls how strictly parameter names must match. ' +
|
||||
# 'STRICT will only match exact parameter paths, ' +
|
||||
# 'FUZZY will search all parameters ignoring hierarchy, ' +
|
||||
# 'MIXED will exact match first, fuzzy fallback.'
|
||||
# )
|
||||
|
||||
+138
-32
@@ -1,3 +1,5 @@
|
||||
"""Module for processing rules against Speckle objects and updating the automate context with the results."""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
@@ -6,11 +8,52 @@ from pandas.core.groupby import DataFrameGroupBy
|
||||
from speckle_automate import AutomationContext, ObjectResultLevel
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
from inputs import MinimumSeverity
|
||||
from src.helpers import speckle_print
|
||||
from src.predicates import PREDICATE_METHOD_MAP
|
||||
from src.rules import PropertyRules
|
||||
|
||||
|
||||
def validate_rule_structure(rule_group: pd.DataFrame) -> None:
|
||||
"""Validates the structure and logic of a rule group.
|
||||
|
||||
Args:
|
||||
rule_group: DataFrame containing the rule conditions
|
||||
|
||||
Raises:
|
||||
ValueError: If rule structure is invalid
|
||||
"""
|
||||
if rule_group.empty:
|
||||
return
|
||||
|
||||
# Validate Logic column exists
|
||||
if "Logic" not in rule_group.columns:
|
||||
raise ValueError("Rule must have a 'Logic' column")
|
||||
|
||||
# Get uppercase Logic values for case-insensitive comparison
|
||||
logic_values = rule_group["Logic"].str.upper()
|
||||
|
||||
# Check if first condition is WHERE
|
||||
if logic_values.iloc[0] != "WHERE":
|
||||
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} must start with WHERE")
|
||||
|
||||
# Count CHECK conditions
|
||||
check_count = sum(1 for value in logic_values if value == "CHECK")
|
||||
if check_count > 1:
|
||||
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} has multiple CHECK conditions")
|
||||
|
||||
# If CHECK exists, ensure it's the last condition
|
||||
check_indices = logic_values[logic_values == "CHECK"].index
|
||||
if check_count == 1 and check_indices[0] != rule_group.index[-1]:
|
||||
raise ValueError(f"CHECK must be the last condition in rule {rule_group.iloc[0]['Rule Number']}")
|
||||
|
||||
# Validate Logic values
|
||||
valid_values = {"WHERE", "AND", "CHECK"}
|
||||
invalid_values = set(logic_values.unique()) - valid_values
|
||||
if invalid_values:
|
||||
raise ValueError(f"Invalid Logic values found: {invalid_values}")
|
||||
|
||||
|
||||
def evaluate_condition(
|
||||
speckle_object: Base, condition: pd.Series, rule_number: str | None = None, case_number: int | None = None
|
||||
) -> bool:
|
||||
@@ -42,12 +85,52 @@ def evaluate_condition(
|
||||
method = getattr(PropertyRules, method_name, None)
|
||||
|
||||
if method:
|
||||
check_answer = method(speckle_object, property_name, value)
|
||||
return method(speckle_object, property_name, value)
|
||||
|
||||
return check_answer
|
||||
return False
|
||||
|
||||
|
||||
def get_filters_and_check(rule_group: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
|
||||
"""Separates rule conditions into filters and final check.
|
||||
|
||||
Args:
|
||||
rule_group: DataFrame containing rule conditions
|
||||
|
||||
Returns:
|
||||
Tuple containing filter conditions and final check condition
|
||||
"""
|
||||
if rule_group.empty:
|
||||
return pd.DataFrame(), pd.Series()
|
||||
|
||||
# Get uppercase Logic values for case-insensitive comparison
|
||||
logic_values = rule_group["Logic"].str.upper()
|
||||
|
||||
# Look for explicit CHECK
|
||||
check_conditions = rule_group[logic_values == "CHECK"]
|
||||
has_explicit_check = not check_conditions.empty
|
||||
|
||||
if has_explicit_check:
|
||||
# Use first CHECK condition as final check
|
||||
final_check = check_conditions.iloc[0]
|
||||
# All other conditions are filters
|
||||
filters = rule_group[logic_values != "CHECK"]
|
||||
else:
|
||||
# Legacy behavior: use last AND as check if present
|
||||
and_conditions = rule_group[logic_values == "AND"]
|
||||
if not and_conditions.empty:
|
||||
# Get the last AND as the check
|
||||
final_check = and_conditions.iloc[-1]
|
||||
# All conditions up to the last AND are filters
|
||||
last_and_idx = and_conditions.index[-1]
|
||||
filters = rule_group[rule_group.index < last_and_idx]
|
||||
else:
|
||||
# No AND conditions found, just use WHERE as filter
|
||||
filters = rule_group
|
||||
final_check = rule_group.iloc[0] # Default to first condition as check
|
||||
|
||||
return filters, final_check
|
||||
|
||||
|
||||
def process_rule(
|
||||
speckle_objects: list[Base], rule_group: pd.DataFrame
|
||||
) -> tuple[list[Any], list[Any]] | tuple[list[Base], list[Base]]:
|
||||
@@ -62,36 +145,47 @@ def process_rule(
|
||||
Returns:
|
||||
A tuple of lists containing objects that passed and failed the rule.
|
||||
"""
|
||||
# Extract the 'WHERE' condition and subsequent 'AND' conditions
|
||||
filter_condition = rule_group.iloc[0]
|
||||
subsequent_conditions = rule_group.iloc[1:]
|
||||
|
||||
# get the last row of the rule_group and get the Message and Report Severity
|
||||
rule_info = rule_group.iloc[-1]
|
||||
rule_number = rule_info["Rule Number"]
|
||||
|
||||
# Filter objects based on the 'WHERE' condition
|
||||
filtered_objects = [
|
||||
speckle_object for speckle_object in speckle_objects if evaluate_condition(speckle_object, filter_condition)
|
||||
]
|
||||
|
||||
if not filtered_objects or len(list(filtered_objects)) == 0:
|
||||
if not speckle_objects or rule_group.empty:
|
||||
return [], []
|
||||
|
||||
# Initialize lists for passed and failed objects
|
||||
pass_objects, fail_objects = [], []
|
||||
try:
|
||||
validate_rule_structure(rule_group)
|
||||
except ValueError as e:
|
||||
speckle_print(f"Rule validation error: {str(e)}")
|
||||
return [], []
|
||||
|
||||
# Evaluate each filtered object against the 'AND' conditions
|
||||
for speckle_object in filtered_objects:
|
||||
if all(
|
||||
evaluate_condition(
|
||||
speckle_object=speckle_object, condition=condition, rule_number=rule_number, case_number=index
|
||||
# Get filters and final check
|
||||
filters, final_check = get_filters_and_check(rule_group)
|
||||
|
||||
# Start with all objects
|
||||
filtered_objects = speckle_objects.copy()
|
||||
rule_number = rule_group.iloc[0]["Rule Number"]
|
||||
|
||||
# Apply each filter condition sequentially
|
||||
for index, (_, filter_condition) in enumerate(filters.iterrows()):
|
||||
filtered_objects = [
|
||||
obj
|
||||
for obj in filtered_objects
|
||||
if evaluate_condition(
|
||||
speckle_object=obj, condition=filter_condition, rule_number=rule_number, case_number=index
|
||||
)
|
||||
for index, condition in subsequent_conditions.iterrows()
|
||||
]
|
||||
|
||||
# Early exit if no objects pass filters
|
||||
if not filtered_objects:
|
||||
return [], []
|
||||
|
||||
# For remaining objects, evaluate the final check
|
||||
pass_objects = []
|
||||
fail_objects = []
|
||||
|
||||
for obj in filtered_objects:
|
||||
if evaluate_condition(
|
||||
speckle_object=obj, condition=final_check, rule_number=rule_number, case_number=len(filters)
|
||||
):
|
||||
pass_objects.append(speckle_object)
|
||||
pass_objects.append(obj)
|
||||
else:
|
||||
fail_objects.append(speckle_object)
|
||||
fail_objects.append(obj)
|
||||
|
||||
return pass_objects, fail_objects
|
||||
|
||||
@@ -100,6 +194,8 @@ def apply_rules_to_objects(
|
||||
speckle_objects: list[Base],
|
||||
grouped_rules: DataFrameGroupBy,
|
||||
automate_context: AutomationContext,
|
||||
minimum_severity: MinimumSeverity = MinimumSeverity.INFO,
|
||||
hide_skipped: bool = False,
|
||||
) -> dict[str, tuple[list[Base], list[Base]]]:
|
||||
"""Applies defined rules to a list of objects and updates the automate context based on the results.
|
||||
|
||||
@@ -107,14 +203,16 @@ def apply_rules_to_objects(
|
||||
speckle_objects (List[Base]): The list of objects to which rules are applied.
|
||||
grouped_rules (pd.DataFrameGroupBy): The DataFrame containing rule definitions.
|
||||
automate_context (Any): Context manager for attaching rule results.
|
||||
minimum_severity: Minimum severity level to report
|
||||
hide_skipped: Whether to hide skipped tests
|
||||
"""
|
||||
grouped_results = {}
|
||||
|
||||
rules_processed = 0
|
||||
severity_levels = {MinimumSeverity.INFO: 0, MinimumSeverity.WARNING: 1, MinimumSeverity.ERROR: 2}
|
||||
min_severity_level = severity_levels[minimum_severity]
|
||||
|
||||
for rule_id, rule_group in grouped_rules:
|
||||
rule_id_str = str(rule_id) # Convert rule_id to string
|
||||
|
||||
rules_processed += 1
|
||||
|
||||
# Ensure rule_group has necessary columns
|
||||
@@ -123,17 +221,25 @@ def apply_rules_to_objects(
|
||||
|
||||
pass_objects, fail_objects = process_rule(speckle_objects, rule_group)
|
||||
|
||||
attach_results(pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True)
|
||||
attach_results(fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False)
|
||||
# Get the severity level for this rule
|
||||
rule_severity = get_severity(rule_group.iloc[-1])
|
||||
rule_severity_level = severity_levels[MinimumSeverity(rule_severity.value)]
|
||||
|
||||
if len(pass_objects) == 0 and len(fail_objects) == 0:
|
||||
# For passing objects, only attach if we're showing all levels (INFO)
|
||||
if minimum_severity == MinimumSeverity.INFO:
|
||||
attach_results(pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True)
|
||||
|
||||
# For failing objects, attach if they meet minimum severity threshold
|
||||
if rule_severity_level >= min_severity_level:
|
||||
attach_results(fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False)
|
||||
|
||||
if len(pass_objects) == 0 and len(fail_objects) == 0 and not hide_skipped:
|
||||
automate_context.attach_info_to_objects(
|
||||
category=f"Rule {rule_id_str} Skipped",
|
||||
object_ids=["0"], # This is a hack to get a rule to report with no valid objects
|
||||
message=f"No objects found for rule {rule_id_str}",
|
||||
metadata={},
|
||||
)
|
||||
# pass
|
||||
|
||||
grouped_results[rule_id_str] = (pass_objects, fail_objects)
|
||||
|
||||
|
||||
+37
-18
@@ -1,3 +1,5 @@
|
||||
"""A collection of rules for processing Speckle objects and their properties."""
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
@@ -243,27 +245,39 @@ class PropertyRules:
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_greater_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
|
||||
"""Checks if parameter value is greater than threshold."""
|
||||
"""Checks if parameter value is greater than threshold.
|
||||
|
||||
Note: From a UX perspective, if someone writes 'height greater than 2401',
|
||||
they mean "flag an error if height <= 2401". So we flip the comparison.
|
||||
"""
|
||||
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, int | float):
|
||||
raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
|
||||
return parameter_value > PropertyRules.parse_number_from_string(threshold)
|
||||
return parameter_value <= PropertyRules.parse_number_from_string(threshold)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_less_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
|
||||
"""Checks if parameter value is less than threshold."""
|
||||
"""Checks if parameter value is less than threshold.
|
||||
|
||||
Note: From a UX perspective, if someone writes 'height less than 2401',
|
||||
they mean "flag an error if height >= 2401". So we flip the comparison.
|
||||
"""
|
||||
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, int | float):
|
||||
raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
|
||||
return parameter_value < PropertyRules.parse_number_from_string(threshold)
|
||||
return parameter_value >= PropertyRules.parse_number_from_string(threshold)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_in_range(speckle_object: Base, parameter_name: str, value_range: str) -> bool:
|
||||
"""Checks if parameter value falls within range."""
|
||||
"""Checks if parameter value falls outside specified range.
|
||||
|
||||
Note: From a UX perspective, if someone writes 'height in range 2401,3000',
|
||||
they mean "flag an error if height < 2401 or height > 3000".
|
||||
"""
|
||||
min_value, max_value = value_range.split(",")
|
||||
min_value = PropertyRules.parse_number_from_string(min_value)
|
||||
max_value = PropertyRules.parse_number_from_string(max_value)
|
||||
@@ -311,7 +325,7 @@ class PropertyRules:
|
||||
return is_value_in_list(parameter_value, value_list)
|
||||
|
||||
@staticmethod
|
||||
def _check_boolean_value(value: Any, values_to_match: tuple[str, ...]) -> bool:
|
||||
def check_boolean_value(value: Any, values_to_match: tuple[str, ...]) -> bool:
|
||||
"""Check if a value matches any target value in expected format."""
|
||||
if isinstance(value, bool):
|
||||
return value is (True if "true" in values_to_match else False)
|
||||
@@ -325,13 +339,13 @@ class PropertyRules:
|
||||
def is_parameter_value_true(speckle_object: Base, parameter_name: str) -> bool:
|
||||
"""Check if parameter value represents true."""
|
||||
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return PropertyRules._check_boolean_value(parameter_value, ("yes", "true", "1"))
|
||||
return PropertyRules.check_boolean_value(parameter_value, ("yes", "true", "1"))
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_false(speckle_object: Base, parameter_name: str) -> bool:
|
||||
"""Check if parameter value represents false."""
|
||||
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return PropertyRules._check_boolean_value(parameter_value, ("no", "false", "0"))
|
||||
return PropertyRules.check_boolean_value(parameter_value, ("no", "false", "0"))
|
||||
|
||||
@staticmethod
|
||||
def has_category(speckle_object: Base) -> bool:
|
||||
@@ -350,7 +364,7 @@ class PropertyRules:
|
||||
return PropertyRules.get_parameter_value(speckle_object, "category")
|
||||
|
||||
@staticmethod
|
||||
def _try_boolean_comparison(value1: Any, value2: Any, allow_yes_no: bool) -> tuple[bool, bool]:
|
||||
def try_boolean_comparison(value1: Any, value2: Any, allow_yes_no: bool) -> tuple[bool, bool]:
|
||||
"""Attempts to compare two values as booleans."""
|
||||
|
||||
def strict_convert_boolean(value: Any) -> Any:
|
||||
@@ -380,7 +394,7 @@ class PropertyRules:
|
||||
return False, False
|
||||
|
||||
@staticmethod
|
||||
def _compare_values(
|
||||
def compare_values(
|
||||
value1: Any,
|
||||
value2: Any,
|
||||
case_sensitive: bool = False,
|
||||
@@ -401,15 +415,20 @@ class PropertyRules:
|
||||
bool: True if values are considered equal, False otherwise
|
||||
"""
|
||||
# Try boolean comparison first
|
||||
can_compare, result = PropertyRules._try_boolean_comparison(value1, value2, allow_yes_no_bools)
|
||||
can_compare, result = PropertyRules.try_boolean_comparison(value1, value2, allow_yes_no_bools)
|
||||
if can_compare:
|
||||
return result
|
||||
|
||||
# Handle case where one value is a string that can be interpreted as a number
|
||||
if isinstance(value1, str) and value1.replace(".", "", 1).isdigit():
|
||||
value1 = float(value1)
|
||||
if isinstance(value2, str) and value2.replace(".", "", 1).isdigit():
|
||||
value2 = float(value2)
|
||||
def safe_convert_to_number(val):
|
||||
if isinstance(val, str):
|
||||
val = val.strip() # Remove whitespace
|
||||
if val.replace(".", "", 1).replace("-", "", 1).isdigit(): # Handle negative numbers
|
||||
return float(val)
|
||||
return val
|
||||
|
||||
value1 = safe_convert_to_number(value1)
|
||||
value2 = safe_convert_to_number(value2)
|
||||
|
||||
# For strings: Allow case insensitivity if specified
|
||||
if isinstance(value1, str) and isinstance(value2, str):
|
||||
@@ -450,7 +469,7 @@ class PropertyRules:
|
||||
if parameter_value is None:
|
||||
return False
|
||||
|
||||
return PropertyRules._compare_values(
|
||||
return PropertyRules.compare_values(
|
||||
parameter_value, value_to_match, case_sensitive, tolerance, allow_yes_no_bools=True
|
||||
)
|
||||
|
||||
@@ -478,7 +497,7 @@ class PropertyRules:
|
||||
if parameter_value is None:
|
||||
return True # Non-existent parameters are considered not equal
|
||||
|
||||
return not PropertyRules._compare_values(
|
||||
return not PropertyRules.compare_values(
|
||||
parameter_value, value_to_match, case_sensitive, tolerance, allow_yes_no_bools=True
|
||||
)
|
||||
|
||||
@@ -500,6 +519,6 @@ class PropertyRules:
|
||||
if parameter_value is None:
|
||||
return False
|
||||
|
||||
return PropertyRules._compare_values(
|
||||
return PropertyRules.compare_values(
|
||||
parameter_value, value_to_match, case_sensitive=True, tolerance=0, allow_yes_no_bools=False, use_exact=True
|
||||
)
|
||||
|
||||
+119
-14
@@ -1,38 +1,143 @@
|
||||
"""Module for reading and processing rules from a cloud hosted TSV file."""
|
||||
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
from pandas.core.groupby import DataFrameGroupBy
|
||||
|
||||
|
||||
def read_rules_from_spreadsheet(url: str) -> DataFrame | None:
|
||||
"""Reads a TSV file from a provided URL and returns a DataFrame.
|
||||
def process_rule_numbers(df: DataFrame) -> DataFrame:
|
||||
"""Process rule numbers in a DataFrame.
|
||||
|
||||
1. Finding rule groups based on 'WHERE' logic
|
||||
2. Using the first non-null rule number in each group
|
||||
3. Handling duplicate rule numbers by incrementing
|
||||
|
||||
Args:
|
||||
url (str): The URL to the TSV file.
|
||||
df: DataFrame with columns including 'Rule Number' and 'Logic'
|
||||
|
||||
Returns:
|
||||
DataFrame: Pandas DataFrame containing the TSV data.
|
||||
DataFrame with processed rule numbers
|
||||
"""
|
||||
# Create a copy to avoid modifying original
|
||||
df = df.copy()
|
||||
|
||||
# Initialize tracking variables
|
||||
current_rule_num = None
|
||||
used_rule_nums = set()
|
||||
processed_rule_nums = []
|
||||
|
||||
# Find indices where Logic is 'WHERE' to identify rule group starts
|
||||
where_indices = df[df["Logic"].str.upper() == "WHERE"].index
|
||||
|
||||
# Process each group
|
||||
for i in range(len(where_indices)):
|
||||
start_idx = where_indices[i]
|
||||
end_idx = where_indices[i + 1] if i + 1 < len(where_indices) else len(df)
|
||||
|
||||
# Get slice of rows for this group
|
||||
group_slice = df.iloc[start_idx:end_idx]
|
||||
|
||||
# Try to get rule number from first row
|
||||
group_rule_num = group_slice["Rule Number"].iloc[0]
|
||||
|
||||
if pd.isna(group_rule_num):
|
||||
# If no rule number, generate next available
|
||||
if current_rule_num is None:
|
||||
current_rule_num = 1
|
||||
else:
|
||||
current_rule_num += 1
|
||||
group_rule_num = current_rule_num
|
||||
else:
|
||||
# Convert to int if it's a float or string
|
||||
group_rule_num = int(group_rule_num)
|
||||
|
||||
# Handle duplicate rule numbers
|
||||
while group_rule_num in used_rule_nums:
|
||||
group_rule_num += 1
|
||||
|
||||
# Update tracking
|
||||
current_rule_num = group_rule_num
|
||||
used_rule_nums.add(group_rule_num)
|
||||
|
||||
# Fill rule numbers for this group
|
||||
processed_rule_nums.extend([group_rule_num] * len(group_slice))
|
||||
|
||||
# Update DataFrame with processed rule numbers
|
||||
df["Rule Number"] = processed_rule_nums
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def validate_rule_numbers(df: DataFrame) -> list[str]:
|
||||
"""Validate rule numbers and return any warnings or errors.
|
||||
|
||||
Args:
|
||||
df: DataFrame with processed rule numbers
|
||||
|
||||
Returns:
|
||||
List of warning/error messages
|
||||
"""
|
||||
messages = []
|
||||
|
||||
# Check for missing rule numbers
|
||||
if df["Rule Number"].isna().any():
|
||||
messages.append("Warning: Some rules are missing rule numbers")
|
||||
|
||||
# Check for non-integer rule numbers
|
||||
non_int_mask = df["Rule Number"].apply(lambda x: not pd.isna(x) and not float(x).is_integer())
|
||||
if non_int_mask.any():
|
||||
messages.append("Warning: Some rule numbers are not integers")
|
||||
|
||||
# Check for duplicate rule numbers in WHERE rows
|
||||
where_rules = df[df["Logic"].str.upper() == "WHERE"]["Rule Number"]
|
||||
duplicates = where_rules[where_rules.duplicated()]
|
||||
if not duplicates.empty:
|
||||
messages.append(f"Warning: Duplicate rule numbers found: {list(duplicates)}")
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def read_rules_from_spreadsheet(url: str) -> tuple[DataFrameGroupBy, list[str]] | tuple[None, list[str]]:
|
||||
"""Reads a TSV file from a provided URL, processes rule numbers, and returns grouped rules.
|
||||
|
||||
Args:
|
||||
url (str): The URL to the TSV file
|
||||
|
||||
Returns:
|
||||
Tuple containing:
|
||||
- DataFrameGroupBy object with rules grouped by rule number (or None if error)
|
||||
- List of validation messages/warnings
|
||||
"""
|
||||
try:
|
||||
# Since the output is a TSV, we use `pd.read_csv` with `sep='\t'` to specify tab-separated values.
|
||||
# Read the TSV file
|
||||
df = pd.read_csv(url, sep="\t")
|
||||
|
||||
# Convert mixed type columns
|
||||
df = convert_mixed_columns(df)
|
||||
|
||||
# Convert columns to appropriate types based on their content.
|
||||
return df
|
||||
# Process rule numbers
|
||||
df = process_rule_numbers(df)
|
||||
|
||||
# Get validation messages
|
||||
messages = validate_rule_numbers(df)
|
||||
|
||||
# Group by rule number
|
||||
grouped_rules = df.groupby("Rule Number")
|
||||
|
||||
return grouped_rules, messages
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to read the TSV from the URL: {e}")
|
||||
return None
|
||||
return None, [f"Failed to read the TSV from the URL: {str(e)}"]
|
||||
|
||||
|
||||
def convert_mixed_columns(df):
|
||||
def convert_mixed_columns(df: DataFrame) -> DataFrame:
|
||||
"""Converts columns in a DataFrame to appropriate types based on their content.
|
||||
|
||||
Args:
|
||||
df (DataFrame): The DataFrame whose columns are to be converted.
|
||||
df (DataFrame): The DataFrame whose columns are to be converted
|
||||
|
||||
Returns:
|
||||
DataFrame: The DataFrame with columns converted to appropriate types.
|
||||
DataFrame with columns converted to appropriate types
|
||||
"""
|
||||
df = df.apply(lambda c: c.astype(object) if any(str(x).replace(".", "", 1).isdigit() for x in c) else c.astype(str))
|
||||
|
||||
# df = df.apply(lambda c: c.astype(object) if any(str(x).replace(".", "", 1).isdigit() for x in c) else c.astype(str))
|
||||
return df
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from src.rules import PropertyRules
|
||||
|
||||
|
||||
class TestValueComparison:
|
||||
"""Test suite for value comparison functionality."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value1, value2",
|
||||
[
|
||||
# Basic numeric strings
|
||||
("1400", 1400.0),
|
||||
("1400.0", 1400),
|
||||
("1400.00", 1400),
|
||||
# Whitespace handling
|
||||
(" 1400 ", 1400.0),
|
||||
(" 1400 ", 1400.0),
|
||||
("\t1400\n", 1400.0),
|
||||
# Negative numbers
|
||||
("-1400", -1400.0),
|
||||
(" -1400 ", -1400.0),
|
||||
("-1400.0", -1400),
|
||||
# Zero handling
|
||||
("0", 0.0),
|
||||
("-0", 0.0),
|
||||
("0.0", 0),
|
||||
# Simple integers
|
||||
("1", 1),
|
||||
("1.0", 1),
|
||||
],
|
||||
)
|
||||
def test_numeric_string_comparison(self, value1: Any, value2: Any):
|
||||
"""Test comparison of numeric strings with numbers."""
|
||||
assert PropertyRules.compare_values(value1, value2)
|
||||
# Test reverse comparison
|
||||
assert PropertyRules.compare_values(value2, value1)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value1, value2, expected",
|
||||
[
|
||||
("Yes", True, True),
|
||||
("No", False, True),
|
||||
("yes", True, True),
|
||||
("no", False, True),
|
||||
("YES", True, True),
|
||||
("NO", False, True),
|
||||
("true", True, True),
|
||||
("false", False, True),
|
||||
("True", True, True),
|
||||
("False", False, True),
|
||||
],
|
||||
)
|
||||
def test_boolean_string_comparison(self, value1: str, value2: bool, expected: bool):
|
||||
"""Test comparison of boolean strings with booleans."""
|
||||
assert PropertyRules.compare_values(value1, value2) == expected
|
||||
# Test reverse comparison
|
||||
assert PropertyRules.compare_values(value2, value1) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value1, value2, case_sensitive, expected",
|
||||
[
|
||||
("hello", "HELLO", False, True),
|
||||
("hello", "HELLO", True, False),
|
||||
("Hello", "hello", False, True),
|
||||
("Hello", "Hello", True, True),
|
||||
],
|
||||
)
|
||||
def test_string_comparison(self, value1: str, value2: str, case_sensitive: bool, expected: bool):
|
||||
"""Test string comparison with case sensitivity options."""
|
||||
assert PropertyRules.compare_values(value1, value2, case_sensitive=case_sensitive) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value1, value2, tolerance, expected",
|
||||
[
|
||||
(1.0001, 1.0, 1e-3, True),
|
||||
(1.0001, 1.0, 1e-6, False),
|
||||
(1.00000001, 1.0, 1e-6, True),
|
||||
(-1.0001, -1.0, 1e-3, True),
|
||||
],
|
||||
)
|
||||
def test_float_comparison_tolerance(self, value1: float, value2: float, tolerance: float, expected: bool):
|
||||
"""Test float comparison with different tolerance levels."""
|
||||
assert PropertyRules.compare_values(value1, value2, tolerance=tolerance) == expected
|
||||
+21
-21
@@ -8,34 +8,34 @@ from speckle_automate import (
|
||||
)
|
||||
from speckle_automate.fixtures import * # noqa: F401, F403
|
||||
|
||||
from inputs import MinimumSeverity
|
||||
from src.function import automate_function
|
||||
from src.helpers import speckle_print
|
||||
from src.inputs import FunctionInputs
|
||||
|
||||
|
||||
def test_function_run(test_automation_run_data: AutomationRunData, test_automation_token: str):
|
||||
"""Run an integration test for the automate function.
|
||||
class TestFunction:
|
||||
"""Test suite for the automate function."""
|
||||
|
||||
Args:
|
||||
test_automation_run_data (AutomationRunData): The automation run data provided by sdk.
|
||||
test_automation_token (str): The automation token.
|
||||
def test_function_run(self, test_automation_run_data: AutomationRunData, test_automation_token: str):
|
||||
"""Run an integration test for the automate function.
|
||||
|
||||
"""
|
||||
speckle_print(str(test_automation_run_data))
|
||||
speckle_print(str(test_automation_token))
|
||||
Args:
|
||||
test_automation_run_data (AutomationRunData): The automation run data provided by sdk.
|
||||
test_automation_token (str): The automation token.
|
||||
|
||||
"""Run an integration test for the automate function."""
|
||||
automation_context = AutomationContext.initialize(
|
||||
test_automation_run_data, test_automation_token
|
||||
)
|
||||
default_url: str = (
|
||||
"https://drive.google.com/uc?export=download&id=1hiPSw23eOaqd27QD_YsXvZg9PWm7_XBx"
|
||||
)
|
||||
"""
|
||||
speckle_print(str(test_automation_run_data))
|
||||
speckle_print(str(test_automation_token))
|
||||
|
||||
automate_sdk = run_function(
|
||||
automation_context,
|
||||
automate_function,
|
||||
FunctionInputs(spreadsheet_url=default_url),
|
||||
)
|
||||
"""Run an integration test for the automate function."""
|
||||
automation_context = AutomationContext.initialize(test_automation_run_data, test_automation_token)
|
||||
default_url: str = "https://drive.google.com/uc?export=download&id=1hiPSw23eOaqd27QD_YsXvZg9PWm7_XBx"
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
automate_sdk = run_function(
|
||||
automation_context,
|
||||
automate_function,
|
||||
FunctionInputs(spreadsheet_url=default_url, minimum_severity=MinimumSeverity.WARNING, hide_skipped=True),
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
|
||||
+338
-354
@@ -1,3 +1,5 @@
|
||||
"""Test suite for parameter handling functionality."""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
@@ -15,390 +17,372 @@ from helpers import speckle_print
|
||||
from src.rules import PropertyRules
|
||||
|
||||
|
||||
def load_test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
|
||||
"""Load test objects from a Speckle server."""
|
||||
client = SpeckleClient(host="https://app.speckle.systems", use_ssl=True)
|
||||
class TestParameterHandling:
|
||||
"""Test suite for parameter handling functionality."""
|
||||
|
||||
load_dotenv(dotenv_path="../.env")
|
||||
@staticmethod
|
||||
def load_test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
|
||||
"""Load test objects from a Speckle server."""
|
||||
client = SpeckleClient(host="https://app.speckle.systems", use_ssl=True)
|
||||
|
||||
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
|
||||
load_dotenv(dotenv_path="../.env")
|
||||
|
||||
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
|
||||
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
|
||||
|
||||
speckle_print(v2_wall)
|
||||
v2_obj = operations.receive("cdb18060dc48281909e94f0f1d8d3cc0", transport)
|
||||
v3_obj = operations.receive("46f06fef727d64a0bbcbd7ced51e0cd2", transport)
|
||||
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
|
||||
|
||||
# return v2_wall, v3_wall
|
||||
return v2_obj, v3_obj
|
||||
speckle_print(v2_wall)
|
||||
speckle_print(v3_wall)
|
||||
v2_obj = operations.receive("cdb18060dc48281909e94f0f1d8d3cc0", transport)
|
||||
v3_obj = operations.receive("46f06fef727d64a0bbcbd7ced51e0cd2", transport)
|
||||
|
||||
# return v2_wall, v3_wall
|
||||
return v2_obj, v3_obj
|
||||
|
||||
@pytest.fixture
|
||||
def test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
|
||||
"""Pytest fixture to provide test objects."""
|
||||
return load_test_objects(v2_wall, v3_wall)
|
||||
@pytest.fixture
|
||||
def test_objects(self, v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
|
||||
"""Pytest fixture to provide test objects."""
|
||||
return self.load_test_objects(v2_wall, v3_wall)
|
||||
|
||||
def test_deserialization_structure(self, test_objects):
|
||||
"""Test that objects are properly deserialized with correct structure."""
|
||||
v2_obj, v3_obj = test_objects
|
||||
|
||||
def test_deserialization_structure(test_objects):
|
||||
"""Test that objects are properly deserialized with correct structure."""
|
||||
v2_obj, v3_obj = test_objects
|
||||
# Check base class type
|
||||
for obj in [v2_obj, v3_obj]:
|
||||
assert isinstance(obj, Base), f"Expected {obj} to be an instance of Base"
|
||||
|
||||
# Check base class type
|
||||
for obj in [v2_obj, v3_obj]:
|
||||
assert isinstance(obj, Base), f"Expected {obj} to be an instance of Base"
|
||||
# Check v2 structure
|
||||
assert hasattr(v2_obj, "parameters"), "v2_obj should have 'parameters' attribute"
|
||||
assert v2_obj["parameters"] is not None, "v2_obj['parameters'] should not be None"
|
||||
|
||||
# Check v2 structure
|
||||
assert hasattr(v2_obj, "parameters"), "v2_obj should have 'parameters' attribute"
|
||||
assert v2_obj["parameters"] is not None, "v2_obj['parameters'] should not be None"
|
||||
# Check v3 structure
|
||||
assert hasattr(v3_obj, "properties"), "v3_obj should have 'properties' attribute"
|
||||
assert v3_obj["properties"] is not None, "v3_obj['properties'] should not be None"
|
||||
assert "Parameters" in v3_obj["properties"], "'Parameters' key should exist in v3_obj['properties']"
|
||||
|
||||
# Check v3 structure
|
||||
assert hasattr(v3_obj, "properties"), "v3_obj should have 'properties' attribute"
|
||||
assert v3_obj["properties"] is not None, "v3_obj['properties'] should not be None"
|
||||
assert "Parameters" in v3_obj["properties"], "'Parameters' key should exist in v3_obj['properties']"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("category", True), # Test parameters that should exist
|
||||
("WALL_ATTR_WIDTH_PARAM", True), # Test nested parameters
|
||||
("WALL_ATTR_WIDTH_PARAM.value", True),
|
||||
("WALL_ATTR_WIDTH_PARAM.id", True),
|
||||
("WALL_ATTR_WIDTH_PARAM.units", True),
|
||||
("non_existent_param", False), # Test non-existent parameters
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_exists(test_objects, param_name, expected_result):
|
||||
"""Test parameter existence checking in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.has_parameter(v2_obj, param_name) == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name",
|
||||
[
|
||||
"WALL_ATTR_WIDTH_PARAM.id",
|
||||
"WALL_ATTR_WIDTH_PARAM.value",
|
||||
"WALL_ATTR_WIDTH_PARAM",
|
||||
"WALL_ATTR_WIDTH_PARAM.units",
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_retrieval(test_objects, param_name):
|
||||
"""Test parameter value retrieval in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.get_parameter_value(v2_obj, param_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("category", True), # Test parameters that should exist
|
||||
("Width", True), # Test nested parameters
|
||||
("non_existent_param", False), # Test non-existent parameters
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_exists(test_objects, param_name, expected_result):
|
||||
"""Test parameter existence checking in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name_1, param_name_2",
|
||||
[
|
||||
(
|
||||
"properties.Parameters.Instance Parameters.Dimensions.Length.value",
|
||||
"Instance Parameters.Dimensions.Length",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_search_equivalence(test_objects, param_name_1, param_name_2):
|
||||
"""Test parameter existence checking equivalence in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.get_parameter_value(v3_obj, param_name_1) == PropertyRules.get_parameter_value(
|
||||
v3_obj, param_name_2
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("category", True), # Test parameters that should exist
|
||||
("WALL_ATTR_WIDTH_PARAM", True), # Test nested parameters
|
||||
("WALL_ATTR_WIDTH_PARAM.value", True),
|
||||
("WALL_ATTR_WIDTH_PARAM.id", True),
|
||||
("WALL_ATTR_WIDTH_PARAM.units", True),
|
||||
("non_existent_param", False), # Test non-existent parameters
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_exists(self, test_objects, param_name, expected_result):
|
||||
"""Test parameter existence checking in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.has_parameter(v2_obj, param_name) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name",
|
||||
[
|
||||
"WALL_ATTR_WIDTH_PARAM.id",
|
||||
"WALL_ATTR_WIDTH_PARAM.value",
|
||||
"WALL_ATTR_WIDTH_PARAM",
|
||||
"WALL_ATTR_WIDTH_PARAM.units",
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_retrieval(self, test_objects, param_name):
|
||||
"""Test parameter value retrieval in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.get_parameter_value(v2_obj, param_name)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"obj_version, param_name, expected_value, default_value",
|
||||
[
|
||||
# Test direct parameters
|
||||
("v2", "category", "Walls", None),
|
||||
("v3", "category", "Walls", None),
|
||||
# Test nested parameters - using both internal and friendly names
|
||||
("v2", "WALL_ATTR_WIDTH_PARAM", 300, None),
|
||||
("v3", "Construction.Width", 300, None),
|
||||
# Test parameters with units
|
||||
("v2", "CURVE_ELEM_LENGTH", 5300.000000000001, None),
|
||||
("v3", "Instance Parameters.Dimensions.Length", 5300.000000000001, None),
|
||||
# Test non-existent parameters with a default value
|
||||
("v2", "parameters.non_existent", "default", "default"),
|
||||
("v3", "properties.Parameters.non_existent", "default", "default"),
|
||||
],
|
||||
)
|
||||
def test_parameter_value_retrieval(test_objects, obj_version, param_name, expected_value, default_value):
|
||||
"""Test parameter value retrieval from both v2 and v3 objects."""
|
||||
v2_obj, v3_obj = test_objects
|
||||
obj = v2_obj if obj_version == "v2" else v3_obj
|
||||
result = PropertyRules.get_parameter_value(obj, param_name, default_value=default_value)
|
||||
assert result == expected_value
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("category", True), # Test parameters that should exist
|
||||
("Width", True), # Test nested parameters
|
||||
("non_existent_param", False), # Test non-existent parameters
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_exists(self, test_objects, param_name, expected_result):
|
||||
"""Test parameter existence checking in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name_1, param_name_2",
|
||||
[
|
||||
(
|
||||
"properties.Parameters.Instance Parameters.Dimensions.Length.value",
|
||||
"Instance Parameters.Dimensions.Length",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_search_equivalence(self, test_objects, param_name_1, param_name_2):
|
||||
"""Test parameter existence checking equivalence in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.get_parameter_value(v3_obj, param_name_1) == PropertyRules.get_parameter_value(
|
||||
v3_obj, param_name_2
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_value, expected_result",
|
||||
[
|
||||
("category", "Walls", True), # Test exact match
|
||||
("WALL_ATTR_WIDTH_PARAM", 300, True), # Test numeric match
|
||||
("category", "Windows", False), # Test non-match
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_matching(test_objects, param_name, expected_value, expected_result):
|
||||
"""Test parameter value matching in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value(v2_obj, param_name, expected_value) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"obj_version, param_name, expected_value, default_value",
|
||||
[
|
||||
# Test direct parameters
|
||||
("v2", "category", "Walls", None),
|
||||
("v3", "category", "Walls", None),
|
||||
# Test nested parameters - using both internal and friendly names
|
||||
("v2", "WALL_ATTR_WIDTH_PARAM", 300, None),
|
||||
("v3", "Construction.Width", 300, None),
|
||||
# Test parameters with units
|
||||
("v2", "CURVE_ELEM_LENGTH", 5300.000000000001, None),
|
||||
("v3", "Instance Parameters.Dimensions.Length", 5300.000000000001, None),
|
||||
# Test non-existent parameters with a default value
|
||||
("v2", "parameters.non_existent", "default", "default"),
|
||||
("v3", "properties.Parameters.non_existent", "default", "default"),
|
||||
],
|
||||
)
|
||||
def test_parameter_value_retrieval(self, test_objects, obj_version, param_name, expected_value, default_value):
|
||||
"""Test parameter value retrieval from both v2 and v3 objects."""
|
||||
v2_obj, v3_obj = test_objects
|
||||
obj = v2_obj if obj_version == "v2" else v3_obj
|
||||
result = PropertyRules.get_parameter_value(obj, param_name, default_value=default_value)
|
||||
assert result == expected_value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_value, expected_result",
|
||||
[
|
||||
("category", "Walls", True), # Test exact match
|
||||
("WALL_ATTR_WIDTH_PARAM", 300, True), # Test numeric match
|
||||
("category", "Windows", False), # Test non-match
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_matching(self, test_objects, param_name, expected_value, expected_result):
|
||||
"""Test parameter value matching in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value(v2_obj, param_name, expected_value) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_value, expected_result",
|
||||
[
|
||||
("category", "Walls", True), # Test exact match
|
||||
("Width", 300, True), # Test numeric match
|
||||
("category", "Windows", False), # Test non-match
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_value_matching(test_objects, param_name, expected_value, expected_result):
|
||||
"""Test parameter value matching in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.is_parameter_value(v3_obj, param_name, expected_value) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_value, expected_result",
|
||||
[
|
||||
("category", "Walls", True), # Test exact match
|
||||
("Width", 300, True), # Test numeric match
|
||||
("category", "Windows", False), # Test non-match
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_value_matching(self, test_objects, param_name, expected_value, expected_result):
|
||||
"""Test parameter value matching in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.is_parameter_value(v3_obj, param_name, expected_value) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"comparison_func, param_name, value, expected_result",
|
||||
[
|
||||
(PropertyRules.is_parameter_value_greater_than, "WALL_ATTR_WIDTH_PARAM", "200", True), # Test greater than
|
||||
(PropertyRules.is_parameter_value_less_than, "WALL_ATTR_WIDTH_PARAM", "400", True), # Test less than
|
||||
(PropertyRules.is_parameter_value_in_range, "WALL_ATTR_WIDTH_PARAM", "200,400", True), # Test in range
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_numeric_comparisons(self, test_objects, comparison_func, param_name, value, expected_result):
|
||||
"""Test numeric parameter comparisons in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert comparison_func(v2_obj, param_name, value) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"comparison_func, param_name, value, expected_result",
|
||||
[
|
||||
(PropertyRules.is_parameter_value_greater_than, "WALL_ATTR_WIDTH_PARAM", "200", True), # Test greater than
|
||||
(PropertyRules.is_parameter_value_less_than, "WALL_ATTR_WIDTH_PARAM", "400", True), # Test less than
|
||||
(PropertyRules.is_parameter_value_in_range, "WALL_ATTR_WIDTH_PARAM", "200,400", True), # Test in range
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_numeric_comparisons(test_objects, comparison_func, param_name, value, expected_result):
|
||||
"""Test numeric parameter comparisons in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert comparison_func(v2_obj, param_name, value) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"comparison_func, param_name, value, expected_result",
|
||||
[
|
||||
(PropertyRules.is_parameter_value_greater_than, "Width", "200", True), # Test greater than
|
||||
(PropertyRules.is_parameter_value_less_than, "Width", "400", True), # Test less than
|
||||
(PropertyRules.is_parameter_value_in_range, "Width", "200,400", True), # Test in range
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_numeric_comparisons(self, test_objects, comparison_func, param_name, value, expected_result):
|
||||
"""Test numeric parameter comparisons in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert comparison_func(v3_obj, param_name, value) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, pattern, fuzzy, expected_result",
|
||||
[
|
||||
("category", "^Walls$", False, True), # Test exact pattern matches
|
||||
("category", "Walls", True, True), # Test fuzzy matches
|
||||
("category", "Wall", False, True), # Test partial pattern matches
|
||||
("category", "^Windows$", False, False), # Test non-matches
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_like(self, test_objects, param_name, pattern, fuzzy, expected_result):
|
||||
"""Test pattern matching on parameter values in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value_like(v2_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"comparison_func, param_name, value, expected_result",
|
||||
[
|
||||
(PropertyRules.is_parameter_value_greater_than, "Width", "200", True), # Test greater than
|
||||
(PropertyRules.is_parameter_value_less_than, "Width", "400", True), # Test less than
|
||||
(PropertyRules.is_parameter_value_in_range, "Width", "200,400", True), # Test in range
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_numeric_comparisons(test_objects, comparison_func, param_name, value, expected_result):
|
||||
"""Test numeric parameter comparisons in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert comparison_func(v3_obj, param_name, value) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, pattern, fuzzy, expected_result",
|
||||
[
|
||||
("category", "^Walls$", False, True), # Test exact pattern matches
|
||||
("category", "Walls", True, True), # Test fuzzy matches
|
||||
("category", "Wall", False, True), # Test partial pattern matches
|
||||
("category", "^Windows$", False, False), # Test non-matches
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_value_like(self, test_objects, param_name, pattern, fuzzy, expected_result):
|
||||
"""Test pattern matching on parameter values in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.is_parameter_value_like(v3_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, valid_list, expected_result",
|
||||
[
|
||||
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
|
||||
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
|
||||
("category", ["Windows", "Doors"], False), # Test value not in list
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_lists(self, test_objects, param_name, valid_list, expected_result):
|
||||
"""Test list-based parameter checks in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value_in_list(v2_obj, param_name, valid_list) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, pattern, fuzzy, expected_result",
|
||||
[
|
||||
("category", "^Walls$", False, True), # Test exact pattern matches
|
||||
("category", "Walls", True, True), # Test fuzzy matches
|
||||
("category", "Wall", False, True), # Test partial pattern matches
|
||||
("category", "^Windows$", False, False), # Test non-matches
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_like(test_objects, param_name, pattern, fuzzy, expected_result):
|
||||
"""Test pattern matching on parameter values in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value_like(v2_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, valid_list, expected_result",
|
||||
[
|
||||
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
|
||||
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
|
||||
("category", ["Windows", "Doors"], False), # Test value not in list
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_lists(self, test_objects, param_name, valid_list, expected_result):
|
||||
"""Test list-based parameter checks in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.is_parameter_value_in_list(v3_obj, param_name, valid_list) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("WALL_ATTR_ROOM_BOUNDING.value", True), # Test true values
|
||||
("wall_top_is_attached", False), # Test false values
|
||||
],
|
||||
)
|
||||
def test_v2_boolean_parameters(self, test_objects, param_name, expected_result):
|
||||
"""Test boolean parameter checks in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
if expected_result:
|
||||
assert PropertyRules.is_parameter_value_true(v2_obj, param_name)
|
||||
else:
|
||||
assert PropertyRules.is_parameter_value_false(v2_obj, param_name)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, pattern, fuzzy, expected_result",
|
||||
[
|
||||
("category", "^Walls$", False, True), # Test exact pattern matches
|
||||
("category", "Walls", True, True), # Test fuzzy matches
|
||||
("category", "Wall", False, True), # Test partial pattern matches
|
||||
("category", "^Windows$", False, False), # Test non-matches
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_value_like(test_objects, param_name, pattern, fuzzy, expected_result):
|
||||
"""Test pattern matching on parameter values in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.is_parameter_value_like(v3_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("Room Bounding", True), # Test true values
|
||||
("top is attached", False), # Test false values
|
||||
("Top is Attached", False), # Case sensitivity test
|
||||
],
|
||||
)
|
||||
def test_v3_boolean_parameters(self, test_objects, param_name, expected_result):
|
||||
"""Test boolean parameter checks in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
if expected_result:
|
||||
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
|
||||
else:
|
||||
assert PropertyRules.is_parameter_value_false(v3_obj, param_name)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_value, expected_result",
|
||||
[
|
||||
# Test numeric value comparisons
|
||||
("WALL_ATTR_WIDTH_PARAM", 300, True),
|
||||
("WALL_ATTR_WIDTH_PARAM.value", 300, True),
|
||||
("baseLine.length", 5300.000000000002, True),
|
||||
# Test string value comparisons
|
||||
("STRUCTURAL_MATERIAL_PARAM.value", "Fc24", True),
|
||||
("ee1f33e1-5506-4a64-b87b-7b98d30aea52.value", "W30", True),
|
||||
# Test non-matches
|
||||
("WALL_ATTR_WIDTH_PARAM", 301, False),
|
||||
("nonexistent_param", "any_value", False),
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_comparisons(self, v2_wall, param_name, expected_value, expected_result):
|
||||
"""Test value comparisons using v2 wall parameters."""
|
||||
assert PropertyRules.is_equal_value(v2_wall, param_name, expected_value) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, valid_list, expected_result",
|
||||
[
|
||||
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
|
||||
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
|
||||
("category", ["Windows", "Doors"], False), # Test value not in list
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_lists(test_objects, param_name, valid_list, expected_result):
|
||||
"""Test list-based parameter checks in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value_in_list(v2_obj, param_name, valid_list) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"attribute, value, expected",
|
||||
[
|
||||
# Test numeric value comparisons
|
||||
("Type Parameters.Structure.Fc24 (0).thickness", 300, True),
|
||||
("location.length", 5300.000000000002, True),
|
||||
("location.length", 5300, True),
|
||||
# Test string value comparisons
|
||||
("Type Parameters.Text.符号.value", "W30", True),
|
||||
("Instance Parameters.Structural.Structural.value", "Yes", True),
|
||||
# Test non-matches
|
||||
("Type Parameters.Structure.Fc24 (0).thickness", 301, False),
|
||||
("nonexistent_param", "any_value", False),
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_value_comparisons(self, v3_wall, attribute, value, expected):
|
||||
"""Test value comparisons using v3 wall parameters."""
|
||||
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wall, attribute, value, expected",
|
||||
[
|
||||
# V2 wall tests
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300, True),
|
||||
("v2_wall", "type", "W30(Fc24)", True),
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300.0001, False),
|
||||
# V3 wall tests
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300, True),
|
||||
("v3_wall", "type", "W30(Fc24)", True),
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300.0001, False),
|
||||
("v3_wall", "location.length", 5300.000000000002, True),
|
||||
("v3_wall", "location.length", 5300, False),
|
||||
],
|
||||
)
|
||||
def test_identical_comparisons(self, request, wall, attribute, value, expected):
|
||||
"""Test identical value comparisons on both wall versions."""
|
||||
wall_instance = request.getfixturevalue(wall)
|
||||
assert PropertyRules.is_identical_value(wall_instance, attribute, value) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, valid_list, expected_result",
|
||||
[
|
||||
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
|
||||
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
|
||||
("category", ["Windows", "Doors"], False), # Test value not in list
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_lists(test_objects, param_name, valid_list, expected_result):
|
||||
"""Test list-based parameter checks in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
assert PropertyRules.is_parameter_value_in_list(v3_obj, param_name, valid_list) == expected_result
|
||||
@pytest.mark.parametrize(
|
||||
"wall, attribute, value",
|
||||
[
|
||||
# V2 wall tests
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 301),
|
||||
("v2_wall", "STRUCTURAL_MATERIAL_PARAM.value", "Fc25"),
|
||||
("v2_wall", "nonexistent_param", "any_value"),
|
||||
# V3 wall tests
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 301),
|
||||
("v3_wall", "Type Parameters.Text.符号.value", "W31"),
|
||||
("v3_wall", "nonexistent_param", "any_value"),
|
||||
],
|
||||
)
|
||||
def test_not_equal_comparisons(self, request, wall, attribute, value):
|
||||
"""Test not equal comparisons on both wall versions."""
|
||||
wall_instance = request.getfixturevalue(wall)
|
||||
assert PropertyRules.is_not_equal_value(wall_instance, attribute, value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attribute, value, expected_equal, expected_identical",
|
||||
[
|
||||
# Test Yes/No conversion in equals (should convert)
|
||||
("Instance Parameters.Structural.Structural.value", True, True, False), # Yes vs True
|
||||
("Instance Parameters.Structural.Structural.value", "Yes", True, True), # Yes vs "Yes"
|
||||
("Instance Parameters.Structural.Structural.value", "yes", True, False), # Yes vs "yes"
|
||||
],
|
||||
)
|
||||
def test_boolean_conversions(self, v3_wall, attribute, value, expected_equal, expected_identical):
|
||||
"""Test conversion of Yes/No strings to boolean values."""
|
||||
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected_equal
|
||||
assert PropertyRules.is_identical_value(v3_wall, attribute, value) == expected_identical
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("WALL_ATTR_ROOM_BOUNDING.value", True), # Test true values
|
||||
("wall_top_is_attached", False), # Test false values
|
||||
],
|
||||
)
|
||||
def test_v2_boolean_parameters(test_objects, param_name, expected_result):
|
||||
"""Test boolean parameter checks in v2 objects."""
|
||||
v2_obj, _ = test_objects
|
||||
if expected_result:
|
||||
assert PropertyRules.is_parameter_value_true(v2_obj, param_name)
|
||||
else:
|
||||
assert PropertyRules.is_parameter_value_false(v2_obj, param_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_result",
|
||||
[
|
||||
("Room Bounding", True), # Test true values
|
||||
("top is attached", False), # Test false values
|
||||
("Top is Attached", False), # Case sensitivity test
|
||||
],
|
||||
)
|
||||
def test_v3_boolean_parameters(test_objects, param_name, expected_result):
|
||||
"""Test boolean parameter checks in v3 objects."""
|
||||
_, v3_obj = test_objects
|
||||
if expected_result:
|
||||
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
|
||||
else:
|
||||
assert PropertyRules.is_parameter_value_false(v3_obj, param_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, expected_value, expected_result",
|
||||
[
|
||||
# Test numeric value comparisons
|
||||
("WALL_ATTR_WIDTH_PARAM", 300, True),
|
||||
("WALL_ATTR_WIDTH_PARAM.value", 300, True),
|
||||
("baseLine.length", 5300.000000000002, True),
|
||||
# Test string value comparisons
|
||||
("STRUCTURAL_MATERIAL_PARAM.value", "Fc24", True),
|
||||
("ee1f33e1-5506-4a64-b87b-7b98d30aea52.value", "W30", True),
|
||||
# Test non-matches
|
||||
("WALL_ATTR_WIDTH_PARAM", 301, False),
|
||||
("nonexistent_param", "any_value", False),
|
||||
],
|
||||
)
|
||||
def test_v2_parameter_value_comparisons(v2_wall, param_name, expected_value, expected_result):
|
||||
"""Test value comparisons using v2 wall parameters."""
|
||||
assert PropertyRules.is_equal_value(v2_wall, param_name, expected_value) == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attribute, value, expected",
|
||||
[
|
||||
# Test numeric value comparisons
|
||||
("Type Parameters.Structure.Fc24 (0).thickness", 300, True),
|
||||
("location.length", 5300.000000000002, True),
|
||||
("location.length", 5300, True),
|
||||
# Test string value comparisons
|
||||
("Type Parameters.Text.符号.value", "W30", True),
|
||||
("Instance Parameters.Structural.Structural.value", "Yes", True),
|
||||
# Test non-matches
|
||||
("Type Parameters.Structure.Fc24 (0).thickness", 301, False),
|
||||
("nonexistent_param", "any_value", False),
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_value_comparisons(v3_wall, attribute, value, expected):
|
||||
"""Test value comparisons using v3 wall parameters."""
|
||||
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wall, attribute, value, expected",
|
||||
[
|
||||
# V2 wall tests
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300, True),
|
||||
("v2_wall", "type", "W30(Fc24)", True),
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300.0001, False),
|
||||
# V3 wall tests
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300, True),
|
||||
("v3_wall", "type", "W30(Fc24)", True),
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300.0001, False),
|
||||
("v3_wall", "location.length", 5300.000000000002, True),
|
||||
("v3_wall", "location.length", 5300, False),
|
||||
],
|
||||
)
|
||||
def test_identical_comparisons(request, wall, attribute, value, expected):
|
||||
"""Test identical value comparisons on both wall versions."""
|
||||
wall_instance = request.getfixturevalue(wall)
|
||||
assert PropertyRules.is_identical_value(wall_instance, attribute, value) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wall, attribute, value",
|
||||
[
|
||||
# V2 wall tests
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 301),
|
||||
("v2_wall", "STRUCTURAL_MATERIAL_PARAM.value", "Fc25"),
|
||||
("v2_wall", "nonexistent_param", "any_value"),
|
||||
# V3 wall tests
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 301),
|
||||
("v3_wall", "Type Parameters.Text.符号.value", "W31"),
|
||||
("v3_wall", "nonexistent_param", "any_value"),
|
||||
],
|
||||
)
|
||||
def test_not_equal_comparisons(request, wall, attribute, value):
|
||||
"""Test not equal comparisons on both wall versions."""
|
||||
wall_instance = request.getfixturevalue(wall)
|
||||
assert PropertyRules.is_not_equal_value(wall_instance, attribute, value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attribute, value, expected_equal, expected_identical",
|
||||
[
|
||||
# Test Yes/No conversion in equals (should convert)
|
||||
("Instance Parameters.Structural.Structural.value", True, True, False), # Yes vs True
|
||||
("Instance Parameters.Structural.Structural.value", "Yes", True, True), # Yes vs "Yes"
|
||||
("Instance Parameters.Structural.Structural.value", "yes", True, False), # Yes vs "yes"
|
||||
],
|
||||
)
|
||||
def test_boolean_conversions(v3_wall, attribute, value, expected_equal, expected_identical):
|
||||
"""Test conversion of Yes/No strings to boolean values."""
|
||||
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected_equal
|
||||
assert PropertyRules.is_identical_value(v3_wall, attribute, value) == expected_identical
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wall, attribute, expected_value",
|
||||
[
|
||||
# V2 wall tests
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", "300"),
|
||||
("v2_wall", "baseLine.length", "5300.000000000002"),
|
||||
# V3 wall tests
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", "300"),
|
||||
("v3_wall", "location.length", "5300.000000000002"),
|
||||
],
|
||||
)
|
||||
def test_numeric_string_handling(wall, attribute, expected_value, request):
|
||||
"""Test handling of numeric strings in both wall versions."""
|
||||
wall_instance = request.getfixturevalue(wall) # Retrieve fixture dynamically
|
||||
assert PropertyRules.is_equal_value(wall_instance, attribute, expected_value)
|
||||
@pytest.mark.parametrize(
|
||||
"wall, attribute, expected_value",
|
||||
[
|
||||
# V2 wall tests
|
||||
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", "300"),
|
||||
("v2_wall", "baseLine.length", "5300.000000000002"),
|
||||
# V3 wall tests
|
||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", "300"),
|
||||
("v3_wall", "location.length", "5300.000000000002"),
|
||||
],
|
||||
)
|
||||
def test_numeric_string_handling(self, wall, attribute, expected_value, request):
|
||||
"""Test handling of numeric strings in both wall versions."""
|
||||
wall_instance = request.getfixturevalue(wall) # Retrieve fixture dynamically
|
||||
assert PropertyRules.is_equal_value(wall_instance, attribute, expected_value)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Tests for rule processing functionality."""
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def explicit_check_rule():
|
||||
"""Create a rule using explicit CHECK format."""
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"Rule Number": [1, 1, 1],
|
||||
"Logic": ["WHERE", "AND", "CHECK"],
|
||||
"Property Name": ["category", "width", "material"],
|
||||
"Predicate": ["matches", "greater than", "matches"],
|
||||
"Value": ["Walls", "200", "Concrete"],
|
||||
"Message": ["Test message", "", ""],
|
||||
"Severity": ["Error", "", ""],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_rule():
|
||||
"""Create a rule using legacy format (last AND is implicit check)."""
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"Rule Number": [1, 1, 1],
|
||||
"Logic": ["WHERE", "AND", "AND"],
|
||||
"Property Name": ["category", "width", "material"],
|
||||
"Predicate": ["matches", "greater than", "matches"],
|
||||
"Value": ["Walls", "200", "Concrete"],
|
||||
"Message": ["Test message", "", ""],
|
||||
"Severity": ["Error", "", ""],
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user