Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

Expand Down Expand Up @@ -60,22 +60,22 @@ 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
}
return str.slice(1, -1)
},

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 (
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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),
{},
Expand All @@ -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
Expand Down
30 changes: 17 additions & 13 deletions openc3-cosmos-script-runner-api/scripts/script_instrumentor.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 == []

Expand Down Expand Up @@ -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)
Expand Down
7 changes: 2 additions & 5 deletions playwright/tests/utc-time.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand Down
Loading