Skip to content

Commit af2ee79

Browse files
authored
Merge pull request #26 from anurag03/ansinha/cleanup
Adding pre-commit hook and standardizing repo code.
2 parents 5c0cf8d + 6be5e5b commit af2ee79

File tree

8 files changed

+178
-92
lines changed

8 files changed

+178
-92
lines changed

.pre-commit-config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
repos:
2+
- repo: https://github.com/psf/black
3+
rev: 24.3.0
4+
hooks:
5+
- id: black
6+
language_version: python3
7+
- repo: https://github.com/pycqa/flake8
8+
rev: 7.0.0
9+
hooks:
10+
- id: flake8
11+
language_version: python3
12+
additional_dependencies: ["flake8"]
13+
args: ["--ignore=E501"]

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="testcato",
8-
version="1.2.2",
8+
version="1.2.3",
99
packages=find_packages(),
1010
description="A package for categorizing test results.",
1111
long_description=long_description,

testcato/__init__.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
21
# Package metadata for wheel
32
__title__ = "testcato"
4-
__version__ = "1.2.2"
3+
__version__ = "1.2.3"
54

65
# Automatically create config file if not present
76
import os
87
import shutil
98

9+
1010
def _ensure_config():
11-
config_name = "testcato_config.yaml"
12-
config_src = os.path.join(os.path.dirname(__file__), "..", config_name)
13-
config_dst = os.path.join(os.getcwd(), config_name)
14-
if not os.path.exists(config_dst):
15-
if os.path.exists(config_src):
16-
shutil.copyfile(config_src, config_dst)
11+
config_name = "testcato_config.yaml"
12+
config_src = os.path.join(os.path.dirname(__file__), "..", config_name)
13+
config_dst = os.path.join(os.getcwd(), config_name)
14+
if not os.path.exists(config_dst):
15+
if os.path.exists(config_src):
16+
shutil.copyfile(config_src, config_dst)
17+
1718

1819
_ensure_config()

testcato/ai_agent.py

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,142 @@
11
import os
22
import glob
3-
## import xml.etree.ElementTree as ET # No longer needed, switched to JSONL
3+
4+
# import xml.etree.ElementTree as ET # No longer needed, switched to JSONL
45
import datetime
56
import requests
67
import yaml
78

9+
810
def get_latest_jsonl(result_dir):
9-
files = glob.glob(os.path.join(result_dir, 'test_run_*.jsonl'))
11+
files = glob.glob(os.path.join(result_dir, "test_run_*.jsonl"))
1012
if not files:
1113
return None
1214
return max(files, key=os.path.getctime)
1315

16+
1417
def load_agent_config(config_path):
15-
with open(config_path, 'r') as f:
18+
with open(config_path, "r") as f:
1619
config = yaml.safe_load(f)
1720
if not isinstance(config, dict):
1821
return None
19-
default_agent = config.get('default', 'agent1')
22+
default_agent = config.get("default", "agent1")
2023
agent = config.get(default_agent, {})
2124
# Check if agent config is empty or missing required fields
22-
if not agent or not agent.get('api_key') or not agent.get('api_url'):
25+
if not agent or not agent.get("api_key") or not agent.get("api_url"):
2326
return None
2427
return agent
2528

29+
2630
def send_to_ai_agent(agent, test_name, traceback):
27-
api_url = agent.get('api_url')
31+
api_url = agent.get("api_url")
2832
if not api_url:
2933
return "No api_url provided in agent config."
3034
headers = {
31-
'Authorization': f"Bearer {agent.get('api_key', '')}",
32-
'Content-Type': 'application/json'
35+
"Authorization": f"Bearer {agent.get('api_key', '')}",
36+
"Content-Type": "application/json",
3337
}
3438
# Payload for OpenAI chat completions API
3539
payload = {
36-
"model": agent.get('model', 'gpt-4'),
40+
"model": agent.get("model", "gpt-4"),
3741
"messages": [
38-
{"role": "system", "content": "You are a helpful test debugging assistant."},
39-
{"role": "user", "content": f"Debug this test failure: {test_name}\nTraceback:\n{traceback}"}
40-
]
42+
{
43+
"role": "system",
44+
"content": "You are a helpful test debugging assistant.",
45+
},
46+
{
47+
"role": "user",
48+
"content": f"Debug this test failure: {test_name}\nTraceback:\n{traceback}",
49+
},
50+
],
4151
}
4252
response = requests.post(api_url, json=payload, headers=headers)
4353
if response.ok:
4454
try:
45-
return response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
55+
return (
56+
response.json()
57+
.get("choices", [{}])[0]
58+
.get("message", {})
59+
.get("content", "")
60+
)
4661
except Exception:
4762
return response.text
4863
# Print error details in red to CLI for visibility
49-
RED = '\033[31m'
50-
RESET = '\033[0m'
51-
print(f"{RED}TESTCATO AI agent error for test '{test_name}': {response.status_code} - {response.text}{RESET}")
64+
RED = "\033[31m"
65+
RESET = "\033[0m"
66+
print(
67+
f"{RED}TESTCATO AI agent error for test '{test_name}': {response.status_code} - {response.text}{RESET}"
68+
)
5269
return "AI agent failed to respond."
5370

71+
5472
def debug_latest_jsonl():
5573
# Generate debug JSONL and HTML report after lines, result_dir, and timestamp are defined
56-
result_dir = os.path.join(os.getcwd(), 'testcato_result')
57-
config_path = os.path.join(os.getcwd(), 'testcato_config.yaml')
74+
result_dir = os.path.join(os.getcwd(), "testcato_result")
75+
config_path = os.path.join(os.getcwd(), "testcato_config.yaml")
5876
latest_jsonl = get_latest_jsonl(result_dir)
5977
if not latest_jsonl:
6078
print("No test_run JSONL file found.")
6179
return
6280
agent = load_agent_config(config_path)
6381
if not agent:
6482
# Print warning in yellow in pytest output or console
65-
YELLOW = '\033[33m'
66-
RESET = '\033[0m'
83+
YELLOW = "\033[33m"
84+
RESET = "\033[0m"
6785
warning_msg = f"{YELLOW}WARNING: TESTCATO: No valid AI agent config found. Debugging is disabled.{RESET}"
6886
import sys
69-
tr = getattr(sys, '_pytest_terminalreporter', None)
87+
88+
tr = getattr(sys, "_pytest_terminalreporter", None)
7089
if tr:
7190
tr.write_line(warning_msg)
7291
else:
7392
print(warning_msg)
7493
return
7594
import json
95+
7696
lines = []
77-
with open(latest_jsonl, 'r', encoding='utf-8') as f:
97+
with open(latest_jsonl, "r", encoding="utf-8") as f:
7898
for raw_line in f:
7999
try:
80100
test_data = json.loads(raw_line)
81101
except Exception:
82102
continue
83-
test_name = test_data.get('name') or test_data.get('test_name')
84-
status = test_data.get('status', 'failed')
85-
traceback = test_data.get('traceback')
103+
test_name = test_data.get("name") or test_data.get("test_name")
104+
status = test_data.get("status", "failed")
105+
traceback = test_data.get("traceback")
86106
debug_result = None
87107
if traceback:
88108
debug_result = send_to_ai_agent(agent, test_name, traceback)
89109
line = {
90110
"test_name": test_name,
91111
"status": status,
92112
"traceback": traceback,
93-
"debug_result": debug_result
113+
"debug_result": debug_result,
94114
}
95115
lines.append(line)
96-
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
97-
debug_jsonl_path = os.path.join(result_dir, f'test_debug_{timestamp}.jsonl')
98-
with open(debug_jsonl_path, 'w', encoding='utf-8') as f:
116+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
117+
debug_jsonl_path = os.path.join(result_dir, f"test_debug_{timestamp}.jsonl")
118+
with open(debug_jsonl_path, "w", encoding="utf-8") as f:
99119
for line in lines:
100-
f.write(json.dumps(line, ensure_ascii=False) + '\n')
101-
GREEN = '\033[32m'
102-
RESET = '\033[0m'
120+
f.write(json.dumps(line, ensure_ascii=False) + "\n")
121+
GREEN = "\033[32m"
122+
RESET = "\033[0m"
103123
print(f"{GREEN}Debug results saved to {debug_jsonl_path}{RESET}")
104124

105125
# Also generate a human-readable HTML report
106-
html_path = os.path.join(result_dir, f'test_debug_{timestamp}.html')
107-
with open(html_path, 'w', encoding='utf-8') as html:
108-
html.write('<html><head><title>Test Debug Report</title></head><body>')
109-
html.write('<h1>Test Debug Report</h1>')
126+
html_path = os.path.join(result_dir, f"test_debug_{timestamp}.html")
127+
with open(html_path, "w", encoding="utf-8") as html:
128+
html.write("<html><head><title>Test Debug Report</title></head><body>")
129+
html.write("<h1>Test Debug Report</h1>")
110130
html.write('<table border="1" cellpadding="5" cellspacing="0">')
111-
html.write('<tr><th>Test Name</th><th>Status</th><th>Traceback</th><th>Debug Result</th></tr>')
131+
html.write(
132+
"<tr><th>Test Name</th><th>Status</th><th>Traceback</th><th>Debug Result</th></tr>"
133+
)
112134
for line in lines:
113-
html.write('<tr>')
135+
html.write("<tr>")
114136
html.write(f'<td>{line["test_name"]}</td>')
115137
html.write(f'<td>{line["status"]}</td>')
116138
html.write(f'<td><pre>{line["traceback"]}</pre></td>')
117139
html.write(f'<td><pre>{line["debug_result"]}</pre></td>')
118-
html.write('</tr>')
119-
html.write('</table></body></html>')
140+
html.write("</tr>")
141+
html.write("</table></body></html>")
120142
print(f"{GREEN}HTML debug report saved to {html_path}{RESET}")

testcato/categorizer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Core logic for categorizing test results
22

3+
34
class TestCategorizer:
45
def __init__(self):
56
pass
@@ -10,10 +11,10 @@ def categorize(self, test_results):
1011
:param test_results: List of test result dicts
1112
:return: Dict with categories as keys and lists of test names as values
1213
"""
13-
categories = {'passed': [], 'failed': [], 'skipped': []}
14+
categories = {"passed": [], "failed": [], "skipped": []}
1415
for result in test_results:
15-
status = result.get('status')
16-
name = result.get('name')
16+
status = result.get("status")
17+
name = result.get("name")
1718
if status in categories:
1819
categories[status].append(name)
1920
return categories

testcato/pytest_testcato.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,59 @@
11
import re
2-
import pytest
2+
3+
# import pytest # Unused import removed
34
import os
45
import datetime
5-
## import xml.etree.ElementTree as ET # No longer needed, switched to JSONL
6+
7+
# import xml.etree.ElementTree as ET # No longer needed, switched to JSONL
68
from .categorizer import TestCategorizer
79

10+
811
def pytest_addoption(parser):
912
parser.addoption(
1013
"--testcato",
1114
action="store_true",
1215
default=False,
13-
help="Categorize test results using testcato"
16+
help="Categorize test results using testcato",
1417
)
1518

19+
1620
def pytest_configure(config):
1721
if config.getoption("--testcato"):
1822
# Add -vvv for maximum verbosity if not already present
1923
# Pytest config.option.verbose is an int, 0=default, 1=-v, 2=-vv, 3=-vvv
2024
if getattr(config.option, "verbose", 0) < 3:
2125
config.option.verbose = 3
2226

27+
2328
def pytest_terminal_summary(terminalreporter, exitstatus, config):
2429
def remove_ansi_codes(text):
25-
ansi_escape = re.compile(r'\x1B[@-_][0-?]*[ -/]*[@-~]')
26-
return ansi_escape.sub('', text)
30+
ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
31+
return ansi_escape.sub("", text)
32+
2733
if config.getoption("--testcato"):
2834
results = []
2935
tracebacks = []
3036
for report in terminalreporter.getreports("passed"):
31-
results.append({'name': report.nodeid, 'status': 'passed'})
37+
results.append({"name": report.nodeid, "status": "passed"})
3238
for report in terminalreporter.getreports("failed"):
33-
results.append({'name': report.nodeid, 'status': 'failed'})
34-
if hasattr(report, 'longrepr') and report.longrepr:
39+
results.append({"name": report.nodeid, "status": "failed"})
40+
if hasattr(report, "longrepr") and report.longrepr:
3541
tb = remove_ansi_codes(str(report.longrepr))
36-
tracebacks.append({'name': report.nodeid, 'traceback': tb})
42+
tracebacks.append({"name": report.nodeid, "traceback": tb})
3743
for report in terminalreporter.getreports("skipped"):
38-
results.append({'name': report.nodeid, 'status': 'skipped'})
44+
results.append({"name": report.nodeid, "status": "skipped"})
3945

4046
# Save tracebacks to JSON Lines
4147
if tracebacks:
4248
import json
43-
result_dir = os.path.join(os.getcwd(), 'testcato_result')
49+
50+
result_dir = os.path.join(os.getcwd(), "testcato_result")
4451
os.makedirs(result_dir, exist_ok=True)
45-
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
46-
jsonl_path = os.path.join(result_dir, f'test_run_{timestamp}.jsonl')
47-
with open(jsonl_path, 'w', encoding='utf-8') as f:
52+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
53+
jsonl_path = os.path.join(result_dir, f"test_run_{timestamp}.jsonl")
54+
with open(jsonl_path, "w", encoding="utf-8") as f:
4855
for tb in tracebacks:
49-
f.write(json.dumps(tb, ensure_ascii=False) + '\n')
56+
f.write(json.dumps(tb, ensure_ascii=False) + "\n")
5057
terminalreporter.write_line(f"Tracebacks saved to {jsonl_path}")
5158

5259
categorizer = TestCategorizer()
@@ -57,9 +64,10 @@ def remove_ansi_codes(text):
5764
for test in tests:
5865
terminalreporter.write_line(f" {test}")
5966

60-
# Automatically send latest JSONL to AI agent and save debug file
67+
# Automatically send latest JSONL to AI agent and save debug file
6168
try:
6269
from .ai_agent import debug_latest_jsonl
70+
6371
debug_latest_jsonl()
6472
except Exception as e:
6573
terminalreporter.write_line(f"Error sending JSONL to AI agent: {e}")

0 commit comments

Comments
 (0)