From 2987578552acf3bb496075ed18693519d2ec3cfb Mon Sep 17 00:00:00 2001 From: Brandon Fergerson Date: Tue, 4 Oct 2022 18:02:14 +0400 Subject: [PATCH] fix: live variable decoding error on Python dicts --- marker/py-marker/build.gradle.kts | 1 + .../py/presentation/PythonVariableNode.kt | 57 +++++- .../py/presentation/LiveVariableParseTest.kt | 39 ++++ .../resources/breakpointHit/dictParse.json | 167 ++++++++++++++++++ 4 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 marker/py-marker/src/test/kotlin/spp/jetbrains/marker/py/presentation/LiveVariableParseTest.kt create mode 100644 marker/py-marker/src/test/resources/breakpointHit/dictParse.json diff --git a/marker/py-marker/build.gradle.kts b/marker/py-marker/build.gradle.kts index 90e4be5db..36d758f16 100644 --- a/marker/py-marker/build.gradle.kts +++ b/marker/py-marker/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { compileOnly("org.jetbrains:annotations:23.0.0") compileOnly("io.vertx:vertx-core:$vertxVersion") + testImplementation("io.vertx:vertx-core:$vertxVersion") testImplementation(projectDependency(":common")) testImplementation("org.junit.jupiter:junit-jupiter:$jupiterVersion") } diff --git a/marker/py-marker/src/main/kotlin/spp/jetbrains/marker/py/presentation/PythonVariableNode.kt b/marker/py-marker/src/main/kotlin/spp/jetbrains/marker/py/presentation/PythonVariableNode.kt index ee7af8067..7e4770583 100644 --- a/marker/py-marker/src/main/kotlin/spp/jetbrains/marker/py/presentation/PythonVariableNode.kt +++ b/marker/py-marker/src/main/kotlin/spp/jetbrains/marker/py/presentation/PythonVariableNode.kt @@ -45,10 +45,7 @@ class PythonVariableNode( override fun getChildren(): Array { if (variable.liveClazz == "") { - val dict = JsonObject( - (variable.value as String).replace("'", "\"") - .replace(": True", ": true").replace(": False", ": false") - ) + val dict = JsonObject(parseDict(variable.value as String)) val children = mutableListOf() dict.map.forEach { children.add(PythonVariableNode(LiveVariable("'" + it.key + "'", it.value))) @@ -101,4 +98,56 @@ class PythonVariableNode( } } } + + companion object { + fun parseDict(value: String): Map { + var dictVar = value + + //remove outer braces + if (dictVar.startsWith("{")) { + dictVar = dictVar.substring(1) + } + if (dictVar.endsWith("}")) { + dictVar = dictVar.substring(0, dictVar.length - 1) + } + + //split by , but not inside quotes + val tokens = dictVar.split(Regex(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) + + //each token should contain ":", for those that don't we need to merge them with the previous token + val mergedTokens = mutableListOf() + for (i in tokens.indices) { + if (tokens[i].contains(":")) { + mergedTokens.add(tokens[i]) + } else { + mergedTokens[mergedTokens.size - 1] += "," + tokens[i] + } + } + + //now we can split each token into key and value + val dict = mutableMapOf() + for (token in mergedTokens) { + var key = token.substringBefore(":").trim() + var value = token.substringAfter(":").trim() + + //remove quotes + if (key.startsWith("\"")) { + key = key.substring(1) + } + if (key.endsWith("\"")) { + key = key.substring(0, key.length - 1) + } + if (value.startsWith("\"")) { + value = value.substring(1) + } + if (value.endsWith("\"")) { + value = value.substring(0, value.length - 1) + } + + dict[key] = value + } + + return dict + } + } } diff --git a/marker/py-marker/src/test/kotlin/spp/jetbrains/marker/py/presentation/LiveVariableParseTest.kt b/marker/py-marker/src/test/kotlin/spp/jetbrains/marker/py/presentation/LiveVariableParseTest.kt new file mode 100644 index 000000000..824748ba8 --- /dev/null +++ b/marker/py-marker/src/test/kotlin/spp/jetbrains/marker/py/presentation/LiveVariableParseTest.kt @@ -0,0 +1,39 @@ +/* + * Source++, the open-source live coding platform. + * Copyright (C) 2022 CodeBrig, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package spp.jetbrains.marker.py.presentation + +import com.google.common.io.Resources +import io.vertx.core.json.JsonObject +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import spp.protocol.instrument.event.LiveBreakpointHit + +class LiveVariableParseTest { + + @Test + fun testDictParse() { + val bpHitJson = Resources.getResource("breakpointHit/dictParse.json").readText() + val bpHit = LiveBreakpointHit(JsonObject(bpHitJson)) + val builtInsVar = bpHit.stackTrace.elements.first().variables.find { it.name == "__builtins__" } + assertNotNull(builtInsVar) + + val parsedDict = PythonVariableNode.parseDict(builtInsVar!!.value as String) + assertNotNull(parsedDict) + assertEquals(152, parsedDict.size) + } +} diff --git a/marker/py-marker/src/test/resources/breakpointHit/dictParse.json b/marker/py-marker/src/test/resources/breakpointHit/dictParse.json new file mode 100644 index 000000000..3d9c47d22 --- /dev/null +++ b/marker/py-marker/src/test/resources/breakpointHit/dictParse.json @@ -0,0 +1,167 @@ +{ + "breakpointId": "914439a3-863b-40b5-8911-2519ba697413", + "traceId": "cdb70afe43e411ed94590242c0a8900b", + "occurredAt": "2022-10-04T13:02:30.128Z", + "serviceInstance": "9f7f48ec43dd11ed94410242c0a8900b", + "service": "demo-python", + "stackTrace": { + "exceptionType": "n/a", + "message": "n/a", + "elements": [ + { + "method": "simple_breakpoint", + "source": "/demo-python/src/command/AddBreakpoint.py:30", + "column": null, + "variables": [ + { + "name": "__name__", + "value": "command.AddBreakpoint", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259926832512", + "presentation": null + }, + { + "name": "__doc__", + "value": "None", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259935552032", + "presentation": null + }, + { + "name": "__package__", + "value": "command", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259851025840", + "presentation": null + }, + { + "name": "__loader__", + "value": "<_frozen_importlib_external.SourceFileLoader object at 0x7f90ca97e340>", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259850969920", + "presentation": null + }, + { + "name": "__spec__", + "value": "ModuleSpec(name='command.AddBreakpoint', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f90ca97e340>, origin='/demo-python/src/command/AddBreakpoint.py')", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259850971552", + "presentation": null + }, + { + "name": "__file__", + "value": "/demo-python/src/command/AddBreakpoint.py", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259850966096", + "presentation": null + }, + { + "name": "__cached__", + "value": "/demo-python/src/command/__pycache__/AddBreakpoint.cpython-38.pyc", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259927272240", + "presentation": null + }, + { + "name": "__builtins__", + "value": "{'__name__': 'builtins', '__doc__': \"Built-in functions, exceptions, and other objects.\\n\\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.\", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2022 Python Software Foundation.\nAll Rights Reserved.\n\nCopyright (c) 2000 BeOpen.com.\nAll Rights Reserved.\n\nCopyright (c) 1995-2001 Corporation for National Research Initiatives.\nAll Rights Reserved.\n\nCopyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.\nAll Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands\n for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259928018176", + "presentation": null + }, + { + "name": "AddBreakpoint", + "value": "", + "lineNumber": -1, + "scope": "GLOBAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140258331079168", + "presentation": null + }, + { + "name": "random", + "value": "", + "lineNumber": -1, + "scope": "LOCAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259914813504", + "presentation": null + }, + { + "name": "random_number", + "value": "49.90006897806545", + "lineNumber": -1, + "scope": "LOCAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259851605296", + "presentation": null + }, + { + "name": "is_even", + "value": "False", + "lineNumber": -1, + "scope": "LOCAL_VARIABLE", + "liveClazz": "", + "liveIdentity": "140259935470944", + "presentation": null + } + ], + "sourceCode": "print(f\"{random_number} and is \" + (\"even\" if is_even else \"odd\"))" + }, + { + "method": "trigger_add_breakpoint", + "source": "src/main.py:10", + "column": null, + "variables": [], + "sourceCode": "command.AddBreakpoint.AddBreakpoint.simple_breakpoint()" + }, + { + "method": "execute_demos", + "source": "src/main.py:15", + "column": null, + "variables": [], + "sourceCode": "trigger_add_breakpoint()" + }, + { + "method": "run", + "source": "/usr/local/lib/python3.8/threading.py:870", + "column": null, + "variables": [], + "sourceCode": "self._target(*self._args, **self._kwargs)" + }, + { + "method": "_bootstrap_inner", + "source": "/usr/local/lib/python3.8/threading.py:932", + "column": null, + "variables": [], + "sourceCode": "self.run()" + }, + { + "method": "_bootstrap", + "source": "/usr/local/lib/python3.8/threading.py:890", + "column": null, + "variables": [], + "sourceCode": "self._bootstrap_inner()" + } + ], + "causedBy": null, + "language": "PYTHON" + }, + "eventType": "BREAKPOINT_HIT" +} \ No newline at end of file