Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a704aded80 | |||
| 90c5051fc6 | |||
| ec6bdf3485 | |||
| ceaa75d40a | |||
| 0566f7d890 | |||
| b431662031 | |||
| e520d9bc91 | |||
| b6dcfe57df | |||
| ba8443ce92 | |||
| 0bab18d2f2 | |||
| dffb7ea7ba | |||
| 4420fd31f4 | |||
| 168a1f517a | |||
| e49bf225ec | |||
| f3987fced9 | |||
| 1ae3372f42 | |||
| b071380a4f | |||
| 460b21772a |
@@ -31,7 +31,9 @@ jobs:
|
||||
- name: Extract functionInputSchema
|
||||
id: extract_schema
|
||||
run: |
|
||||
python main.py generate_schema "${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
|
||||
python main.py generate_schema "${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
|
||||
echo "Checking if functionSchema.json exists after generation..."
|
||||
ls -lah "${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
|
||||
|
||||
# Step 5: Build and publish the Speckle function
|
||||
- name: Speckle Automate Function - Build and Publish
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.12
|
||||
3.13
|
||||
|
||||
+10
-11
@@ -1,16 +1,15 @@
|
||||
# We use the official Python 3.11 image as our base image and will add our code to it. For more details, see https://hub.docker.com/_/python
|
||||
FROM python:3.13-slim
|
||||
# Use the official Python 3.11 slim image as the base
|
||||
FROM python:3.11-slim
|
||||
|
||||
# We install poetry to generate a list of dependencies which will be required by our application
|
||||
RUN pip install poetry==1.8.4
|
||||
|
||||
# We set the working directory to be the /home/speckle directory; all of our files will be copied here.
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /home/speckle
|
||||
|
||||
# Copy all of our code and assets from the local directory into the /home/speckle directory of the container.
|
||||
# We also ensure that the user 'speckle' owns these files, so it can access them
|
||||
# This assumes that the Dockerfile is in the same directory as the rest of the code
|
||||
# Copy the application files to the working directory
|
||||
COPY . /home/speckle
|
||||
|
||||
# Using poetry, we generate a list of requirements, save them to requirements.txt, and then use pip to install them
|
||||
RUN poetry export --format requirements.txt --output /home/speckle/requirements.txt && pip install --requirement /home/speckle/requirements.txt
|
||||
# Upgrade pip and install dependencies using requirements.txt
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r /home/speckle/requirements.txt
|
||||
|
||||
# Set the entrypoint for running the Speckle function
|
||||
CMD ["python", "-u", "main.py", "run"]
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pydantic==2.10.6",
|
||||
"python-dotenv>=1.0.1",
|
||||
"python-levenshtein>=0.26.1",
|
||||
"specklepy>=2.21.2",
|
||||
"specklepy>=2.21.3",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+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 src.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)
|
||||
|
||||
|
||||
+48
-22
@@ -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,44 @@ 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)}")
|
||||
try:
|
||||
parameter_value = float(parameter_value)
|
||||
except (ValueError, TypeError):
|
||||
return False # Return False if the value cannot be converted to a number
|
||||
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)}")
|
||||
try:
|
||||
parameter_value = float(parameter_value)
|
||||
except (ValueError, TypeError):
|
||||
return False # Return False if the value cannot be converted to a number
|
||||
|
||||
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)
|
||||
@@ -271,8 +290,10 @@ class PropertyRules:
|
||||
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)}")
|
||||
try:
|
||||
parameter_value = float(parameter_value)
|
||||
except (ValueError, TypeError):
|
||||
return False # Return False if the value cannot be converted to a number
|
||||
|
||||
return min_value <= parameter_value <= max_value
|
||||
|
||||
@@ -311,7 +332,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 +346,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 +371,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 +401,7 @@ class PropertyRules:
|
||||
return False, False
|
||||
|
||||
@staticmethod
|
||||
def _compare_values(
|
||||
def compare_values(
|
||||
value1: Any,
|
||||
value2: Any,
|
||||
case_sensitive: bool = False,
|
||||
@@ -401,15 +422,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 +476,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 +504,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 +526,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", "", ""],
|
||||
}
|
||||
)
|
||||
@@ -16,8 +16,8 @@ from src.rule_processor import SeverityLevel, get_severity
|
||||
("ERROR", SeverityLevel.ERROR),
|
||||
("error", SeverityLevel.ERROR),
|
||||
("Error", SeverityLevel.ERROR),
|
||||
("WARN", SeverityLevel.WARNING), # Invalid → Defaults to ERROR
|
||||
("warn", SeverityLevel.WARNING), # Invalid → Defaults to ERROR
|
||||
("WARN", SeverityLevel.WARNING),
|
||||
("warn", SeverityLevel.WARNING),
|
||||
("Critical", SeverityLevel.ERROR), # Invalid → Defaults to ERROR
|
||||
("Severe", SeverityLevel.ERROR), # Invalid → Defaults to ERROR
|
||||
("", SeverityLevel.ERROR), # Empty string → Defaults to ERROR
|
||||
|
||||
Reference in New Issue
Block a user