fix(script): align check, wait and wait_check comparison parsing - #3832
fix(script): align check, wait and wait_check comparison parsing#3832jmthomas wants to merge 2 commits into
Conversation
check validated the comparison operator while wait and wait_check passed the text straight to eval, so compound expressions such as "TIMEUS & 0x0001 == 0x0000" silently worked in two of the three APIs. All three now share one parser and comparator, and eval is gone from the comparison path entirely (check_expression and friends still use it by design). Operands are parsed as literals, which adds hex, octal, binary, underscore separators and recursive list elements, and rejects ambiguous or unparsable forms up front instead of after a timeout. Closes #3802 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3832 +/- ##
==========================================
+ Coverage 79.27% 79.33% +0.06%
==========================================
Files 895 896 +1
Lines 67271 67467 +196
Branches 2641 2659 +18
==========================================
+ Hits 53327 53527 +200
+ Misses 13273 13272 -1
+ Partials 671 668 -3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
There’s a confirmed edge-case bug in Ruby double-quoted \\u{...} escape handling (empty codepoint list) and a documentation example that is misleading for Python users.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR standardizes how check(), wait(), and wait_check() parse and evaluate comparison expressions across Ruby and Python, removing eval from the comparison path to eliminate inconsistent behavior and reduce security risk. It introduces shared literal operand parsing (including numeric bases, underscores, and list literals), consistent operator validation, and expands unit tests and documentation accordingly.
Changes:
- Replaced
eval-based comparison evaluation with a shared operator+operand parser and explicit comparator in both Ruby and Python. - Expanded operand parsing to support additional literal forms (hex/octal/binary, underscores, lists, escape handling) and fail fast on invalid/ambiguous inputs.
- Added/updated unit tests and docs to enforce consistent behavior across
check,wait, andwait_check.
File summaries
| File | Description |
|---|---|
| openc3/spec/script/extract_spec.rb | Adds Ruby unit tests for expanded operand parsing and comparison validation. |
| openc3/spec/script/api_shared_spec.rb | Adds Ruby API-level consistency tests across check/wait/wait_check. |
| openc3/python/test/utilities/test_extract.py | Adds Python unit tests for operand parsing and compare_values. |
| openc3/python/test/script/test_api_shared.py | Updates Python script API tests for new comparison behavior and adds consistency tests. |
| openc3/python/test/api/test_api_shared.py | Updates Python API tests for new comparison behavior and adds consistency tests. |
| openc3/python/openc3/utilities/extract.py | Implements operand parsing without eval and adds compare_values. |
| openc3/python/openc3/script/api_shared.py | Switches script-side check/wait to parser+comparator callables instead of eval. |
| openc3/python/openc3/api/api_shared.py | Switches API-side check/wait to parser+comparator callables instead of eval. |
| openc3/lib/openc3/script/extract.rb | Implements Ruby operand parsing (strings, escapes, lists, numeric formats) and compare_values. |
| openc3/lib/openc3/script/api_shared.rb | Switches Ruby check/wait implementation to parsed comparisons/callables instead of eval. |
| docs.openc3.com/docs/guides/scripting-api.md | Documents supported comparison syntax and limitations for check, wait, and wait_check. |
Review details
Suppressed comments (2)
docs.openc3.com/docs/guides/scripting-api.md:4163
- This note is shown for the Python tab too, but the interpolation example uses Ruby syntax (
#{expected}) and referencescheck()specifically in thewait()section. Consider making the example language-neutral or giving both Ruby and Python examples.
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). An unquoted value is rejected with `Uninitialized constant ON. Did you mean 'ON' as a string?`. Quoted values follow the string literal rules of the script language, so escape sequences are processed in Ruby double quoted strings and in all Python strings. The Ruby control and meta escapes `\c`, `\C-` and `\M-` are rejected rather than silently changed, as is string interpolation (Ruby `"#{...}"`, Python f-strings) because the comparison is not evaluated as code. Interpolate in the script itself, e.g. `check("INST HEALTH_STATUS TYPE == '#{expected}'")`.
docs.openc3.com/docs/guides/scripting-api.md:4452
- This note is shown for the Python tab too, but the interpolation example uses Ruby syntax (
#{expected}) and referencescheck()in thewait_check()section. Consider making the example language-neutral or giving both Ruby and Python examples.
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). An unquoted value is rejected with `Uninitialized constant ON. Did you mean 'ON' as a string?`. Quoted values follow the string literal rules of the script language, so escape sequences are processed in Ruby double quoted strings and in all Python strings. The Ruby control and meta escapes `\c`, `\C-` and `\M-` are rejected rather than silently changed, as is string interpolation (Ruby `"#{...}"`, Python f-strings) because the comparison is not evaluated as code. Interpolate in the script itself, e.g. `check("INST HEALTH_STATUS TYPE == '#{expected}'")`.
- Files reviewed: 11/11 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # so unlike eval it can not execute arbitrary code. It processes string escape sequences | ||
| # and requires a single complete literal, so "== 'a' garbage 'b'" is a syntax error. | ||
| try: | ||
| return ast.literal_eval(operand) |
There was a problem hiding this comment.
This made the python version a lot simpler than the Ruby version
|
|
||
| operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval) | ||
| return nil unless operator | ||
| lambda { |value| compare_values(value, operator, operand) } |
| operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval) | ||
| if operator is None: | ||
| return None | ||
| return lambda value: compare_values(value, operator, operand) |
| rescue ArgumentError, NoMethodError, TypeError | ||
| # Comparing incompatible types, e.g. nil > 1, is simply not a match | ||
| false | ||
| end |
There was a problem hiding this comment.
Ruby compare_values switch statement with all the operands we support
| raise RuntimeError(f"ERROR: Invalid operator: '{operator}'") | ||
| except TypeError: | ||
| # Comparing incompatible types, e.g. None > 1, is simply not a match | ||
| return False |
There was a problem hiding this comment.
Python compare_values switch statement with all the operands we support
|
|
SonarCloud is complaining about duplication between Ruby and Python |


What changed
check validated the comparison operator while wait and wait_check passed the text straight to eval, so compound expressions such as "TIMEUS & 0x0001 == 0x0000" silently worked in two of the three APIs. All three now share one parser and comparator, and eval is gone from the comparison path entirely (check_expression and friends still use it by design). Operands are parsed as literals, which adds hex, octal, binary, underscore separators and recursive list elements, and rejects ambiguous or unparsable forms up front instead of after a timeout.
Why it changed
Closes #3802 which was the different behavior between wait and wait_check. This also closes potential security escapes by not using eval.
Testing strategy
Added a bunch new unit tests. Ran various script code in COSMOS 7.3 and compared output to this version.
🤖 Generated with Claude Code