forked from JesperDramsch/python-deadlines
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_interactive_merge.py
More file actions
556 lines (478 loc) · 19.6 KB
/
Copy pathtest_interactive_merge.py
File metadata and controls
556 lines (478 loc) · 19.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
"""Tests for interactive merge functionality."""
import sys
from io import StringIO
from pathlib import Path
from unittest.mock import patch
import pandas as pd
import pytest
sys.path.append(str(Path(__file__).parent.parent / "utils"))
from tidy_conf.interactive_merge import fuzzy_match
from tidy_conf.interactive_merge import merge_conferences
@pytest.fixture()
def mock_title_mappings():
"""Mock the title mappings to avoid file I/O issues.
The fuzzy_match function calls load_title_mappings from multiple locations:
- tidy_conf.interactive_merge.load_title_mappings
- tidy_conf.titles.load_title_mappings (via tidy_df_names)
It also calls update_title_mappings which writes to files.
We need to mock all of these to avoid file system operations.
"""
with patch("tidy_conf.interactive_merge.load_title_mappings") as mock_load1, patch(
"tidy_conf.titles.load_title_mappings",
) as mock_load2, patch(
"tidy_conf.interactive_merge.update_title_mappings",
) as mock_update:
# Return empty mappings (list, dict) for both load calls
mock_load1.return_value = ([], {})
mock_load2.return_value = ([], {})
mock_update.return_value = None
yield mock_load1
class TestFuzzyMatch:
"""Test fuzzy matching functionality."""
def test_fuzzy_match_identical_names(self, mock_title_mappings):
"""Test fuzzy matching with identical conference names."""
df_yml = pd.DataFrame(
{
"conference": ["PyCon Test"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://existing.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
df_csv = pd.DataFrame(
{
"conference": ["PyCon Test"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://new.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
merged, _remote, _report = fuzzy_match(df_yml, df_csv)
# Should find a match and merge the data
assert not merged.empty
assert len(merged) == 1
assert merged.iloc[0]["conference"] == "PyCon Test"
def test_fuzzy_match_similar_names(self, mock_title_mappings):
"""Test fuzzy matching with similar but not identical names."""
df_yml = pd.DataFrame(
{
"conference": ["PyCon US"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://existing.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
df_csv = pd.DataFrame(
{
"conference": ["PyCon United States"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://new.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
with patch(
"builtins.input",
return_value="y",
): # Simulate user accepting the match
merged, remote, _report = fuzzy_match(df_yml, df_csv)
# Should find and accept a fuzzy match
assert not merged.empty
# Verify the merged dataframe has conference data
conference_names = merged["conference"].tolist()
# Note: title mappings may transform names (e.g., "PyCon US" -> "PyCon USA")
# Check that we have at least one conference in the result
assert len(conference_names) >= 1, "Should have at least one conference in result"
# Verify fuzzy matching was attempted - remote should still be returned
assert remote is not None, "Remote dataframe should be returned for further processing"
def test_fuzzy_match_no_matches(self, mock_title_mappings):
"""Test fuzzy matching when there are no matches."""
df_yml = pd.DataFrame(
{
"conference": ["PyCon Test"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://existing.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
df_csv = pd.DataFrame(
{
"conference": ["DjangoCon Completely Different"],
"year": [2025],
"cfp": ["2025-03-15 23:59:00"],
"link": ["https://different.com"],
"place": ["Different City"],
"start": ["2025-07-01"],
"end": ["2025-07-03"],
},
)
merged, remote, _report = fuzzy_match(df_yml, df_csv)
# Both dataframes should be non-empty after fuzzy_match
assert not merged.empty, "Merged dataframe should not be empty"
assert not remote.empty, "Remote dataframe should be returned"
# Verify the YML conference is preserved in merged result
conference_names = merged["conference"].tolist()
assert "PyCon Test" in conference_names, f"YML conference 'PyCon Test' should be in {conference_names}"
# Verify the dissimilar CSV conference remains in remote (unmatched)
remote_names = remote["conference"].tolist()
assert (
"DjangoCon Completely Different" in remote_names
), f"Unmatched CSV conference should be in remote: {remote_names}"
# Verify the dissimilar conferences weren't incorrectly merged
# The YML row should still have its original link (not overwritten by CSV)
yml_rows = merged[merged["conference"] == "PyCon Test"]
assert not yml_rows.empty, "YML conference should exist in merged"
assert (
yml_rows.iloc[0]["link"] == "https://existing.com"
), "YML link should not be changed when no match is found"
class TestMergeConferences:
"""Test conference merging functionality."""
def test_merge_conferences_after_fuzzy_match(self, mock_title_mappings):
"""Test conference merging using output from fuzzy_match.
This test verifies that conference names are preserved through the merge.
"""
df_yml = pd.DataFrame(
{
"conference": ["PyCon Test"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://existing.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
df_remote = pd.DataFrame(
{
"conference": ["DjangoCon"],
"year": [2025],
"cfp": ["2025-03-15 23:59:00"],
"link": ["https://django.com"],
"place": ["Django City"],
"start": ["2025-08-01"],
"end": ["2025-08-03"],
},
)
# First do fuzzy match to set up data properly
with patch("builtins.input", return_value="n"): # Reject any fuzzy matches
df_merged, df_remote_processed, _ = fuzzy_match(df_yml, df_remote)
# Then test merge_conferences
with patch("sys.stdin", StringIO("")):
result = merge_conferences(df_merged, df_remote_processed)
# Should combine both DataFrames - we expect exactly 2 conferences
assert isinstance(result, pd.DataFrame)
assert len(result) == 2, f"Expected 2 conferences (1 merged + 1 remote), got {len(result)}"
# Verify conference names are preserved correctly (not corrupted to index values)
assert "conference" in result.columns
conference_names = result["conference"].tolist()
# Names should be actual conference names, not index values like "0"
for name in conference_names:
assert not str(
name,
).isdigit(), f"Conference name '{name}' is corrupted to index value"
assert "PyCon Test" in conference_names, "Original YML conference should be in result"
assert "DjangoCon" in conference_names, "Remote conference should be in result"
def test_merge_conferences_preserves_names(self, mock_title_mappings):
"""Test that merge preserves conference names correctly."""
df_yml = pd.DataFrame(
{
"conference": ["Original Conference Name"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://original.com"],
"place": ["Original City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
"sub": ["PY"],
},
)
df_remote = pd.DataFrame(
{
"conference": ["Remote Conference Name"],
"year": [2025],
"cfp": ["2025-03-15 23:59:00"],
"link": ["https://remote.com"],
"place": ["Remote City"],
"start": ["2025-08-01"],
"end": ["2025-08-03"],
"sub": ["PY"],
},
)
# Mock user input to reject matches
with patch("builtins.input", return_value="n"):
df_merged, df_remote_processed, _ = fuzzy_match(df_yml, df_remote)
with patch("sys.stdin", StringIO("")), patch(
"tidy_conf.schema.get_schema",
) as mock_schema:
# Mock schema with empty DataFrame
empty_schema = pd.DataFrame(
columns=[
"conference",
"year",
"cfp",
"link",
"place",
"start",
"end",
"sub",
],
)
mock_schema.return_value = empty_schema
result = merge_conferences(df_merged, df_remote_processed)
# Basic validation that we get some result
assert isinstance(result, pd.DataFrame)
assert "conference" in result.columns
def test_merge_conferences_empty_dataframes(self, mock_title_mappings):
"""Test merging with empty DataFrames."""
df_empty = pd.DataFrame(
columns=[
"conference",
"year",
"cfp",
"link",
"place",
"start",
"end",
"sub",
],
)
df_with_data = pd.DataFrame(
{
"conference": ["Test Conference"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://test.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
"sub": ["PY"],
},
)
# Test with empty remote - fuzzy_match should handle empty DataFrames gracefully
with patch("builtins.input", return_value="n"):
df_merged, df_remote_processed, _ = fuzzy_match(df_with_data, df_empty)
with patch("sys.stdin", StringIO("")), patch(
"tidy_conf.schema.get_schema",
) as mock_schema:
# Mock schema
empty_schema = pd.DataFrame(
columns=[
"conference",
"year",
"cfp",
"link",
"place",
"start",
"end",
"sub",
],
)
mock_schema.return_value = empty_schema
result = merge_conferences(df_merged, df_remote_processed)
assert isinstance(result, pd.DataFrame)
assert "conference" in result.columns
class TestInteractivePrompts:
"""Test interactive prompt functionality."""
def test_interactive_user_input_yes(self, mock_title_mappings):
"""Test interactive prompts with 'yes' response."""
df_yml = pd.DataFrame(
{
"conference": ["PyCon Similar"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://existing.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
df_csv = pd.DataFrame(
{
"conference": ["PyCon Slightly Different"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://new.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
# Mock user input to accept match
with patch("builtins.input", return_value="y"):
merged, _remote, _ = fuzzy_match(df_yml, df_csv)
# Should accept the match
assert not merged.empty
def test_interactive_user_input_no(self, mock_title_mappings):
"""Test interactive prompts with 'no' response."""
df_yml = pd.DataFrame(
{
"conference": ["PyCon Similar"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://existing.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
df_csv = pd.DataFrame(
{
"conference": ["PyCon Slightly Different"],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://new.com"],
"place": ["Test City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
},
)
# Mock user input to reject match
with patch("builtins.input", return_value="n"):
_merged, remote, _ = fuzzy_match(df_yml, df_csv)
# Should reject the match and keep data separate
assert len(remote) == 1, f"Expected exactly 1 rejected conference in remote, got {len(remote)}"
assert remote.iloc[0]["conference"] == "PyCon Slightly Different"
class TestDataIntegrity:
"""Test data integrity during merge operations."""
def test_conference_name_corruption_prevention(self, mock_title_mappings):
"""Test prevention of conference name corruption bug.
This test specifically targets a bug where conference names were being
set to pandas index values (e.g., "0", "1") instead of actual names.
The test verifies that original conference names are preserved through
the merge process.
"""
# Use distinctive names that can't be confused with index values
original_name = "Important Conference With Specific Name"
remote_name = "Another Important Conference With Unique Name"
df_yml = pd.DataFrame(
{
"conference": [original_name],
"year": [2025],
"cfp": ["2025-02-15 23:59:00"],
"link": ["https://important.com"],
"place": ["Important City"],
"start": ["2025-06-01"],
"end": ["2025-06-03"],
"sub": ["PY"],
},
)
df_remote = pd.DataFrame(
{
"conference": [remote_name],
"year": [2025],
"cfp": ["2025-03-15 23:59:00"],
"link": ["https://another.com"],
"place": ["Another City"],
"start": ["2025-08-01"],
"end": ["2025-08-03"],
"sub": ["PY"],
},
)
# First do fuzzy match to set up data properly
with patch("builtins.input", return_value="n"):
df_merged, df_remote_processed, _ = fuzzy_match(df_yml, df_remote)
with patch("sys.stdin", StringIO("")), patch(
"tidy_conf.schema.get_schema",
) as mock_schema:
# Mock schema
empty_schema = pd.DataFrame(
columns=[
"conference",
"year",
"cfp",
"link",
"place",
"start",
"end",
"sub",
],
)
mock_schema.return_value = empty_schema
result = merge_conferences(df_merged, df_remote_processed)
# Verify we got a valid result
assert isinstance(result, pd.DataFrame)
assert "conference" in result.columns
assert len(result) > 0, "Expected at least one conference in result"
# CRITICAL: Verify conference names are actual names, not index values
conference_names = result["conference"].tolist()
for name in conference_names:
# Names should not be numeric strings (the corruption bug)
assert not str(
name,
).isdigit(), f"Conference name '{name}' appears to be a numeric index value"
# Names should be reasonable strings (not just numbers)
assert len(str(name)) > 2, f"Conference name '{name}' is too short, likely corrupted"
# Verify the expected conference names are present (at least one should be)
expected_names = {original_name, remote_name}
actual_names = set(conference_names)
assert actual_names & expected_names, f"Expected at least one of {expected_names} but got {actual_names}"
def test_data_consistency_after_merge(self, mock_title_mappings):
"""Test that data remains consistent after merge operations."""
original_data = {
"conference": "Test Conference",
"year": 2025,
"cfp": "2025-02-15 23:59:00",
"link": "https://test.com",
"place": "Test City",
"start": "2025-06-01",
"end": "2025-06-03",
"sub": "PY",
}
df_yml = pd.DataFrame([original_data])
df_remote = pd.DataFrame(
columns=[
"conference",
"year",
"cfp",
"link",
"place",
"start",
"end",
"sub",
],
) # Empty remote
# First do fuzzy match
with patch("builtins.input", return_value="n"):
df_merged, df_remote_processed, _ = fuzzy_match(df_yml, df_remote)
with patch("sys.stdin", StringIO("")), patch(
"tidy_conf.schema.get_schema",
) as mock_schema:
# Mock schema
empty_schema = pd.DataFrame(
columns=[
"conference",
"year",
"cfp",
"link",
"place",
"start",
"end",
"sub",
],
)
mock_schema.return_value = empty_schema
result = merge_conferences(df_merged, df_remote_processed)
# Verify the result is valid
assert isinstance(result, pd.DataFrame)
assert "conference" in result.columns
# Verify original data was preserved through the merge
if len(result) > 0:
# Check that original conference name appears in result
conference_names = result["conference"].tolist()
assert (
original_data["conference"] in conference_names
), f"Original conference '{original_data['conference']}' not found in result: {conference_names}"