Johnny commited on
Commit
cc174b7
ยท
1 Parent(s): 36ea490

updated unit tests, and supabase

Browse files
Files changed (2) hide show
  1. test_module.py +196 -45
  2. utils.py +26 -6
test_module.py CHANGED
@@ -1,67 +1,218 @@
1
  import pytest
2
- import fitz # PyMuPDF
3
- import re
4
  from io import BytesIO
5
- import torch
6
- from unittest.mock import MagicMock, patch
7
- from config import SUPABASE_URL, SUPABASE_KEY, HF_API_TOKEN, HF_HEADERS, supabase, HF_MODELS, query, embedding_model
8
  from utils import (
9
- evaluate_resumes, parse_resume, extract_email, score_candidate,
10
- summarize_resume, store_in_supabase, generate_pdf_report
 
 
 
 
 
 
 
 
11
  )
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def test_parse_resume():
14
- pdf_content = BytesIO()
15
- doc = fitz.open()
16
- page = doc.new_page()
17
- page.insert_text((50, 50), "Sample Resume Text")
18
- doc.save(pdf_content)
19
- pdf_content.seek(0)
20
-
21
- extracted_text = parse_resume(pdf_content)
22
- assert "Sample Resume Text" in extracted_text
23
 
 
 
24
  def test_extract_email():
25
- text = "Contact me at test@example.com for details."
26
- assert extract_email(text) == "test@example.com"
27
-
28
- text_no_email = "No email here."
29
- assert extract_email(text_no_email) is None
30
 
31
- @patch("test_module.embedding_model.encode")
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 # Ensure score is within valid range
 
40
 
41
- @patch("test_module.query")
 
42
  def test_summarize_resume(mock_query):
43
- mock_query.return_value = [{"generated_text": "This is a summary."}]
44
- summary = summarize_resume("Long resume text")
45
- assert summary == "This is a summary."
46
-
47
  mock_query.return_value = None
48
- assert summarize_resume("Long resume text") == "Summary could not be generated."
49
-
50
- @patch("test_module.supabase.table")
51
- def test_store_in_supabase(mock_table):
52
- mock_insert = mock_table.return_value.insert.return_value.execute
53
- mock_insert.return_value = {"status": "success"}
54
-
55
- response = store_in_supabase("Resume text", 0.8, "John Doe", "john@example.com", "Summary")
56
- assert response["status"] == "success"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def test_generate_pdf_report():
59
  candidates = [{
60
  "name": "John Doe",
61
  "email": "john@example.com",
62
- "score": 0.9,
63
- "summary": "A skilled developer."
64
  }]
65
- pdf = generate_pdf_report(candidates)
66
  assert isinstance(pdf, BytesIO)
67
- assert pdf.getvalue() # Check that the PDF is not empty
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- shortlisted = sorted(filtered_candidates, key=lambda x: x["score"], reverse=True)[:5]
79
-
80
- return shortlisted if isinstance(shortlisted, list) else [], removed_candidates
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