Spaces:
Running
Running
Johnny
commited on
Commit
ยท
cc174b7
1
Parent(s):
36ea490
updated unit tests, and supabase
Browse files- test_module.py +196 -45
- utils.py +26 -6
test_module.py
CHANGED
@@ -1,67 +1,218 @@
|
|
1 |
import pytest
|
2 |
-
import
|
3 |
-
import re
|
4 |
from io import BytesIO
|
5 |
-
|
6 |
-
|
7 |
-
from config import SUPABASE_URL, SUPABASE_KEY, HF_API_TOKEN, HF_HEADERS, supabase, HF_MODELS, query, embedding_model
|
8 |
from utils import (
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
)
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
def test_parse_resume():
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
assert "Sample Resume Text" in extracted_text
|
23 |
|
|
|
|
|
24 |
def test_extract_email():
|
25 |
-
text = "Contact me at
|
26 |
-
assert extract_email(text) == "
|
27 |
-
|
28 |
-
text_no_email = "No email here."
|
29 |
-
assert extract_email(text_no_email) is None
|
30 |
|
31 |
-
|
32 |
-
def test_score_candidate(mock_encode):
|
33 |
-
# Mock embeddings as tensors with the same shape as actual embeddings
|
34 |
-
mock_encode.return_value = torch.tensor([0.1, 0.2, 0.3])
|
35 |
|
36 |
-
score = score_candidate("Resume text", "Job description")
|
37 |
|
|
|
|
|
|
|
38 |
assert isinstance(score, float)
|
39 |
-
assert 0 <= score <= 1
|
|
|
40 |
|
41 |
-
|
|
|
42 |
def test_summarize_resume(mock_query):
|
43 |
-
mock_query.return_value = [{"generated_text": "This is a summary
|
44 |
-
summary = summarize_resume("
|
45 |
-
assert summary == "This is a summary
|
46 |
-
|
47 |
mock_query.return_value = None
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
def test_generate_pdf_report():
|
59 |
candidates = [{
|
60 |
"name": "John Doe",
|
61 |
"email": "john@example.com",
|
62 |
-
"score": 0.
|
63 |
-
"summary": "
|
64 |
}]
|
65 |
-
pdf = generate_pdf_report(candidates)
|
66 |
assert isinstance(pdf, BytesIO)
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import pytest
|
2 |
+
from unittest.mock import patch, MagicMock
|
|
|
3 |
from io import BytesIO
|
4 |
+
|
5 |
+
# Import all functions to test
|
|
|
6 |
from utils import (
|
7 |
+
extract_keywords,
|
8 |
+
parse_resume,
|
9 |
+
extract_email,
|
10 |
+
score_candidate,
|
11 |
+
summarize_resume,
|
12 |
+
filter_resumes_by_keywords,
|
13 |
+
evaluate_resumes,
|
14 |
+
store_in_supabase,
|
15 |
+
generate_pdf_report,
|
16 |
+
generate_interview_questions_from_summaries
|
17 |
)
|
18 |
|
19 |
+
# Run Command for Full Coverage Report: pytest --cov=utils --cov-report=term-missing -v
|
20 |
+
|
21 |
+
# --- Mock Models and External APIs ---
|
22 |
+
@pytest.fixture(autouse=True)
|
23 |
+
def patch_embedding_model(monkeypatch):
|
24 |
+
mock_model = MagicMock()
|
25 |
+
mock_model.encode.return_value = [0.1, 0.2, 0.3]
|
26 |
+
monkeypatch.setattr("utils.embedding_model", mock_model)
|
27 |
+
|
28 |
+
|
29 |
+
@pytest.fixture(autouse=True)
|
30 |
+
def patch_spacy(monkeypatch):
|
31 |
+
nlp_mock = MagicMock()
|
32 |
+
nlp_mock.return_value = [MagicMock(text="python", pos_="NOUN", is_stop=False)]
|
33 |
+
monkeypatch.setattr("utils.nlp", nlp_mock)
|
34 |
+
|
35 |
+
|
36 |
+
# --- extract_keywords ---
|
37 |
+
def test_extract_keywords():
|
38 |
+
text = "We are looking for a Python developer with Django and REST experience."
|
39 |
+
keywords = extract_keywords(text)
|
40 |
+
assert isinstance(keywords, list)
|
41 |
+
assert "python" in keywords or len(keywords) > 0
|
42 |
+
|
43 |
+
|
44 |
+
# --- parse_resume ---
|
45 |
def test_parse_resume():
|
46 |
+
dummy_pdf = MagicMock()
|
47 |
+
dummy_pdf.read.return_value = b"%PDF-1.4"
|
48 |
+
with patch("fitz.open") as mocked_fitz:
|
49 |
+
page_mock = MagicMock()
|
50 |
+
page_mock.get_text.return_value = "Resume Text Here"
|
51 |
+
mocked_fitz.return_value = [page_mock]
|
52 |
+
result = parse_resume(dummy_pdf)
|
53 |
+
assert "Resume Text" in result
|
|
|
54 |
|
55 |
+
|
56 |
+
# --- extract_email ---
|
57 |
def test_extract_email():
|
58 |
+
text = "Contact me at johndoe@example.com for more info."
|
59 |
+
assert extract_email(text) == "johndoe@example.com"
|
|
|
|
|
|
|
60 |
|
61 |
+
assert extract_email("No email here!") is None
|
|
|
|
|
|
|
62 |
|
|
|
63 |
|
64 |
+
# --- score_candidate ---
|
65 |
+
def test_score_candidate():
|
66 |
+
score = score_candidate("Experienced Python developer", "Looking for Python engineer")
|
67 |
assert isinstance(score, float)
|
68 |
+
assert 0 <= score <= 1
|
69 |
+
|
70 |
|
71 |
+
# --- summarize_resume ---
|
72 |
+
@patch("utils.query")
|
73 |
def test_summarize_resume(mock_query):
|
74 |
+
mock_query.return_value = [{"generated_text": "This is a summary"}]
|
75 |
+
summary = summarize_resume("This is a long resume text.")
|
76 |
+
assert summary == "This is a summary"
|
77 |
+
|
78 |
mock_query.return_value = None
|
79 |
+
fallback = summarize_resume("Another resume")
|
80 |
+
assert "unavailable" in fallback.lower()
|
81 |
+
|
82 |
+
|
83 |
+
# --- filter_resumes_by_keywords ---
|
84 |
+
def test_filter_resumes_by_keywords():
|
85 |
+
resumes = [
|
86 |
+
{"name": "John", "resume": "python django rest api"},
|
87 |
+
{"name": "Doe", "resume": "java spring"}
|
88 |
+
]
|
89 |
+
job_description = "Looking for a python developer with API knowledge."
|
90 |
+
filtered, removed = filter_resumes_by_keywords(resumes, job_description, min_keyword_match=1)
|
91 |
+
|
92 |
+
assert isinstance(filtered, list)
|
93 |
+
assert isinstance(removed, list)
|
94 |
+
assert len(filtered) + len(removed) == 2
|
95 |
+
|
96 |
+
|
97 |
+
# --- evaluate_resumes ---
|
98 |
+
@patch("utils.parse_resume", return_value="python flask api")
|
99 |
+
@patch("utils.extract_email", return_value="test@example.com")
|
100 |
+
@patch("utils.summarize_resume", return_value="A senior Python developer.")
|
101 |
+
@patch("utils.score_candidate", return_value=0.85)
|
102 |
+
def test_evaluate_resumes(_, __, ___, ____):
|
103 |
+
class DummyFile:
|
104 |
+
def __init__(self, name): self.name = name
|
105 |
+
def read(self): return b"%PDF-1.4"
|
106 |
+
|
107 |
+
uploaded_files = [DummyFile("resume1.pdf")]
|
108 |
+
job_desc = "Looking for a python developer."
|
109 |
+
|
110 |
+
shortlisted, removed = evaluate_resumes(uploaded_files, job_desc)
|
111 |
+
assert len(shortlisted) == 1
|
112 |
+
assert isinstance(removed, list)
|
113 |
+
|
114 |
|
115 |
+
# --- store_in_supabase ---
|
116 |
+
@patch("utils.supabase")
|
117 |
+
def test_store_in_supabase(mock_supabase):
|
118 |
+
table_mock = MagicMock()
|
119 |
+
table_mock.insert.return_value.execute.return_value = {"status": "success"}
|
120 |
+
mock_supabase.table.return_value = table_mock
|
121 |
+
|
122 |
+
response = store_in_supabase("text", 0.8, "John", "john@example.com", "summary")
|
123 |
+
assert "status" in response
|
124 |
+
|
125 |
+
|
126 |
+
# --- generate_pdf_report ---
|
127 |
def test_generate_pdf_report():
|
128 |
candidates = [{
|
129 |
"name": "John Doe",
|
130 |
"email": "john@example.com",
|
131 |
+
"score": 0.87,
|
132 |
+
"summary": "Python developer"
|
133 |
}]
|
134 |
+
pdf = generate_pdf_report(candidates, questions=["What are your strengths?"])
|
135 |
assert isinstance(pdf, BytesIO)
|
136 |
+
|
137 |
+
|
138 |
+
# --- generate_interview_questions_from_summaries ---
|
139 |
+
@patch("utils.client.chat_completion")
|
140 |
+
def test_generate_interview_questions_from_summaries(mock_chat):
|
141 |
+
mock_chat.return_value.choices = [
|
142 |
+
MagicMock(message=MagicMock(content="""
|
143 |
+
1. What are your strengths?
|
144 |
+
2. Describe a project you've led.
|
145 |
+
3. How do you handle tight deadlines?
|
146 |
+
"""))
|
147 |
+
]
|
148 |
+
|
149 |
+
candidates = [{"summary": "Experienced Python developer"}]
|
150 |
+
questions = generate_interview_questions_from_summaries(candidates)
|
151 |
+
assert len(questions) > 0
|
152 |
+
assert all(q.startswith("Q") for q in questions)
|
153 |
+
|
154 |
+
@patch("utils.supabase")
|
155 |
+
def test_store_in_supabase(mock_supabase):
|
156 |
+
mock_table = MagicMock()
|
157 |
+
mock_execute = MagicMock()
|
158 |
+
mock_execute.return_value = {"status": "success"}
|
159 |
+
|
160 |
+
# Attach mocks
|
161 |
+
mock_table.insert.return_value.execute = mock_execute
|
162 |
+
mock_supabase.table.return_value = mock_table
|
163 |
+
|
164 |
+
data = {
|
165 |
+
"resume_text": "Some text",
|
166 |
+
"score": 0.85,
|
167 |
+
"candidate_name": "Alice",
|
168 |
+
"email": "alice@example.com",
|
169 |
+
"summary": "Experienced backend developer"
|
170 |
+
}
|
171 |
+
|
172 |
+
response = store_in_supabase(**data)
|
173 |
+
assert response["status"] == "success"
|
174 |
+
|
175 |
+
mock_supabase.table.assert_called_once_with("candidates")
|
176 |
+
mock_table.insert.assert_called_once()
|
177 |
+
inserted_data = mock_table.insert.call_args[0][0]
|
178 |
+
assert inserted_data["name"] == "Alice"
|
179 |
+
assert inserted_data["email"] == "alice@example.com"
|
180 |
+
|
181 |
+
def test_extract_keywords_empty_input():
|
182 |
+
assert extract_keywords("") == []
|
183 |
+
|
184 |
+
def test_extract_email_malformed():
|
185 |
+
malformed_text = "email at example dot com"
|
186 |
+
assert extract_email(malformed_text) is None
|
187 |
+
|
188 |
+
def test_score_candidate_failure(monkeypatch):
|
189 |
+
def broken_encode(*args, **kwargs): raise Exception("fail")
|
190 |
+
monkeypatch.setattr("utils.embedding_model.encode", broken_encode)
|
191 |
+
score = score_candidate("resume", "job description")
|
192 |
+
assert score == 0
|
193 |
+
|
194 |
+
@patch("utils.query")
|
195 |
+
def test_summarize_resume_bad_response(mock_query):
|
196 |
+
mock_query.return_value = {"weird_key": "no summary here"}
|
197 |
+
summary = summarize_resume("Resume text")
|
198 |
+
assert "unavailable" in summary.lower()
|
199 |
+
|
200 |
+
@patch("utils.query")
|
201 |
+
def test_summarize_resume_bad_response(mock_query):
|
202 |
+
mock_query.return_value = {"weird_key": "no summary here"}
|
203 |
+
summary = summarize_resume("Resume text")
|
204 |
+
assert "unavailable" in summary.lower()
|
205 |
+
|
206 |
+
@patch("utils.parse_resume", return_value="some text")
|
207 |
+
@patch("utils.extract_email", return_value=None)
|
208 |
+
@patch("utils.summarize_resume", return_value="Summary here")
|
209 |
+
@patch("utils.score_candidate", return_value=0.1)
|
210 |
+
def test_evaluate_resumes_low_score_filtered(_, __, ___, ____):
|
211 |
+
class Dummy:
|
212 |
+
name = "resume.pdf"
|
213 |
+
def read(self): return b"%PDF"
|
214 |
+
|
215 |
+
uploaded = [Dummy()]
|
216 |
+
shortlisted, removed = evaluate_resumes(uploaded, "job description")
|
217 |
+
assert len(shortlisted) == 0
|
218 |
+
assert len(removed) == 1
|
utils.py
CHANGED
@@ -68,17 +68,37 @@ def evaluate_resumes(uploaded_files, job_description, min_keyword_match=2):
|
|
68 |
"summary": summary
|
69 |
})
|
70 |
|
|
|
71 |
filtered_candidates, keyword_removed = filter_resumes_by_keywords(
|
72 |
candidates, job_description, min_keyword_match
|
73 |
)
|
74 |
-
|
|
|
75 |
for name in keyword_removed:
|
76 |
removed_candidates.append({"name": name, "reason": "Insufficient keyword matches"})
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
|
83 |
# === Keyword & Scoring Functions ===
|
84 |
|
|
|
68 |
"summary": summary
|
69 |
})
|
70 |
|
71 |
+
# ๐น Step 2: Filter candidates based on keyword matches
|
72 |
filtered_candidates, keyword_removed = filter_resumes_by_keywords(
|
73 |
candidates, job_description, min_keyword_match
|
74 |
)
|
75 |
+
|
76 |
+
# ๐น Step 3: Log removed candidates
|
77 |
for name in keyword_removed:
|
78 |
removed_candidates.append({"name": name, "reason": "Insufficient keyword matches"})
|
79 |
+
|
80 |
+
# ๐น Step 4: Ensure the final list is sorted by score and limit to top 5 candidates
|
81 |
+
shortlisted_candidates = sorted(filtered_candidates, key=lambda x: x["score"], reverse=True)[:5]
|
82 |
+
|
83 |
+
# ๐น Step 4.5: Store shortlisted candidates in Supabase
|
84 |
+
for candidate in shortlisted_candidates:
|
85 |
+
try:
|
86 |
+
store_in_supabase(
|
87 |
+
resume_text=candidate["resume"],
|
88 |
+
score=candidate["score"],
|
89 |
+
candidate_name=candidate["name"],
|
90 |
+
email=candidate["email"],
|
91 |
+
summary=candidate["summary"]
|
92 |
+
)
|
93 |
+
except Exception as e:
|
94 |
+
print(f"โ Failed to store {candidate['name']} in Supabase: {e}")
|
95 |
+
|
96 |
+
# ๐น Step 5: Ensure return value is always a list
|
97 |
+
if not isinstance(shortlisted_candidates, list):
|
98 |
+
print("โ ๏ธ ERROR: shortlisted_candidates is not a list! Returning empty list.")
|
99 |
+
return [], removed_candidates
|
100 |
+
|
101 |
+
return shortlisted_candidates, removed_candidates
|
102 |
|
103 |
# === Keyword & Scoring Functions ===
|
104 |
|