fix: 4 broken-tool fixes (compile json long, connect_to_device LoginMode,
create_folder SP21+ fallback, ensure_project_open cross-project switch)
+ bench results
compile_project.py + get_compile_messages.py:
- IronPython 2.7's json.dumps cannot serialize System.Int64-backed `long`
values, which is what CODESYS's compile-message objects expose as
line_number / position. Added _coerce_int + _coerce_str helpers and a
shared _build_message_entry function. Three duplicated message-building
blocks collapsed into single helper calls.
- Defensive `try: json.dumps(...) except TypeError: json.dumps(default=str)`
so a stray field that slips past the helpers doesn't kill the emit.
connect_to_device.py:
- SP21+ may expose the login enum as LoginMode rather than
OnlineChangeOption. Extended the candidate sweep to probe both
script_engine.LoginMode and script_engine.OnlineChangeOption AND
online_app.LoginMode/OnlineChangeOption (some builds attach it to the
app object). Added "OnlineChange" + "Login" + "Download" to the
preferred-priority list. Added a 3-arg call shape variant for SPs
that take (mode, secondary-mode, force-bool).
create_folder.py:
- parent_object.create_folder() is no longer exposed on every parent type
in SP21+. Added two fallback factories tried in order:
1. parent.create_object(typeUuid='85d1215e-6520-4983-9a55-2d39d1f24cb4', name=...)
2. parent.add(script_engine.types.IecFolder, name=...)
with detailed warnings when each path fails. Final TypeError now lists
every factory tried so a future SP rotation surfaces clearly.
ensure_project_open.py:
- Uncommented the close-prior-project branch (was a TODO since the
initial fork). Cross-project switches in a persistent CODESYS now do
save() -> close() -> 500ms pump -> open(target). Without this,
projects.open against a different already-primary project fails
intermittently on file lock contention or pops a "project in use"
modal that freezes the IDE thread.
- save() is best-effort: if it raises (transient lock, save-as required)
we still proceed with close + open rather than getting stuck in a
half-switched state forever.
tests/bench-results.json:
- Captured timings from a clean run on MCPTest2 (PLCWinNT, 5 lib refs,
~12 POUs). 9 tools x 2 modes x iterations.
Headers (mean ms): persistent vs headless --
open_project 7700 vs 40041 (~5x; first call cold)
mirror_export 1547 vs 23723 (~15x)
list_project_libraries 1565 vs 23322 (~15x)
get_all_pou_code 1607 vs 23376 (~15x)
save_project 2095 vs 23321 (~11x)
create_pou (FB) 1540 vs 23903 (~16x)
delete_object 1544 vs 27420 (~18x)
bump_project_version 1540 vs 30678 (~20x)
bump_project_version #2 1556 vs 37769 (~24x)
- set_pou_code FAILED in both modes -- bench harness param-shape issue
(multi-line code passed verbatim to triple-quoted-string interpolation
doesn't survive the round-trip). Tool itself works fine through the
MCP tool call path; bench needs to escape newlines / use the same
prepareScriptWithHelpers shape the server uses. Filed for follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2e2ab00ae1
commit
2607063306
6 changed files with 673 additions and 173 deletions
|
|
@ -1,5 +1,72 @@
|
|||
import sys, scriptengine as script_engine, os, traceback, json
|
||||
|
||||
|
||||
def _coerce_int(v):
|
||||
"""IronPython 2.7's json.dumps cannot serialize System.Int64-backed
|
||||
`long` values, which is what CODESYS's compile-message objects expose
|
||||
as line_number / position. Coerce to native int. Returns None on
|
||||
failure so the message still serializes (just with line=null) instead
|
||||
of taking down the whole emit."""
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_str(v):
|
||||
"""Some message fields come back as System.Uri / System.IO.FileInfo /
|
||||
similar CLR objects whose default __str__ json.dumps refuses. Force a
|
||||
str() so the value is always serializable."""
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
return str(v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _build_message_entry(msg):
|
||||
"""Extract a JSON-serializable dict from a single compile-message object.
|
||||
Centralised so both compile_project and get_compile_messages share the
|
||||
same shape and the same coercion logic."""
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
try:
|
||||
sev = str(msg.severity).lower()
|
||||
except Exception:
|
||||
sev = 'unknown'
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
text = None
|
||||
for attr in ('text', 'message'):
|
||||
if hasattr(msg, attr):
|
||||
text = _coerce_str(getattr(msg, attr))
|
||||
if text is not None:
|
||||
break
|
||||
if text is None:
|
||||
text = _coerce_str(msg)
|
||||
entry['text'] = text
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = _coerce_str(msg.object_name)
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = _coerce_str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = _coerce_int(msg.line_number)
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = _coerce_int(msg.position)
|
||||
return entry
|
||||
|
||||
|
||||
try:
|
||||
print("DEBUG: compile_project script: Project='%s'" % PROJECT_FILE_PATH)
|
||||
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
||||
|
|
@ -56,29 +123,7 @@ try:
|
|||
if msg_objects is not None:
|
||||
messages_found = True
|
||||
for msg in msg_objects:
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
sev = str(msg.severity).lower()
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg)))
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = msg.object_name
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = msg.line_number
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = msg.position
|
||||
messages.append(entry)
|
||||
messages.append(_build_message_entry(msg))
|
||||
print("DEBUG: Got %d messages from app.get_message_objects()" % len(messages))
|
||||
except Exception as e:
|
||||
print("DEBUG: app.get_message_objects() failed: %s" % e)
|
||||
|
|
@ -92,29 +137,7 @@ try:
|
|||
if msg_objects is not None:
|
||||
messages_found = True
|
||||
for msg in msg_objects:
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
sev = str(msg.severity).lower()
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg)))
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = msg.object_name
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = msg.line_number
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = msg.position
|
||||
messages.append(entry)
|
||||
messages.append(_build_message_entry(msg))
|
||||
print("DEBUG: Got %d messages from system.get_message_objects()" % len(messages))
|
||||
except Exception as e:
|
||||
print("DEBUG: system.get_message_objects() failed: %s" % e)
|
||||
|
|
@ -128,34 +151,20 @@ try:
|
|||
if msg_objects is not None:
|
||||
messages_found = True
|
||||
for msg in msg_objects:
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
sev = str(msg.severity).lower()
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg)))
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = msg.object_name
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = msg.line_number
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = msg.position
|
||||
messages.append(entry)
|
||||
messages.append(_build_message_entry(msg))
|
||||
print("DEBUG: Got %d messages from system.get_messages()" % len(messages))
|
||||
except Exception as e:
|
||||
print("DEBUG: system.get_messages() failed: %s" % e)
|
||||
|
||||
messages_json = json.dumps(messages)
|
||||
# Defensive json.dumps: if a stray field still slips past the coercion
|
||||
# helpers, retry with a default=str fallback so a single odd type
|
||||
# doesn't kill the whole emit. The default param converts unknown
|
||||
# objects via str() instead of raising TypeError.
|
||||
try:
|
||||
messages_json = json.dumps(messages)
|
||||
except TypeError as je:
|
||||
print("WARN: json.dumps raised %s -- retrying with default=str fallback" % je)
|
||||
messages_json = json.dumps(messages, default=lambda o: str(o))
|
||||
print("### COMPILE_MESSAGES_START ###")
|
||||
print(messages_json)
|
||||
print("### COMPILE_MESSAGES_END ###")
|
||||
|
|
|
|||
|
|
@ -19,36 +19,74 @@ try:
|
|||
if not hasattr(online_app, 'login'):
|
||||
raise TypeError("Online application does not support login().")
|
||||
|
||||
# Discover OnlineChangeOption members defensively. Different SPs expose
|
||||
# different names. Build candidate enum values in priority order.
|
||||
# Discover login-mode enum members defensively. Different SPs expose the
|
||||
# enum under different names and different module locations:
|
||||
# - Pre-SP21: script_engine.OnlineChangeOption (TryOnlineChange / WithDownload / ...)
|
||||
# - SP21+: script_engine.LoginMode (rebadged; some members renamed/removed)
|
||||
# - Some builds attach it to the online_app object instead.
|
||||
# Probe every known location and merge the discovered members.
|
||||
enum_sources = []
|
||||
for src_name in ('LoginMode', 'OnlineChangeOption'):
|
||||
if hasattr(script_engine, src_name):
|
||||
try:
|
||||
enum_sources.append((src_name, getattr(script_engine, src_name)))
|
||||
except Exception:
|
||||
pass
|
||||
for src_name in ('LoginMode', 'OnlineChangeOption'):
|
||||
if hasattr(online_app, src_name):
|
||||
try:
|
||||
enum_sources.append(('online_app.' + src_name, getattr(online_app, src_name)))
|
||||
except Exception:
|
||||
pass
|
||||
if not enum_sources:
|
||||
print("DEBUG: No login-mode enum found on script_engine or online_app -- relying on plain-bool fallbacks.")
|
||||
|
||||
# Priority order: prefer "no-download / online change" semantics (least
|
||||
# invasive), then download variants, then null/none.
|
||||
preferred_order = ('TryOnlineChange', 'OnlineChangeOnly', 'Try', 'OnlineChange',
|
||||
'WithDownload', 'ForceDownload', 'Download',
|
||||
'None_', 'None')
|
||||
|
||||
enum_candidates = []
|
||||
if hasattr(script_engine, 'OnlineChangeOption'):
|
||||
oc = script_engine.OnlineChangeOption
|
||||
oc_members = sorted([m for m in dir(oc) if not m.startswith('_')])
|
||||
print("DEBUG: OnlineChangeOption members: %s" % oc_members)
|
||||
# Priority order: prefer "try"-ish (no-download), then "download" variants
|
||||
for preferred in ('Try', 'TryOnlineChange', 'OnlineChangeOnly',
|
||||
'WithDownload', 'ForceDownload', 'None_', 'None'):
|
||||
if preferred in oc_members:
|
||||
seen_keys = set()
|
||||
for src_name, oc in enum_sources:
|
||||
try:
|
||||
members = sorted([m for m in dir(oc) if not m.startswith('_')])
|
||||
except Exception:
|
||||
members = []
|
||||
print("DEBUG: %s members: %s" % (src_name, members))
|
||||
for preferred in preferred_order:
|
||||
if preferred in members:
|
||||
key = '%s.%s' % (src_name, preferred)
|
||||
if key not in seen_keys:
|
||||
try:
|
||||
enum_candidates.append((key, getattr(oc, preferred)))
|
||||
seen_keys.add(key)
|
||||
except Exception:
|
||||
pass
|
||||
for m in members:
|
||||
key = '%s.%s' % (src_name, m)
|
||||
if key not in seen_keys:
|
||||
try:
|
||||
enum_candidates.append((preferred, getattr(oc, preferred)))
|
||||
except Exception:
|
||||
pass
|
||||
# Append all remaining members as fallbacks
|
||||
for m in oc_members:
|
||||
if m not in [n for n, _ in enum_candidates]:
|
||||
try:
|
||||
enum_candidates.append((m, getattr(oc, m)))
|
||||
enum_candidates.append((key, getattr(oc, m)))
|
||||
seen_keys.add(key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Build call-shape candidates for login(): a list of (description, args-tuple).
|
||||
call_shapes = []
|
||||
for nm, val in enum_candidates:
|
||||
# Two-arg shape (most common SP21+): (mode, force_download_bool).
|
||||
call_shapes.append(("login(%s, False)" % nm, (val, False)))
|
||||
call_shapes.append(("login(%s, True)" % nm, (val, True)))
|
||||
# One-arg shape (older).
|
||||
call_shapes.append(("login(%s)" % nm, (val,)))
|
||||
# Also try plain bools and no-arg as fall-backs (for very old SPs)
|
||||
# Three-arg shape some builds use: (mode, secondary-mode, bool). Try with
|
||||
# the strongest "do nothing" pair we can find at the front of candidates.
|
||||
if enum_candidates:
|
||||
first_nm, first_val = enum_candidates[0]
|
||||
call_shapes.append(("login(%s, %s, False)" % (first_nm, first_nm), (first_val, first_val, False)))
|
||||
# Also try plain bools and no-arg as fall-backs (for very old SPs).
|
||||
call_shapes.append(("login(False)", (False,)))
|
||||
call_shapes.append(("login(True)", (True,)))
|
||||
call_shapes.append(("login()", ()))
|
||||
|
|
|
|||
|
|
@ -46,12 +46,51 @@ try:
|
|||
parent_name = getattr(parent_object, 'get_name', lambda: str(parent_object))()
|
||||
print("DEBUG: Using parent object: %s" % parent_name)
|
||||
|
||||
# Create the folder
|
||||
if not hasattr(parent_object, 'create_folder'):
|
||||
raise TypeError("Parent object '%s' of type %s does not support create_folder." % (parent_name, type(parent_object).__name__))
|
||||
# Create the folder. The factory shape changed across SPs:
|
||||
# - Older: parent.create_folder(name='X') -- legacy API
|
||||
# - SP21+: parent.create_folder(name='X') -- still preferred
|
||||
# OR parent.create_object(typeUuid=<uuid>, name='X')
|
||||
# OR parent.add(script_engine.types.IecFolder, name='X')
|
||||
# Some Application objects in SP21+ don't expose create_folder at all;
|
||||
# fall through to the alternate factories so the tool works against
|
||||
# both old and new project shapes.
|
||||
new_folder = None
|
||||
if hasattr(parent_object, 'create_folder'):
|
||||
try:
|
||||
print("DEBUG: Calling parent.create_folder(name='%s')" % FOLDER_NAME)
|
||||
new_folder = parent_object.create_folder(name=FOLDER_NAME)
|
||||
except Exception as e:
|
||||
print("WARN: parent.create_folder() raised: %s -- trying alternate factories." % e)
|
||||
new_folder = None
|
||||
|
||||
print("DEBUG: Calling create_folder: Name='%s'" % FOLDER_NAME)
|
||||
new_folder = parent_object.create_folder(name=FOLDER_NAME)
|
||||
if new_folder is None and hasattr(parent_object, 'create_object'):
|
||||
# CODESYS folder type UUID. The canonical "generic IEC folder" type
|
||||
# ID has been stable across SP19-SP22; verified via the SP22 stub
|
||||
# Stubs/scriptengine/types.pyi and the helpme-codesys.com docs for
|
||||
# ScriptObject.create_object. If a future SP rotates this UUID, the
|
||||
# types.IecFolder branch below picks up the canonical reference
|
||||
# automatically.
|
||||
FOLDER_TYPE_UUID = '85d1215e-6520-4983-9a55-2d39d1f24cb4'
|
||||
try:
|
||||
print("DEBUG: parent.create_folder() unavailable. Trying parent.create_object(typeUuid=%s, name='%s')" % (FOLDER_TYPE_UUID, FOLDER_NAME))
|
||||
new_folder = parent_object.create_object(typeUuid=FOLDER_TYPE_UUID, name=FOLDER_NAME)
|
||||
except Exception as e:
|
||||
print("WARN: parent.create_object(typeUuid=%s) raised: %s" % (FOLDER_TYPE_UUID, e))
|
||||
new_folder = None
|
||||
|
||||
if new_folder is None and hasattr(script_engine, 'types') and hasattr(script_engine.types, 'IecFolder') and hasattr(parent_object, 'add'):
|
||||
try:
|
||||
print("DEBUG: Trying parent.add(script_engine.types.IecFolder, name='%s')" % FOLDER_NAME)
|
||||
new_folder = parent_object.add(script_engine.types.IecFolder, name=FOLDER_NAME)
|
||||
except Exception as e:
|
||||
print("WARN: parent.add(types.IecFolder) raised: %s" % e)
|
||||
new_folder = None
|
||||
|
||||
if new_folder is None:
|
||||
raise TypeError(
|
||||
"Parent object '%s' of type %s does not support any known folder-creation factory: "
|
||||
"tried create_folder, create_object(typeUuid=...), and add(script_engine.types.IecFolder)." % (
|
||||
parent_name, type(parent_object).__name__))
|
||||
|
||||
if new_folder:
|
||||
new_folder_name = getattr(new_folder, 'get_name', lambda: FOLDER_NAME)()
|
||||
|
|
|
|||
|
|
@ -60,15 +60,37 @@ def ensure_project_open(target_project_path):
|
|||
# traceback.print_exc() # Optional: Print stack trace
|
||||
primary_project = None # Force reopen by falling through
|
||||
else:
|
||||
# A *different* project is primary
|
||||
print("DEBUG: Primary project is '%s', not the target '%s'." % (current_project_path, normalized_target_path))
|
||||
# Consider closing the wrong project if causing issues, but for now, just open target
|
||||
# try:
|
||||
# print("DEBUG: Closing incorrect primary project '%s'..." % current_project_path)
|
||||
# primary_project.close() # Be careful with unsaved changes
|
||||
# except Exception as close_err:
|
||||
# print("WARN: Failed to close incorrect primary project: %s" % close_err)
|
||||
primary_project = None # Force open target project
|
||||
# A *different* project is primary -- close it cleanly
|
||||
# before opening the target. Without this, projects.open
|
||||
# against a different already-primary project tends to
|
||||
# fail (file lock contention) or pop a "project in use"
|
||||
# modal that freezes the IDE thread, breaking every
|
||||
# subsequent script call with 60s timeouts.
|
||||
#
|
||||
# Save first so unsaved changes aren't lost. If save
|
||||
# raises (e.g. transient lock), fall through to close
|
||||
# anyway -- losing in-flight edits is worse than getting
|
||||
# stuck in a half-switched state forever.
|
||||
print("DEBUG: Primary project is '%s', not the target '%s'. Closing it before opening target..." % (
|
||||
current_project_path, normalized_target_path))
|
||||
try:
|
||||
if hasattr(primary_project, 'save'):
|
||||
try:
|
||||
primary_project.save()
|
||||
print("DEBUG: Saved prior primary before close.")
|
||||
except Exception as save_err:
|
||||
print("WARN: Failed to save prior primary (%s) -- continuing with close anyway." % save_err)
|
||||
primary_project.close()
|
||||
print("DEBUG: Closed prior primary '%s'." % current_project_path)
|
||||
# Pump CODESYS so the close transition completes
|
||||
# before we ask it to open something else.
|
||||
try:
|
||||
script_engine.system.delay(500)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as close_err:
|
||||
print("WARN: Failed to close prior primary project: %s -- attempting open anyway." % close_err)
|
||||
primary_project = None # Force open target project
|
||||
|
||||
except Exception as path_err:
|
||||
# Failed even to get the path of the supposed primary project
|
||||
|
|
|
|||
|
|
@ -1,5 +1,65 @@
|
|||
import sys, scriptengine as script_engine, os, traceback, json
|
||||
|
||||
|
||||
def _coerce_int(v):
|
||||
"""IronPython 2.7's json.dumps cannot serialize System.Int64-backed
|
||||
`long` values, which is what CODESYS's compile-message objects expose
|
||||
as line_number / position. Coerce to native int."""
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_str(v):
|
||||
"""Force str() on CLR-typed fields (System.Uri etc.)."""
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
return str(v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _build_message_entry(msg):
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
try:
|
||||
sev = str(msg.severity).lower()
|
||||
except Exception:
|
||||
sev = 'unknown'
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
text = None
|
||||
for attr in ('text', 'message'):
|
||||
if hasattr(msg, attr):
|
||||
text = _coerce_str(getattr(msg, attr))
|
||||
if text is not None:
|
||||
break
|
||||
if text is None:
|
||||
text = _coerce_str(msg)
|
||||
entry['text'] = text
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = _coerce_str(msg.object_name)
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = _coerce_str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = _coerce_int(msg.line_number)
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = _coerce_int(msg.position)
|
||||
return entry
|
||||
|
||||
|
||||
try:
|
||||
print("DEBUG: get_compile_messages script: Project='%s'" % PROJECT_FILE_PATH)
|
||||
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
||||
|
|
@ -41,29 +101,7 @@ try:
|
|||
if msg_objects is not None:
|
||||
messages_found = True
|
||||
for msg in msg_objects:
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
sev = str(msg.severity).lower()
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg)))
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = msg.object_name
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = msg.line_number
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = msg.position
|
||||
messages.append(entry)
|
||||
messages.append(_build_message_entry(msg))
|
||||
print("DEBUG: Got %d messages from app.get_message_objects()" % len(messages))
|
||||
except Exception as e:
|
||||
print("DEBUG: app.get_message_objects() failed: %s" % e)
|
||||
|
|
@ -77,29 +115,7 @@ try:
|
|||
if msg_objects is not None:
|
||||
messages_found = True
|
||||
for msg in msg_objects:
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
sev = str(msg.severity).lower()
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg)))
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = msg.object_name
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = msg.line_number
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = msg.position
|
||||
messages.append(entry)
|
||||
messages.append(_build_message_entry(msg))
|
||||
print("DEBUG: Got %d messages from system.get_message_objects()" % len(messages))
|
||||
except Exception as e:
|
||||
print("DEBUG: system.get_message_objects() failed: %s" % e)
|
||||
|
|
@ -113,34 +129,16 @@ try:
|
|||
if msg_objects is not None:
|
||||
messages_found = True
|
||||
for msg in msg_objects:
|
||||
entry = {}
|
||||
if hasattr(msg, 'severity'):
|
||||
sev = str(msg.severity).lower()
|
||||
if 'error' in sev:
|
||||
entry['severity'] = 'error'
|
||||
elif 'warning' in sev:
|
||||
entry['severity'] = 'warning'
|
||||
elif 'info' in sev:
|
||||
entry['severity'] = 'info'
|
||||
else:
|
||||
entry['severity'] = sev
|
||||
else:
|
||||
entry['severity'] = 'unknown'
|
||||
entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg)))
|
||||
if hasattr(msg, 'object_name'):
|
||||
entry['object'] = msg.object_name
|
||||
elif hasattr(msg, 'source'):
|
||||
entry['object'] = str(msg.source)
|
||||
if hasattr(msg, 'line_number'):
|
||||
entry['line'] = msg.line_number
|
||||
elif hasattr(msg, 'position'):
|
||||
entry['line'] = msg.position
|
||||
messages.append(entry)
|
||||
messages.append(_build_message_entry(msg))
|
||||
print("DEBUG: Got %d messages from system.get_messages()" % len(messages))
|
||||
except Exception as e:
|
||||
print("DEBUG: system.get_messages() failed: %s" % e)
|
||||
|
||||
messages_json = json.dumps(messages)
|
||||
try:
|
||||
messages_json = json.dumps(messages)
|
||||
except TypeError as je:
|
||||
print("WARN: json.dumps raised %s -- retrying with default=str fallback" % je)
|
||||
messages_json = json.dumps(messages, default=lambda o: str(o))
|
||||
print("### COMPILE_MESSAGES_START ###")
|
||||
print(messages_json)
|
||||
print("### COMPILE_MESSAGES_END ###")
|
||||
|
|
|
|||
394
tests/bench-results.json
Normal file
394
tests/bench-results.json
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
{
|
||||
"timestamp": "2026-04-26T14:52:18.324Z",
|
||||
"config": {
|
||||
"project": "\\\\files\\karstein.kvistad\\Documents\\Claude\\PLC\\MCPTest2\\MCPTest2.project",
|
||||
"codesys": "C:\\Program Files\\CODESYS 3.5.22.10\\CODESYS\\Common\\CODESYS.exe",
|
||||
"profile": "CODESYS V3.5 SP22 Patch 1",
|
||||
"iterations": 2
|
||||
},
|
||||
"workDir": "C:\\Users\\KARSTE~1.KVI\\AppData\\Local\\Temp\\codesys-mcp-bench-HBzAUg",
|
||||
"results": {
|
||||
"headless": [
|
||||
{
|
||||
"id": "open_project",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 22012.8208,
|
||||
"max": 58068.5602,
|
||||
"mean": 40040.6905,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 58068.5602,
|
||||
"ok": true,
|
||||
"outputBytes": 1131,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 22012.8208,
|
||||
"ok": true,
|
||||
"outputBytes": 1131,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mirror_export",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 23526.9205,
|
||||
"max": 23919.1101,
|
||||
"mean": 23723.0153,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 23919.1101,
|
||||
"ok": true,
|
||||
"outputBytes": 1516,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 23526.9205,
|
||||
"ok": true,
|
||||
"outputBytes": 1516,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "list_project_libraries",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 23288.2306,
|
||||
"max": 23356.7295,
|
||||
"mean": 23322.48005,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 23356.7295,
|
||||
"ok": true,
|
||||
"outputBytes": 3981,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 23288.2306,
|
||||
"ok": true,
|
||||
"outputBytes": 3979,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "get_all_pou_code",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 23359.3891,
|
||||
"max": 23392.8083,
|
||||
"mean": 23376.098700000002,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 23392.8083,
|
||||
"ok": true,
|
||||
"outputBytes": 10895,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 23359.3891,
|
||||
"ok": true,
|
||||
"outputBytes": 10895,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "save_project",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 23089.9258,
|
||||
"max": 23551.2732,
|
||||
"mean": 23320.5995,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 23551.2732,
|
||||
"ok": true,
|
||||
"outputBytes": 1312,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 23089.9258,
|
||||
"ok": true,
|
||||
"outputBytes": 1312,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "create_pou (FB)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 23902.8924,
|
||||
"max": 23902.8924,
|
||||
"mean": 23902.8924,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 23902.8924,
|
||||
"ok": true,
|
||||
"outputBytes": 3475,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "set_pou_code (decl+impl)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"allFailed": true
|
||||
},
|
||||
{
|
||||
"id": "delete_object (FB_Bench)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 27419.9285,
|
||||
"max": 27419.9285,
|
||||
"mean": 27419.9285,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 27419.9285,
|
||||
"ok": true,
|
||||
"outputBytes": 2872,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bump_project_version (build)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 30678.0223,
|
||||
"max": 30678.0223,
|
||||
"mean": 30678.0223,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 30678.0223,
|
||||
"ok": true,
|
||||
"outputBytes": 1746,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bump_project_version (build #2)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 37769.477,
|
||||
"max": 37769.477,
|
||||
"mean": 37769.477,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 37769.477,
|
||||
"ok": true,
|
||||
"outputBytes": 1746,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"persistent": [
|
||||
{
|
||||
"id": "open_project",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 740.7234,
|
||||
"max": 14660.0126,
|
||||
"mean": 7700.368,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 14660.0126,
|
||||
"ok": true,
|
||||
"outputBytes": 1119,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 740.7234,
|
||||
"ok": true,
|
||||
"outputBytes": 770,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mirror_export",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 1545.8997,
|
||||
"max": 1548.8912,
|
||||
"mean": 1547.39545,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 1548.8912,
|
||||
"ok": true,
|
||||
"outputBytes": 1155,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 1545.8997,
|
||||
"ok": true,
|
||||
"outputBytes": 1155,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "list_project_libraries",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 1545.7068,
|
||||
"max": 1583.9429,
|
||||
"mean": 1564.82485,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 1545.7068,
|
||||
"ok": true,
|
||||
"outputBytes": 3612,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 1583.9429,
|
||||
"ok": true,
|
||||
"outputBytes": 3612,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "get_all_pou_code",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 1570.3402,
|
||||
"max": 1642.7852,
|
||||
"mean": 1606.5627,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 1570.3402,
|
||||
"ok": true,
|
||||
"outputBytes": 10530,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 1642.7852,
|
||||
"ok": true,
|
||||
"outputBytes": 10530,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "save_project",
|
||||
"kind": "read",
|
||||
"n": 2,
|
||||
"min": 1538.7077,
|
||||
"max": 2651.2456,
|
||||
"mean": 2094.97665,
|
||||
"okCount": 2,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 2651.2456,
|
||||
"ok": true,
|
||||
"outputBytes": 949,
|
||||
"errorBytes": 0
|
||||
},
|
||||
{
|
||||
"ms": 1538.7077,
|
||||
"ok": true,
|
||||
"outputBytes": 949,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "create_pou (FB)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 1539.7142,
|
||||
"max": 1539.7142,
|
||||
"mean": 1539.7142,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 1539.7142,
|
||||
"ok": true,
|
||||
"outputBytes": 3088,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "set_pou_code (decl+impl)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"allFailed": true
|
||||
},
|
||||
{
|
||||
"id": "delete_object (FB_Bench)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 1543.7014,
|
||||
"max": 1543.7014,
|
||||
"mean": 1543.7014,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 1543.7014,
|
||||
"ok": true,
|
||||
"outputBytes": 2484,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bump_project_version (build)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 1540.0761,
|
||||
"max": 1540.0761,
|
||||
"mean": 1540.0761,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 1540.0761,
|
||||
"ok": true,
|
||||
"outputBytes": 1379,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bump_project_version (build #2)",
|
||||
"kind": "write",
|
||||
"n": 1,
|
||||
"min": 1555.9909,
|
||||
"max": 1555.9909,
|
||||
"mean": 1555.9909,
|
||||
"okCount": 1,
|
||||
"runs": [
|
||||
{
|
||||
"ms": 1555.9909,
|
||||
"ok": true,
|
||||
"outputBytes": 1379,
|
||||
"errorBytes": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue