Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion openc3/lib/openc3/script/api_shared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,18 @@
# Note: We have to preserve the original 'value' variable because we're going to eval against it
value_str = value.is_a?(String) ? "'#{value}'" : value
with_value = "with value == #{value_str}"
if eval(string)

eval_is_valid = _check_eval_validity(value, comparison_to_eval)
unless eval_is_valid
message = "Invalid comparison for types"
if $disconnect
puts "ERROR: #{message}"
else
raise CheckError, message
end
end

if eval_is_valid && eval(string)
puts "#{check_str} success #{with_value}"
else
message = "#{check_str} failed #{with_value}"
Expand All @@ -883,5 +894,17 @@
raise e
end
end

def _check_eval_validity(value, comparison)
return true if comparison.nil? || comparison.empty?

operator, operand = extract_operator_and_operand_from_comparison(comparison)

if [">=", "<=", ">", "<"].include?(operator)

Check warning on line 903 in openc3/lib/openc3/script/api_shared.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this "if" statement with the nested one.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAgFGVh17VRJvTZR&open=AZ1GZAgFGVh17VRJvTZR&pullRequest=3143
return false if value.nil? || operand.nil? || value.is_a?(Array) || operand.is_a?(Array)
end

return true
end
end
end
55 changes: 42 additions & 13 deletions openc3/lib/openc3/script/extract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

require 'json'
require 'openc3/utilities/store'

module OpenC3
Expand Down Expand Up @@ -154,22 +155,50 @@
end

def extract_fields_from_check_text(text)
split_string = text.split
raise "ERROR: Check improperly specified: #{text}" if split_string.length < 3
target_name, packet_name, item_name, comparison = text.split(nil, 4) # Ruby: second split arg is max number of resultant elements
raise "ERROR: Check improperly specified: #{text}" if item_name.nil?

Check warning on line 159 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZS&open=AZ1GZAjlGVh17VRJvTZS&pullRequest=3143

target_name = split_string[0]
packet_name = split_string[1]
item_name = split_string[2]
comparison_to_eval = nil
return [target_name, packet_name, item_name, comparison_to_eval] if split_string.length == 3
raise "ERROR: Check improperly specified: #{text}" if split_string.length < 4
# comparison is either nil, the comparison string, or an empty string.
# We need it to not be an empty string.
comparison = nil if comparison&.length == 0

operator, _ = comparison&.split(nil, 2)

Check warning on line 165 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the trailing underscore from this multiple assignment.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZT&open=AZ1GZAjlGVh17VRJvTZT&pullRequest=3143
raise "ERROR: Use '==' instead of '=': #{comparison}" if operator == "="

Check warning on line 166 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZU&open=AZ1GZAjlGVh17VRJvTZU&pullRequest=3143

return [target_name, packet_name, item_name, comparison]
end

# Splits `check()` comparison expressions, e.g. "== 'foo bar'" becomes ["==", "foo bar"]
def extract_operator_and_operand_from_comparison(comparison)
valid_operators = ["==", "!=", ">=", "<=", ">", "<", "in"]

split_string = text.split(/ /) # Split on regex spaces to preserve spaces in comparison
index = split_string.rindex(item_name)
comparison_to_eval = split_string[(index + 1)..(split_string.length - 1)].join(" ")
raise "ERROR: Use '==' instead of '=': #{text}" if split_string[3] == '='
operator, operand = comparison.split(nil, 2) # Ruby: second split arg is max number of resultant elements

return [target_name, packet_name, item_name, comparison_to_eval]
if operand.nil?
# Don't allow operator without operand
raise "ERROR: Invalid comparison, must specify an operand: #{comparison}" if !operator.nil?

Check warning on line 179 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZV&open=AZ1GZAjlGVh17VRJvTZV&pullRequest=3143
return [nil, nil]
end

raise "ERROR: Invalid operator: #{comparison}" unless valid_operators.include?(operator)

Check warning on line 183 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZW&open=AZ1GZAjlGVh17VRJvTZW&pullRequest=3143

# Handle string operand: remove surrounding double/single quotes
if operand.match?(/^(['"])(.*)\1$/m) # Starts with single or double quote, and ends with matching quote
operand = operand[1..-2]
return [operator, operand]
end

# Handle other operand types
if operand == "nil"
operand = nil
else
begin
operand = JSON.parse(operand)
rescue JSON::ParserError
raise "ERROR: Unable to parse operand: #{operand}"

Check warning on line 198 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZX&open=AZ1GZAjlGVh17VRJvTZX&pullRequest=3143
end
end
return [operator, operand]
end
end
end
26 changes: 25 additions & 1 deletion openc3/python/openc3/api/api_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from openc3.utilities.extract import (
extract_fields_from_check_text,
extract_fields_from_tlm_text,
extract_operator_and_operand_from_comparison,
)


Expand Down Expand Up @@ -965,8 +966,12 @@
else:
value_str = value
with_value = f"with value == {value_str}"

eval_is_valid = _check_eval_validity(value, comaparison_to_eval)

Check failure on line 970 in openc3/python/openc3/api/api_shared.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F821)

openc3/api/api_shared.py:970:49: F821 Undefined name `comaparison_to_eval`
if not eval_is_valid:
raise CheckError("ERROR: Invalid comparison for types")
try:
if eval(string):
if eval_is_valid and eval(string):
print(f"{check_str} success {with_value}")
else:
message = f"{check_str} failed {with_value}"
Expand All @@ -986,6 +991,25 @@
return value


def _check_eval_validity(value, comparison):
if not comparison:
return True

operator, operand = extract_operator_and_operand_from_comparison(comparison)

if operator in [">=", "<=", ">", "<"]:
if value is None or operand is None or isinstance(value, list) or isinstance(operand, list):

Check failure on line 1001 in openc3/python/openc3/api/api_shared.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (SIM102)

openc3/api/api_shared.py:1000:5: SIM102 Use a single `if` statement instead of nested `if` statements help: Combine `if` statements using `and`
return False

if operator == "in": # Ruby doesn't have this operator
if isinstance(operand, str) and not isinstance(value, str):
return False
elif not isinstance(operand, list):
return False

Check failure on line 1008 in openc3/python/openc3/api/api_shared.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (SIM114)

openc3/api/api_shared.py:1005:9: SIM114 Combine `if` branches using logical `or` operator help: Combine `if` branches

Check warning on line 1008 in openc3/python/openc3/api/api_shared.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either merge this branch with the identical one on line "1006" or change one of the implementations.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAkxGVh17VRJvTZe&open=AZ1GZAkxGVh17VRJvTZe&pullRequest=3143

return True


# Interesting formatter to a specific number of significant digits:
# https://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python?rq=3
# def format(value, sigfigs=9):
Expand Down
30 changes: 29 additions & 1 deletion openc3/python/openc3/script/api_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from openc3.utilities.extract import (
extract_fields_from_check_text,
extract_fields_from_tlm_text,
extract_operator_and_operand_from_comparison,
)

from .exceptions import CheckError
Expand Down Expand Up @@ -1046,8 +1047,16 @@
else:
value_str = value
with_value = f"with value == {value_str}"

eval_is_valid = _check_eval_validity(value, comparison_to_eval)
if not eval_is_valid:
message = "Invalid comparison for types"
if openc3.script.DISCONNECT:
print(f"ERROR: {message}")
else:
raise CheckError(message)
try:
if eval(string):
if eval_is_valid and eval(string):
print(f"{check_str} success {with_value}")
else:
message = f"{check_str} failed {with_value}"
Expand All @@ -1070,6 +1079,25 @@
return value


def _check_eval_validity(value, comparison):
if not comparison:
return True

operator, operand = extract_operator_and_operand_from_comparison(comparison)

if operator in [">=", "<=", ">", "<"]:
if value is None or operand is None or isinstance(value, list) or isinstance(operand, list):

Check failure on line 1089 in openc3/python/openc3/script/api_shared.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (SIM102)

openc3/script/api_shared.py:1088:5: SIM102 Use a single `if` statement instead of nested `if` statements help: Combine `if` statements using `and`
return False

if operator == "in": # Ruby doesn't have this operator
if isinstance(operand, str) and not isinstance(value, str):
return False
elif not isinstance(operand, list):
return False

Check failure on line 1096 in openc3/python/openc3/script/api_shared.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (SIM114)

openc3/script/api_shared.py:1093:9: SIM114 Combine `if` branches using logical `or` operator help: Combine `if` branches

Check warning on line 1096 in openc3/python/openc3/script/api_shared.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either merge this branch with the identical one on line "1094" or change one of the implementations.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAj_GVh17VRJvTZY&open=AZ1GZAj_GVh17VRJvTZY&pullRequest=3143

return True


# Interesting formatter to a specific number of significant digits:
# https://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python?rq=3
# def format(value, sigfigs=9):
Expand Down
77 changes: 58 additions & 19 deletions openc3/python/openc3/utilities/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@
# if purchased from OpenC3, Inc.

import ast
import json
import re


SCANNING_REGULAR_EXPRESSION = re.compile(
r"(?:\"(?:[^\\\"]|\\.)*\") | (?:'(?:[^\\']|\\.)*') | (?:\[.*\]) | \S+", re.VERBOSE
r"(?:\"(?:[^\\\"]|\\.)*\") | (?:'(?:[^\\']|\\.)*') | (?:\[(?:[^\\\[\]]|\\.)*\]) | \S+", re.VERBOSE

Check warning on line 22 in openc3/python/openc3/utilities/extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unwrap this unnecessarily grouped subpattern.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAkbGVh17VRJvTZZ&open=AZ1GZAkbGVh17VRJvTZZ&pullRequest=3143

Check warning on line 22 in openc3/python/openc3/utilities/extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unwrap this unnecessarily grouped subpattern.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAkbGVh17VRJvTZb&open=AZ1GZAkbGVh17VRJvTZb&pullRequest=3143

Check warning on line 22 in openc3/python/openc3/utilities/extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unwrap this unnecessarily grouped subpattern.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAkbGVh17VRJvTZa&open=AZ1GZAkbGVh17VRJvTZa&pullRequest=3143

Check warning on line 22 in openc3/python/openc3/utilities/extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its complexity from 23 to the 20 allowed.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAkbGVh17VRJvTZc&open=AZ1GZAkbGVh17VRJvTZc&pullRequest=3143
)

SPLIT_WITH_REGEX = re.compile(r"\s+with\s+", re.IGNORECASE)
SPLIT_WITH_OPTIONAL_WHITESPACE_REGEX = re.compile(r"\s*with\s*", re.IGNORECASE)

# Regular expression to identify a String as a floating point number
FLOAT_CHECK_REGEX = re.compile(r"\A\s*[-+]?\d*\.\d+\s*\Z")
Expand Down Expand Up @@ -135,8 +137,12 @@


def extract_fields_from_cmd_text(text):
split_string = re.split(SPLIT_WITH_REGEX, text, maxsplit=2)
if len(split_string) == 1 and SPLIT_WITH_REGEX.match(text):
split_string = re.split(SPLIT_WITH_REGEX, text, maxsplit=1) # 1 split, therefore 2 elements
if len(split_string) == 0 or split_string[0] == "":
raise RuntimeError("ERROR: text must not be empty")
if (len(split_string) == 1 and re.search(SPLIT_WITH_OPTIONAL_WHITESPACE_REGEX, text)) or (
len(split_string) == 2 and split_string[1] == ""
):
raise RuntimeError(f"ERROR: 'with' must be followed by parameters : {text:s}")

# Extract target_name and cmd_name
Expand Down Expand Up @@ -182,7 +188,7 @@


def extract_fields_from_tlm_text(text):
split_string = text.split(" ")
split_string = text.split()
if len(split_string) != 3:
raise RuntimeError(f"ERROR: Telemetry Item must be specified as 'TargetName PacketName ItemName' : {text}")
target_name = split_string[0]
Expand Down Expand Up @@ -215,21 +221,54 @@


def extract_fields_from_check_text(text):
split_string = text.split(" ")
if len(split_string) < 3:
raise RuntimeError(f"ERROR: Check improperly specified: {text}")
target_name = split_string[0]
packet_name = split_string[1]
item_name = split_string[2]
comparison_to_eval = None
if len(split_string) == 3:
return [target_name, packet_name, item_name, comparison_to_eval]
if len(split_string) < 4:
fields_split = text.split(None, 3) # Python: second split arg is max number of splits
if len(fields_split) < 3:
raise RuntimeError(f"ERROR: Check improperly specified: {text}")
target_name, packet_name, item_name, *comparison = fields_split

# comparison is a list, guaranteed to be of length 0 or 1 because of the split 3 with the splat operator above.
# We need it to be either None or the comparison string.
if len(comparison):
comparison = comparison[0]
else:
comparison = None

if comparison and len(comparison):
operator, *_ = comparison.split(None, 1)
if operator == "=":
raise RuntimeError(f"ERROR: Use '==' instead of '=': {comparison}")

# TODO: Ruby version has additional code to split on regex spaces
comparison_to_eval = " ".join(split_string[3:])
if split_string[3] == "=":
raise RuntimeError(f"ERROR: Use '==' instead of '=': {text}")
return target_name, packet_name, item_name, comparison

return target_name, packet_name, item_name, comparison_to_eval

# Splits `check()` comparison expressions, e.g. "== 'foo bar'" becomes ["==", "foo bar"]
def extract_operator_and_operand_from_comparison(comparison):
valid_operators = ["==", "!=", ">=", "<=", ">", "<", "in"]

operator, operand = comparison.split(None, 1) # Python: second split arg is max number of splits

if operand is None:
if operator is not None:
raise RuntimeError(f"ERROR: Invalid comparison, must specify an operand: {comparison}")
return [None, None]

if operator not in valid_operators:
raise RuntimeError(f"ERROR: Invalid operator: {comparison}")

# Handle string operand: remove surrounding double/single quotes
quote_match = re.match(
r"^(['\"])(.*)\1$", operand, re.DOTALL
) # Starts with single or double quote, and ends with matching quote
if quote_match:
operand = quote_match.group(2)
return operator, operand

# Handle other operand types
if operand == "None":
operand = None
else:
try:
operand = json.loads(operand)
except json.JSONDecodeError:
raise RuntimeError(f"ERROR: Unable to parse operand: {operand}")

Check failure on line 273 in openc3/python/openc3/utilities/extract.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (B904)

openc3/utilities/extract.py:273:13: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
return operator, operand
Loading
Loading