Skip to content

Commit 2da155f

Browse files
committed
stats: Support --force and --output for JSON mode
Signed-off-by: Matt Wozniski <mwozniski@bloomberg.net>
1 parent c7345fd commit 2da155f

4 files changed

Lines changed: 175 additions & 12 deletions

File tree

src/memray/commands/stats.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import os
33
from pathlib import Path
4+
from typing import Optional
45

56
from memray._errors import MemrayCommandError
67
from memray._memray import compute_statistics
@@ -39,6 +40,19 @@ def valid_positive_int(value: str) -> int:
3940
action="store_true",
4041
default=False,
4142
)
43+
parser.add_argument(
44+
"-o",
45+
"--output",
46+
help="Output file name for JSON output",
47+
default=None,
48+
)
49+
parser.add_argument(
50+
"-f",
51+
"--force",
52+
help="If the JSON output file already exists, overwrite it",
53+
action="store_true",
54+
default=False,
55+
)
4256

4357
def run(self, args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
4458
result_path = Path(args.results)
@@ -56,5 +70,24 @@ def run(self, args: argparse.Namespace, parser: argparse.ArgumentParser) -> None
5670
exit_code=1,
5771
)
5872

73+
json_output_file: Optional[Path] = None
74+
if args.json:
75+
if args.output:
76+
json_output_file = Path(args.output)
77+
else:
78+
filename = str(result_path.name) + ".json"
79+
if filename.startswith("memray-"):
80+
filename = filename[len("memray-") :]
81+
filename = "memray-stats-" + filename
82+
json_output_file = result_path.with_name(filename)
83+
84+
if not args.force and json_output_file.exists():
85+
raise MemrayCommandError(
86+
f"File already exists, will not overwrite: {json_output_file}",
87+
exit_code=1,
88+
)
89+
5990
reporter = StatsReporter(stats, args.num_largest)
60-
reporter.render(to_json=args.json, result_path=result_path)
91+
reporter.render(json_output_file=json_output_file)
92+
if json_output_file is not None:
93+
print(f"Wrote {json_output_file}")

src/memray/reporters/stats.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import datetime
22
import json
33
import math
4-
import re
54
from collections import Counter
65
from dataclasses import asdict
76
from pathlib import Path
87
from typing import Any
98
from typing import Dict
109
from typing import Iterator
1110
from typing import List
11+
from typing import Optional
1212
from typing import Tuple
1313

1414
import rich
@@ -110,17 +110,13 @@ def __init__(self, stats: Stats, num_largest: int):
110110
raise ValueError(f"Invalid input num_largest={num_largest}, should be >=1")
111111
self.num_largest = num_largest
112112

113-
def render(self, to_json: bool = False, result_path: Path = Path(".")) -> None:
113+
def render(self, json_output_file: Optional[Path] = None) -> None:
114114
histogram_params = dict(
115115
num_bins=10,
116116
histogram_scale_factor=25,
117117
)
118-
if to_json:
119-
# appends suffix (works better with --follow-fork)
120-
out_name = result_path.with_suffix(result_path.suffix + ".json").name
121-
out_name = re.sub("^memray-", "", out_name)
122-
out_path = result_path.parent / f"memray-stats-{out_name}"
123-
self._render_to_json(histogram_params, out_path)
118+
if json_output_file:
119+
self._render_to_json(histogram_params, json_output_file)
124120
else:
125121
self._render_to_terminal(histogram_params)
126122

tests/integration/test_main.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import json
23
import os
34
import platform
45
import pty
@@ -863,6 +864,138 @@ def test_report_generated(self, tmp_path, simple_test_file):
863864
# THEN
864865
assert "VALLOC" in output
865866

867+
def test_json_generated(self, tmp_path, simple_test_file):
868+
# GIVEN
869+
results_file, _ = generate_sample_results(tmp_path, simple_test_file)
870+
json_file = tmp_path / "memray-stats-result.bin.json"
871+
872+
# WHEN
873+
subprocess.check_output(
874+
[
875+
sys.executable,
876+
"-m",
877+
"memray",
878+
"stats",
879+
"--json",
880+
str(results_file),
881+
],
882+
cwd=str(tmp_path),
883+
text=True,
884+
)
885+
886+
# THEN
887+
assert json_file.exists()
888+
assert isinstance(json.loads(json_file.read_text()), dict)
889+
890+
def test_json_generated_to_pretty_file_name(self, tmp_path, simple_test_file):
891+
# GIVEN
892+
orig_results_file, _ = generate_sample_results(tmp_path, simple_test_file)
893+
results_file = orig_results_file.with_name("memray-foobar.bin")
894+
orig_results_file.rename(results_file)
895+
json_file = tmp_path / "memray-stats-foobar.bin.json"
896+
897+
# WHEN
898+
subprocess.check_output(
899+
[
900+
sys.executable,
901+
"-m",
902+
"memray",
903+
"stats",
904+
"--json",
905+
str(results_file),
906+
],
907+
cwd=str(tmp_path),
908+
text=True,
909+
)
910+
911+
# THEN
912+
assert json_file.exists()
913+
assert isinstance(json.loads(json_file.read_text()), dict)
914+
915+
def test_json_generated_to_known_file(self, tmp_path, simple_test_file):
916+
# GIVEN
917+
results_file, _ = generate_sample_results(tmp_path, simple_test_file)
918+
json_file = tmp_path / "output.json"
919+
920+
# WHEN
921+
subprocess.check_output(
922+
[
923+
sys.executable,
924+
"-m",
925+
"memray",
926+
"stats",
927+
"--json",
928+
"-o",
929+
str(json_file),
930+
str(results_file),
931+
],
932+
cwd=str(tmp_path),
933+
text=True,
934+
)
935+
936+
# THEN
937+
assert json_file.exists()
938+
assert isinstance(json.loads(json_file.read_text()), dict)
939+
940+
def test_json_generated_to_existing_known_file(self, tmp_path, simple_test_file):
941+
# GIVEN
942+
results_file, _ = generate_sample_results(tmp_path, simple_test_file)
943+
json_file = tmp_path / "output.json"
944+
json_file.write_text("oops")
945+
946+
# WHEN
947+
try:
948+
exc = None
949+
subprocess.check_output(
950+
[
951+
sys.executable,
952+
"-m",
953+
"memray",
954+
"stats",
955+
"--json",
956+
"-o",
957+
str(json_file),
958+
str(results_file),
959+
],
960+
cwd=str(tmp_path),
961+
stderr=subprocess.PIPE,
962+
text=True,
963+
)
964+
except subprocess.CalledProcessError as e:
965+
exc = e
966+
967+
# THEN
968+
assert exc is not None
969+
assert "File already exists, will not overwrite" in exc.stderr
970+
971+
def test_json_overwrites_existing_known_file(self, tmp_path, simple_test_file):
972+
# GIVEN
973+
results_file, _ = generate_sample_results(tmp_path, simple_test_file)
974+
json_file = tmp_path / "output.json"
975+
json_file.write_text("oops")
976+
977+
# WHEN
978+
subprocess.check_output(
979+
[
980+
sys.executable,
981+
"-m",
982+
"memray",
983+
"stats",
984+
"--json",
985+
"--force",
986+
"--output",
987+
str(json_file),
988+
str(results_file),
989+
],
990+
cwd=str(tmp_path),
991+
stderr=subprocess.PIPE,
992+
text=True,
993+
)
994+
995+
# THEN
996+
assert json_file.exists()
997+
assert isinstance(json.loads(json_file.read_text()), dict)
998+
866999
def test_report_detects_corrupt_input(self, tmp_path):
8671000
# GIVEN
8681001
bad_file = Path(tmp_path) / "badfile.bin"

tests/unit/test_stats_reporter.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from collections import Counter
23
from datetime import datetime
34
from typing import List
@@ -391,9 +392,9 @@ def test_stats_output(fake_stats):
391392

392393

393394
def test_stats_output_json(fake_stats, tmp_path):
395+
output_file = tmp_path / "json.out"
394396
reporter = StatsReporter(fake_stats, 5)
395-
with patch("json.dump") as json_dump:
396-
reporter.render(to_json=True, result_path=tmp_path)
397+
reporter.render(json_output_file=output_file)
397398
expected = {
398399
"total_num_allocations": 20,
399400
"total_bytes_allocated": 3341500,
@@ -437,5 +438,5 @@ def test_stats_output_json(fake_stats, tmp_path):
437438
"has_native_traces": False,
438439
},
439440
}
440-
actual = json_dump.call_args[0][0]
441+
actual = json.loads(output_file.read_text())
441442
assert expected == actual

0 commit comments

Comments
 (0)