diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/utilities.js b/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/utilities.js index f92bc4cc07..04aa923b34 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/utilities.js +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/utilities.js @@ -13,10 +13,10 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2025, OpenC3, Inc. # All Rights Reserved # -# This file may also be used under the terms of a commercial license +# This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. */ @@ -60,11 +60,11 @@ export default { if (str.length < 2) { return str } - var firstChar = str.charAt(0) + let firstChar = str.charAt(0) if (firstChar !== '"' && firstChar !== "'") { return str } - var lastChar = str.charAt(str.length - 1) + let lastChar = str.charAt(str.length - 1) if (firstChar !== lastChar) { return str } @@ -72,10 +72,10 @@ export default { }, convertToString(value) { - var i = 0 - var returnValue = '' + let i = 0 + let returnValue = '' if (Object.prototype.toString.call(value).slice(8, -1) === 'Array') { - var arrayLength = value.length + let arrayLength = value.length returnValue = '[ ' for (i = 0; i < arrayLength; i++) { if ( @@ -97,7 +97,7 @@ export default { // This is binary data, display in hex. returnValue = '0x' for (i = 0; i < value.raw.length; i++) { - var nibble = value.raw[i].toString(16).toUpperCase() + let nibble = value.raw[i].toString(16).toUpperCase() if (nibble.length < 2) { nibble = '0' + nibble } diff --git a/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/autocomplete/screenCompleter.js b/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/autocomplete/screenCompleter.js index 88e94e536d..6437dfa301 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/autocomplete/screenCompleter.js +++ b/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/autocomplete/screenCompleter.js @@ -1,5 +1,5 @@ /* -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -19,7 +19,7 @@ import { Api, OpenC3Api } from '@openc3/js-common/services' // Test data useful for testing ScreenCompleter -// var autocompleteData = [ +// let autocompleteData = [ // { // caption: 'HORIZONTAL', // meta: 'Places the widgets it encapsulates horizontally', @@ -52,16 +52,16 @@ export default class ScreenCompleter { } async getCompletions(editor, session, pos, prefix, callback) { - var line = session.getLine(pos.row) - var lineBefore = line.slice(0, pos.column) - var parsedLine = lineBefore.trimStart().split(/ (?![^<]*>)/) - var suggestions = this.autocompleteData + let line = session.getLine(pos.row) + let lineBefore = line.slice(0, pos.column) + let parsedLine = lineBefore.trimStart().split(/ (?![^<]*>)/) + let suggestions = this.autocompleteData // If we have more than 1 we've selected a keyword if (parsedLine.length > 1) { suggestions = suggestions.find((x) => x.caption === parsedLine[0]) } - var result = {} - var more = true + let result = {} + let more = true // If we found suggestions and the suggestions have params // then we do logic to substitute suggestions using the actual // target, packet, item data @@ -72,23 +72,26 @@ export default class ScreenCompleter { } // parsedLine.length - 2 because the last element is blank // e.g. ['LABELVALUE', 'INST', ''] - var current = suggestions['params'][parsedLine.length - 2] + let current = suggestions['params'][parsedLine.length - 2] // Check for Target name, Packet name, and Item name and use // api calls to substitute actual values for suggestions if (current['Target name']) { - var names = await this.api.get_target_names() - suggestions = names.reduce((acc, curr) => ((acc[curr] = 1), acc), {}) + let target_names = await this.api.get_target_names() + suggestions = target_names.reduce( + (acc, curr) => ((acc[curr] = 1), acc), + {}, + ) } else if (current['Packet name']) { - var target = parsedLine[parsedLine.length - 2] - var packets = await this.api.get_all_tlm(target) + let target_name = parsedLine[parsedLine.length - 2] + let packets = await this.api.get_all_tlm(target_name) suggestions = packets.reduce( (acc, pkt) => ((acc[pkt.packet_name] = pkt.description), acc), {}, ) } else if (current['Item name']) { - var target = parsedLine[parsedLine.length - 3] - var packet = parsedLine[parsedLine.length - 2] - var packet = await this.api.get_tlm(target, packet) + let target_name = parsedLine[parsedLine.length - 3] + let packet_name = parsedLine[parsedLine.length - 2] + let packet = await this.api.get_tlm(target_name, packet_name) suggestions = packet.items.reduce( (acc, item) => ((acc[item.name] = item.description), acc), {}, @@ -99,7 +102,7 @@ export default class ScreenCompleter { } result = Object.keys(suggestions || {}).map((x) => { - var completions = { + let completions = { value: x + (more ? ' ' : ''), // We want the autoComplete to continue right up // to the last parameter diff --git a/openc3-cosmos-script-runner-api/scripts/script_instrumentor.py b/openc3-cosmos-script-runner-api/scripts/script_instrumentor.py index 5ce37ac873..3b75fa0888 100644 --- a/openc3-cosmos-script-runner-api/scripts/script_instrumentor.py +++ b/openc3-cosmos-script-runner-api/scripts/script_instrumentor.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -47,7 +47,7 @@ class ScriptInstrumentor(ast.NodeTransformer): def __init__(self, filename): self.filename = filename - self.in_try = False + self.try_count = 0 self.try_nodes = [ast.Try] if sys.version_info >= (3, 11): self.try_nodes.append(ast.TryStar) @@ -69,14 +69,9 @@ def __init__(self, filename): # RunningScript.instance.post_line_instrumentation('myfile.py', 1) # This allows us to retry statements that raise exceptions def track_enter_leave(self, node): - # Determine if we're in a try block - in_try = self.in_try - if not in_try and type(node) in self.try_nodes: - self.in_try = True # Visit the children of the node node = self.generic_visit(node) - if not in_try and type(node) in self.try_nodes: - self.in_try = False + # ast.parse returns a module, so we need to extract # the first element of the body which is the node pre_line = ast.parse( @@ -85,6 +80,7 @@ def track_enter_leave(self, node): post_line = ast.parse( self.post_line_instrumentation.format(self.filename, node.lineno) ).body[0] + true_node = ast.Constant(True) break_node = ast.Break() for new_node in (pre_line, post_line, true_node, break_node): @@ -102,11 +98,13 @@ def track_enter_leave(self, node): # It's actually surprising how many nodes are nested in the new_node for new_node2 in ast.walk(new_node): ast.copy_location(new_node2, node) + # Create an exception handler node to wrap the exception handler code excepthandler = ast.ExceptHandler(type=None, name=None, body=exception_handler) ast.copy_location(excepthandler, node) + # If we're not already in a try block, we need to wrap the node in a while loop - if not self.in_try: + if self.try_count == 0: try_node = ast.Try( # pre_line is the pre_line_instrumentation, node is the original node # and if the code is executed without an exception, we break @@ -136,12 +134,13 @@ def track_enter_leave(self, node): # Call the pre_line_instrumentation ONLY and then execute the node def track_reached(self, node): # Determine if we're in a try block, this is used by track_enter_leave - in_try = self.in_try - if not in_try and type(node) in self.try_nodes: - self.in_try = True + if type(node) in self.try_nodes: + # Increment the try count to account for nested try / except blocks + self.try_count += 1 # Visit the children of the node node = self.generic_visit(node) + pre_line = ast.parse( self.pre_line_instrumentation.format(self.filename, node.lineno) ).body[0] @@ -153,12 +152,17 @@ def track_reached(self, node): # The if_node is effectively a noop that holds the preline & node that we need to execute if_node = ast.If(test=n, body=[pre_line, node], orelse=[]) ast.copy_location(if_node, node) + + # If we're in a try block decrement the count to account for nested try / except blocks + if type(node) in self.try_nodes: + self.try_count -= 1 + return if_node def track_import_from(self, node): # Don't tract from __future__ imports because they must come first or: # SyntaxError: from __future__ imports must occur at the beginning of the file - if node.module != '__future__': + if node.module != "__future__": return self.track_enter_leave(node) # Notes organized (including newlines) per https://docs.python.org/3/library/ast.html#abstract-grammar diff --git a/openc3-cosmos-script-runner-api/test/scripts/test_script_instrumentor.py b/openc3-cosmos-script-runner-api/test/scripts/test_script_instrumentor.py index 13fe8e7b45..d514ec0c60 100644 --- a/openc3-cosmos-script-runner-api/test/scripts/test_script_instrumentor.py +++ b/openc3-cosmos-script-runner-api/test/scripts/test_script_instrumentor.py @@ -1,3 +1,19 @@ +# Copyright 2025 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + import ast import pytest from scripts.script_instrumentor import ScriptInstrumentor @@ -55,6 +71,7 @@ def test_simple_script(mock_running_script): def test_try_except_script(mock_running_script): script = """ try: + print('start') x = 1 / 0 except ZeroDivisionError: x = 0 @@ -68,14 +85,16 @@ def test_try_except_script(mock_running_script): assert mock_running_script.pre_lines == [ ("testfile.py", 2), ("testfile.py", 3), - ("testfile.py", 5), + ("testfile.py", 4), ("testfile.py", 6), + ("testfile.py", 7), ] assert mock_running_script.post_lines == [ - # Line 2 doesn't exit because it raises an exception + # Line 2 doesn't exit because it is a try ("testfile.py", 3), - ("testfile.py", 5), + ("testfile.py", 4), ("testfile.py", 6), + ("testfile.py", 7), ] assert mock_running_script.exceptions == [] @@ -191,6 +210,55 @@ def test_exception_script(mock_running_script): ] +def test_raise_with_try(mock_running_script): + script = """ +i = 0 +raise RuntimeError("Error1") # Handled by us +try: # Initial try + i = 1 + try: # Nested try + i = 2 + except RuntimeError: + i = 3 + raise RuntimeError("BAD") # Handled by them +except RuntimeError: + i = 5 # This handler should execute +raise RuntimeError("Error2") # Handled by us +""" + parsed = ast.parse(script) + tree = ScriptInstrumentor("testfile.py").visit(parsed) + compiled = compile(tree, filename="testfile.py", mode="exec") + local_vars = {'i': None} + exec(compiled, {"RunningScript": mock_running_script}, local_vars) + assert(local_vars["i"] == 5) + + assert mock_running_script.pre_lines == [ + ("testfile.py", 2), + ("testfile.py", 3), + ("testfile.py", 4), + ("testfile.py", 5), + ("testfile.py", 6), + ("testfile.py", 7), + ("testfile.py", 10), + ("testfile.py", 12), + ("testfile.py", 13), + ] + assert mock_running_script.post_lines == [ + ("testfile.py", 2), + ("testfile.py", 3), + ("testfile.py", 5), + ("testfile.py", 7), + ("testfile.py", 10), + ("testfile.py", 12), + ("testfile.py", 13), + ] + assert mock_running_script.exceptions == [ + ("testfile.py", 3), + # Note the exception on line 10 is handled by them + ("testfile.py", 13), + ] + + def test_import_future_script(mock_running_script): script = "from __future__ import annotations\nprint('hi')" parsed = ast.parse(script) diff --git a/playwright/tests/utc-time.spec.ts b/playwright/tests/utc-time.spec.ts index 4339494003..2ebd654b67 100644 --- a/playwright/tests/utc-time.spec.ts +++ b/playwright/tests/utc-time.spec.ts @@ -25,6 +25,8 @@ import { isWithinInterval, } from 'date-fns' +test.use({ storageState: 'adminStorageState.json' }) + test.beforeEach(async ({ page }) => { // Ensure local time await page.goto('/tools/admin/settings') @@ -47,7 +49,6 @@ test.describe('CmdTlmServer', () => { test.use({ toolPath: '/tools/cmdtlmserver', toolName: 'CmdTlmServer', - storageState: 'adminStorageState.json', }) test('displays in local or UTC time', async ({ page, utils }) => { // Allow the table to populate @@ -112,7 +113,6 @@ test.describe('Data Extractor', () => { test.use({ toolPath: '/tools/dataextractor', toolName: 'Data Extractor', - storageState: 'adminStorageState.json', }) test('displays in local or UTC time', async ({ page, utils }) => { @@ -212,7 +212,6 @@ test.describe('Data Viewer', () => { test.use({ toolPath: '/tools/dataviewer', toolName: 'Data Viewer', - storageState: 'adminStorageState.json', }) test('displays in local or UTC time', async ({ page, utils }) => { @@ -287,7 +286,6 @@ test.describe('Packet Viewer', () => { test.use({ toolPath: '/tools/packetviewer', toolName: 'Packet Viewer', - storageState: 'adminStorageState.json', }) test('displays in local or UTC time', async ({ page, utils }) => { @@ -370,7 +368,6 @@ test.describe('Telemetry Grapher', () => { test.use({ toolPath: '/tools/tlmgrapher', toolName: 'Telemetry Grapher', - storageState: 'adminStorageState.json', }) test('displays in local or UTC time', async ({ page, utils }) => {