Skip to content

Commit 426c926

Browse files
committed
tests and fixes
1 parent 942fb78 commit 426c926

File tree

2 files changed

+241
-24
lines changed

2 files changed

+241
-24
lines changed

counterparty-core/counterpartycore/lib/monitors/profiler.py

+12-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import cProfile
22
import logging
33
import os
4-
import pstats
54
import threading
65
import time
76
from datetime import datetime
@@ -25,7 +24,9 @@ def __init__(self, interval_minutes=15):
2524
self.stop_event = threading.Event()
2625
self.profiler = None
2726
self.active_profiling = False
28-
logger.info(f"Periodic profiler initialized with an interval of {interval_minutes} minutes")
27+
logger.info(
28+
"Periodic profiler initialized with an interval of %s minutes", interval_minutes
29+
)
2930

3031
def start_profiling(self):
3132
"""Starts a profiling session"""
@@ -47,25 +48,13 @@ def stop_profiling_and_save(self):
4748
profile_path = os.path.join(config.CACHE_DIR, f"profile_{timestamp}.prof")
4849

4950
try:
51+
# Ensure the directory exists
52+
os.makedirs(os.path.dirname(profile_path), exist_ok=True)
53+
5054
# Save profiling data
5155
self.profiler.dump_stats(profile_path)
5256
logger.info(f"Profiling report saved to {profile_path}")
5357

54-
# Display a summary in the logs
55-
stats = pstats.Stats(self.profiler)
56-
stats_path = os.path.join(config.CACHE_DIR, f"profile_{timestamp}.txt")
57-
58-
with open(stats_path, "w") as stats_file:
59-
# Functions most expensive in cumulative time
60-
stats_file.write("=== Top 20 functions (cumulative time) ===\n")
61-
stats.sort_stats("cumtime").print_stats(20, file=stats_file)
62-
63-
# Functions most expensive in total time
64-
stats_file.write("\n=== Top 20 functions (total time) ===\n")
65-
stats.sort_stats("tottime").print_stats(20, file=stats_file)
66-
67-
logger.info(f"Profiling summary saved to {stats_path}")
68-
6958
except Exception as e:
7059
logger.error(f"Error generating profiling report: {e}")
7160

@@ -88,17 +77,16 @@ def run(self):
8877
self.start_profiling()
8978
last_report_time = time.time()
9079

91-
# Check every second if stop is requested
92-
time.sleep(1)
93-
94-
# Generate a final report on shutdown
95-
if self.active_profiling:
96-
logger.info("Generating final profiling report before shutdown")
97-
self.stop_profiling_and_save()
80+
# Adjust sleep time based on interval for better precision with short intervals
81+
# Small intervals get shorter sleep times for more frequent checks
82+
sleep_time = min(1.0, max(0.1, self.interval_minutes * 60 / 10))
83+
time.sleep(sleep_time)
9884

9985
def stop(self):
10086
"""Stops the profiling thread"""
10187
logger.info("Stopping periodic profiler thread...")
88+
if self.active_profiling:
89+
self.stop_profiling_and_save()
10290
self.stop_event.set()
10391
self.join()
10492
logger.info("Periodic profiler thread stopped.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import os
2+
import shutil
3+
import tempfile
4+
import time
5+
6+
from counterpartycore.lib import config
7+
from counterpartycore.lib.monitors.profiler import PeriodicProfilerThread
8+
9+
10+
class TestPeriodicProfilerThread:
11+
@classmethod
12+
def setup_class(cls):
13+
"""Prepare the test environment with a temporary folder."""
14+
# Create a temporary directory for tests
15+
cls.temp_dir = tempfile.mkdtemp()
16+
# Save the old CACHE_DIR
17+
if hasattr(config, "CACHE_DIR"):
18+
cls.original_cache_dir = config.CACHE_DIR
19+
else:
20+
cls.original_cache_dir = None
21+
config.CACHE_DIR = cls.temp_dir
22+
23+
# Replace CACHE_DIR with our temporary directory
24+
config.CACHE_DIR = cls.temp_dir
25+
26+
@classmethod
27+
def teardown_class(cls):
28+
"""Clean up the environment after all tests."""
29+
# Restore the old CACHE_DIR
30+
if cls.original_cache_dir is not None:
31+
config.CACHE_DIR = cls.original_cache_dir
32+
else:
33+
delattr(config, "CACHE_DIR")
34+
35+
# Delete the temporary directory
36+
shutil.rmtree(cls.temp_dir)
37+
38+
def setup_method(self):
39+
"""Prepare a new instance for each test."""
40+
# Create a new profiling thread instance for each test
41+
# Use a very short interval to speed up tests
42+
self.profiler_thread = PeriodicProfilerThread(
43+
interval_minutes=0.02
44+
) # 0.02 minutes = 1.2 seconds
45+
46+
# Ensure the cache directory exists
47+
os.makedirs(config.CACHE_DIR, exist_ok=True)
48+
49+
def teardown_method(self):
50+
"""Clean up after each test."""
51+
# Make sure the thread is stopped after each test
52+
if hasattr(self, "profiler_thread") and self.profiler_thread.is_alive():
53+
self.profiler_thread.stop()
54+
self.profiler_thread.join(timeout=1)
55+
56+
# Clean up created files
57+
for file in os.listdir(config.CACHE_DIR):
58+
os.remove(os.path.join(config.CACHE_DIR, file))
59+
60+
def test_init(self):
61+
"""Test the initialization of the class with a custom interval."""
62+
assert self.profiler_thread.interval_minutes == 0.02
63+
assert self.profiler_thread.daemon is True
64+
assert self.profiler_thread.stop_event.is_set() is False
65+
assert self.profiler_thread.profiler is None
66+
assert self.profiler_thread.active_profiling is False
67+
68+
def test_init_default_interval(self):
69+
"""Test initialization with the default interval."""
70+
profiler = PeriodicProfilerThread()
71+
assert profiler.interval_minutes == 15
72+
73+
def test_start_profiling(self):
74+
"""Test starting profiling."""
75+
self.profiler_thread.start_profiling()
76+
assert self.profiler_thread.active_profiling is True
77+
assert self.profiler_thread.profiler is not None
78+
79+
# Calling start_profiling again should not change anything
80+
profiler = self.profiler_thread.profiler
81+
self.profiler_thread.start_profiling()
82+
assert self.profiler_thread.profiler is profiler # same instance
83+
84+
def test_stop_profiling_and_save(self):
85+
"""Test stopping profiling and generating a report."""
86+
self.profiler_thread.start_profiling()
87+
self.profiler_thread.stop_profiling_and_save()
88+
89+
# Check that a file was created in the cache directory
90+
files = os.listdir(config.CACHE_DIR)
91+
assert len(files) == 1
92+
assert files[0].startswith("profile_") and files[0].endswith(".prof")
93+
94+
# Check that the profiling state was reset
95+
assert self.profiler_thread.active_profiling is False
96+
assert self.profiler_thread.profiler is None
97+
98+
def test_stop_profiling_and_save_not_active(self):
99+
"""Test stopping profiling when no profiling is active."""
100+
self.profiler_thread.active_profiling = False
101+
self.profiler_thread.stop_profiling_and_save()
102+
103+
# Check that no file was created
104+
files = os.listdir(config.CACHE_DIR)
105+
assert len(files) == 0
106+
107+
def test_stop_profiling_and_save_profiler_none(self):
108+
"""Test stopping profiling when the profiler is None."""
109+
self.profiler_thread.active_profiling = True
110+
self.profiler_thread.profiler = None
111+
self.profiler_thread.stop_profiling_and_save()
112+
113+
# Check that no file was created
114+
files = os.listdir(config.CACHE_DIR)
115+
assert len(files) == 0
116+
117+
def test_stop_profiling_and_save_with_error(self):
118+
"""Test stopping profiling with an error when writing the file."""
119+
self.profiler_thread.start_profiling()
120+
121+
# Save the current directory
122+
original_cache_dir = config.CACHE_DIR
123+
124+
try:
125+
# Replace with a directory that doesn't exist to cause an error
126+
config.CACHE_DIR = "/path/that/does/not/exist"
127+
128+
# This should raise an exception but the method is designed to catch it
129+
self.profiler_thread.stop_profiling_and_save()
130+
131+
# Check that the state was reset despite the error
132+
assert self.profiler_thread.active_profiling is False
133+
assert self.profiler_thread.profiler is None
134+
finally:
135+
# Restore the original directory
136+
config.CACHE_DIR = original_cache_dir
137+
138+
def test_run_cycle(self):
139+
"""Test the execution cycle of the thread with periodic report generation."""
140+
# Start the thread
141+
self.profiler_thread.start()
142+
143+
# Wait for the thread to start running
144+
time.sleep(0.1)
145+
assert self.profiler_thread.is_alive()
146+
assert self.profiler_thread.active_profiling is True
147+
148+
# Wait until the interval is well exceeded for a report to be generated
149+
# 0.01 minutes = 0.6 seconds, wait 1.2 seconds to be sure (2x the interval)
150+
time.sleep(1.2)
151+
152+
# Check that a report file was created
153+
# If no file is found, display useful diagnostics
154+
files = os.listdir(config.CACHE_DIR)
155+
if len(files) == 0:
156+
print(f"No file found in {config.CACHE_DIR}")
157+
print(f"Profiler interval: {self.profiler_thread.interval_minutes} minutes")
158+
print(f"Active profiling: {self.profiler_thread.active_profiling}")
159+
assert len(files) >= 1, f"No profiling file found in {config.CACHE_DIR}"
160+
161+
# Wait for another complete cycle to verify the creation of a new report
162+
time.sleep(1.2)
163+
new_files = os.listdir(config.CACHE_DIR)
164+
assert len(new_files) > len(
165+
files
166+
), f"No new files created. Before: {len(files)}, After: {len(new_files)}"
167+
168+
# Stop the thread
169+
self.profiler_thread.stop()
170+
171+
# Check that the thread is properly stopped
172+
assert self.profiler_thread.stop_event.is_set() is True
173+
self.profiler_thread.join(timeout=2) # Increased timeout
174+
assert not self.profiler_thread.is_alive()
175+
176+
def test_stop(self):
177+
"""Test stopping the thread with generation of a final report."""
178+
# Start the thread
179+
self.profiler_thread.start()
180+
181+
# Wait for the thread to start running
182+
time.sleep(0.1)
183+
assert self.profiler_thread.is_alive()
184+
assert self.profiler_thread.active_profiling is True
185+
186+
# Stop the thread
187+
self.profiler_thread.stop()
188+
189+
# Check that the thread is properly stopped
190+
assert self.profiler_thread.stop_event.is_set() is True
191+
self.profiler_thread.join(timeout=2) # Increased timeout
192+
assert not self.profiler_thread.is_alive()
193+
194+
# Check that a final report was generated
195+
files = os.listdir(config.CACHE_DIR)
196+
if len(files) == 0:
197+
print(f"No file found in {config.CACHE_DIR} after stop()")
198+
assert len(files) >= 1, f"No profiling file found in {config.CACHE_DIR} after stop()"
199+
200+
def test_stop_without_active_profiling(self):
201+
"""Test stopping the thread when no profiling is active."""
202+
# Ensure the cache directory is empty at the start of the test
203+
for file in os.listdir(config.CACHE_DIR):
204+
os.remove(os.path.join(config.CACHE_DIR, file))
205+
206+
# Start the thread
207+
self.profiler_thread.start()
208+
209+
# Wait for the thread to start running
210+
time.sleep(0.1)
211+
assert self.profiler_thread.is_alive()
212+
213+
# Modify the state to simulate inactive profiling
214+
self.profiler_thread.active_profiling = False
215+
216+
# Wait a bit to ensure the thread has taken the change into account
217+
time.sleep(0.1)
218+
219+
# Stop the thread
220+
self.profiler_thread.stop()
221+
222+
# Check that the thread is properly stopped
223+
assert self.profiler_thread.stop_event.is_set() is True
224+
self.profiler_thread.join(timeout=2)
225+
assert not self.profiler_thread.is_alive()
226+
227+
# Check that no final report was generated
228+
files = os.listdir(config.CACHE_DIR)
229+
assert len(files) == 0, f"Files were found when none were expected: {files}"

0 commit comments

Comments
 (0)