From d127200a02c0bf84d15a0b34fdc5c558204c527f Mon Sep 17 00:00:00 2001
From: Lana W <69004535+lana-w@users.noreply.github.com>
Date: Mon, 19 Aug 2024 22:04:09 -0400
Subject: [PATCH] Add MemoryViz save feature to debug.snapshot function (#1075)
---
.github/workflows/test.yml | 4 +
.pre-commit-config.yaml | 3 +-
CHANGELOG.md | 1 +
python_ta/debug/snapshot.py | 46 +++++++-
tests/test_debug/snapshot_save_file.py | 23 ++++
tests/test_debug/snapshot_save_stdout.py | 12 +++
.../snapshot_testing_snapshots_expected.svg | 33 ++++++
...shot_testing_snapshots_expected_stdout.svg | 33 ++++++
tests/test_debug/test_snapshot.py | 101 ++++++++++++++++++
9 files changed, 253 insertions(+), 3 deletions(-)
create mode 100644 tests/test_debug/snapshot_save_file.py
create mode 100644 tests/test_debug/snapshot_save_stdout.py
create mode 100644 tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected.svg
create mode 100644 tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected_stdout.svg
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d5080055..b5fcff15 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,6 +20,10 @@ jobs:
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python-version }}
+ - name: Set up Node.js 18
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
- name: Set up graphviz
run: |
sudo apt-get install -y graphviz
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 85940896..9d7d3085 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -21,7 +21,8 @@ repos:
exclude: |
(?x)^(
examples|
- tests/fixtures/sample_dir
+ tests/fixtures/sample_dir|
+ tests/test_debug/snapshot_testing_snapshots
)
ci:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 475bf0e6..2d37c344 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Stored valid Python function preconditions in initial edge to function code in generated function control flow graphs.
- Report warning when control flow graph creation encounters a syntax error related to control flow
- Added autoformat option that runs black formatting tool to python_ta.check_all()
+- Extended the `snapshot` function to optionally generate a svg of the snapshot using MemoryViz when save parameter is true.
### 💫 New checkers
diff --git a/python_ta/debug/snapshot.py b/python_ta/debug/snapshot.py
index d5a46e82..129b7c70 100644
--- a/python_ta/debug/snapshot.py
+++ b/python_ta/debug/snapshot.py
@@ -6,8 +6,15 @@
from __future__ import annotations
import inspect
+import json
+import logging
+import shutil
+import subprocess
+import sys
from types import FrameType
-from typing import Any
+from typing import Any, Optional
+
+from packaging.version import Version, parse
def get_filtered_global_variables(frame: FrameType) -> dict:
@@ -29,11 +36,21 @@ def get_filtered_global_variables(frame: FrameType) -> dict:
return {"__main__": true_global_vars}
-def snapshot():
+def snapshot(
+ save: bool = False,
+ memory_viz_args: Optional[list[str]] = None,
+ memory_viz_version: str = "latest",
+):
"""Capture a snapshot of local variables from the current and outer stack frames
where the 'snapshot' function is called. Returns a list of dictionaries,
each mapping function names to their respective local variables.
Excludes the global module context.
+
+ When save is True, a MemoryViz-created svg is produced.
+ memory_viz_args can be used to pass in options to the MemoryViz CLI.
+ For details on the MemoryViz CLI, see https://www.cs.toronto.edu/~david/memory-viz/docs/cli.
+ memory_viz_version can be used to dictate version, with a default of the latest version.
+ Note that this function is compatible only with MemoryViz version 0.3.1 and above.
"""
variables = []
frame = inspect.currentframe().f_back
@@ -47,6 +64,31 @@ def snapshot():
frame = frame.f_back
+ if save:
+ json_compatible_vars = snapshot_to_json(variables)
+
+ # Set up command
+ command = ["npx", "memory-viz"]
+ if memory_viz_args:
+ command.extend(memory_viz_args)
+
+ # Ensure valid memory_viz version
+ if memory_viz_version != "latest" and parse(memory_viz_version) < Version("0.3.1"):
+ logging.warning("PythonTA only supports MemoryViz versions 0.3.1 and later.")
+
+ # Create a child to call the MemoryViz CLI
+ npx_path = shutil.which("npx")
+ subprocess.run(
+ command,
+ input=json.dumps(json_compatible_vars),
+ executable=npx_path,
+ stdout=sys.stdout,
+ stderr=sys.stderr,
+ encoding="utf-8",
+ text=True,
+ check=True,
+ )
+
return variables
diff --git a/tests/test_debug/snapshot_save_file.py b/tests/test_debug/snapshot_save_file.py
new file mode 100644
index 00000000..da62a17a
--- /dev/null
+++ b/tests/test_debug/snapshot_save_file.py
@@ -0,0 +1,23 @@
+"""
+This Python module is designed for testing the snapshot function's ability to, when save is True,
+create a snapshot svg at the specified file path.
+
+This module is intended exclusively for testing purposes and should not be used for any other purpose.
+"""
+
+import os
+import sys
+
+from python_ta.debug.snapshot import snapshot
+
+test_var1a = "David is cool!"
+test_var2a = "Students Developing Software"
+snapshot(
+ True,
+ [
+ "--output=" + os.path.abspath(sys.argv[1]),
+ "--roughjs-config",
+ "seed=12345",
+ ],
+ "0.3.1",
+)
diff --git a/tests/test_debug/snapshot_save_stdout.py b/tests/test_debug/snapshot_save_stdout.py
new file mode 100644
index 00000000..4b0abf8f
--- /dev/null
+++ b/tests/test_debug/snapshot_save_stdout.py
@@ -0,0 +1,12 @@
+"""
+This Python module is designed for testing the snapshot function's ability to, when save is True,
+return the snapshot svg to stdout.
+
+This module is intended exclusively for testing purposes and should not be used for any other purpose.
+"""
+
+from python_ta.debug.snapshot import snapshot
+
+test_var1a = "David is cool!"
+test_var2a = "Students Developing Software"
+snapshot(True, ["--roughjs-config", "seed=12345"], "0.3.1")
diff --git a/tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected.svg b/tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected.svg
new file mode 100644
index 00000000..64b12605
--- /dev/null
+++ b/tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected.svg
@@ -0,0 +1,33 @@
+
\ No newline at end of file
diff --git a/tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected_stdout.svg b/tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected_stdout.svg
new file mode 100644
index 00000000..705b40fe
--- /dev/null
+++ b/tests/test_debug/snapshot_testing_snapshots/snapshot_testing_snapshots_expected_stdout.svg
@@ -0,0 +1,33 @@
+
diff --git a/tests/test_debug/test_snapshot.py b/tests/test_debug/test_snapshot.py
index 1b54953d..1ae32753 100644
--- a/tests/test_debug/test_snapshot.py
+++ b/tests/test_debug/test_snapshot.py
@@ -541,3 +541,104 @@ def test_snapshot_to_json_one_class():
]
assert json_data == expected_output
+
+
+def test_snapshot_no_save_file():
+ """
+ Tests that snapshot's save feature is not triggered when save = False
+ and ensures no svg files are created.
+ """
+
+ file_path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "snapshot_testing_snapshots"
+ )
+
+ snapshot(
+ False,
+ [
+ "--output=" + file_path,
+ "--roughjs-config",
+ "seed=12345",
+ ],
+ )
+
+ assert not os.path.exists(os.path.join(file_path, "snapshot_testing_snapshots.svg"))
+
+
+def test_snapshot_no_save_stdout(capsys):
+ """
+ Tests that snapshot's save feature is not triggered when save = False
+ """
+ snapshot(False)
+ captured = capsys.readouterr()
+ assert captured.out == ""
+
+
+def test_snapshot_save_create_svg(tmp_path):
+ """
+ Test that snapshot's save feature creates a MemoryViz svg of the stack frame as a file to the specified path.
+ """
+
+ # Calls snapshot in separate file
+ current_directory = os.path.dirname(os.path.abspath(__file__))
+ snapshot_save_path = os.path.join(current_directory, "snapshot_save_file.py")
+ result = subprocess.run(
+ [sys.executable, snapshot_save_path, os.path.abspath(tmp_path)],
+ capture_output=True,
+ text=True,
+ check=True,
+ encoding="utf-8",
+ )
+
+ # Read generated file
+ with open(
+ os.path.join(tmp_path, "test_snapshot_save_create_svg0.svg"),
+ mode="r",
+ encoding="utf-8",
+ ) as gen_svg:
+ generated_svg = gen_svg.read()
+
+ # Read expected file
+ with open(
+ os.path.join(
+ current_directory,
+ "snapshot_testing_snapshots",
+ "snapshot_testing_snapshots_expected.svg",
+ ),
+ mode="r",
+ encoding="utf-8",
+ ) as expected_svg_file:
+ expected_svg = expected_svg_file.read()
+
+ assert generated_svg == expected_svg
+
+
+def test_snapshot_save_stdout():
+ """
+ Test that snapshot's save feature successfully returns a MemoryViz svg of the stack frame to stdout.
+ """
+
+ # Calls snapshot in separate file
+ current_directory = os.path.dirname(os.path.abspath(__file__))
+ snapshot_save_path = os.path.join(current_directory, "snapshot_save_stdout.py")
+ result = subprocess.run(
+ [sys.executable, snapshot_save_path],
+ capture_output=True,
+ encoding="utf-8",
+ text=True,
+ check=True,
+ )
+
+ # Read expected svg file
+ with open(
+ os.path.join(
+ current_directory,
+ "snapshot_testing_snapshots",
+ "snapshot_testing_snapshots_expected_stdout.svg",
+ ),
+ mode="r",
+ encoding="utf-8",
+ ) as expected_svg_file:
+ expected_svg = expected_svg_file.read()
+
+ assert result.stdout == expected_svg