|
| 1 | +from unittest.mock import Mock |
| 2 | +import contextlib |
1 | 3 | import os |
| 4 | +import shutil |
| 5 | +import unittest |
| 6 | +from dataclasses import dataclass |
2 | 7 | from pathlib import Path |
3 | | -from unittest.mock import Mock |
4 | | - |
5 | | -import pytest |
| 8 | +from typing import Optional |
6 | 9 |
|
7 | 10 | from codeflash.result.create_pr import existing_tests_source_for |
8 | 11 |
|
@@ -71,7 +74,7 @@ def test_single_test_with_improvement(self): |
71 | 74 |
|
72 | 75 | expected = """| Test File::Test Function | Original ⏱️ | Optimized ⏱️ | Speedup | |
73 | 76 | |:------------------------------------------|:--------------|:---------------|:----------| |
74 | | -| `test_module.py::TestClass.test_function` | 1.00ms | 500μs | ✅100% | |
| 77 | +| `test_module.py::TestClass.test_function` | 1.00ms | 500μs | 100%✅ | |
75 | 78 | """ |
76 | 79 |
|
77 | 80 | assert result == expected |
@@ -99,7 +102,7 @@ def test_single_test_with_regression(self): |
99 | 102 |
|
100 | 103 | expected = """| Test File::Test Function | Original ⏱️ | Optimized ⏱️ | Speedup | |
101 | 104 | |:------------------------------------------|:--------------|:---------------|:----------| |
102 | | -| `test_module.py::TestClass.test_function` | 500μs | 1.00ms | ⚠️-50.0% | |
| 105 | +| `test_module.py::TestClass.test_function` | 500μs | 1.00ms | -50.0%⚠️ | |
103 | 106 | """ |
104 | 107 |
|
105 | 108 | assert result == expected |
@@ -132,7 +135,7 @@ def test_test_without_class_name(self): |
132 | 135 |
|
133 | 136 | expected = """| Test File::Test Function | Original ⏱️ | Optimized ⏱️ | Speedup | |
134 | 137 | |:----------------------------------|:--------------|:---------------|:----------| |
135 | | -| `test_module.py::test_standalone` | 1.00ms | 800μs | ✅25.0% | |
| 138 | +| `test_module.py::test_standalone` | 1.00ms | 800μs | 25.0%✅ | |
136 | 139 | """ |
137 | 140 |
|
138 | 141 | assert result == expected |
@@ -218,8 +221,8 @@ def test_multiple_tests_sorted_output(self): |
218 | 221 |
|
219 | 222 | expected = """| Test File::Test Function | Original ⏱️ | Optimized ⏱️ | Speedup | |
220 | 223 | |:-----------------------------------------------------|:--------------|:---------------|:----------| |
221 | | -| `test_another.py::TestAnother.test_another_function` | 2.00ms | 1.50ms | ✅33.3% | |
222 | | -| `test_module.py::TestClass.test_function` | 1.00ms | 800μs | ✅25.0% | |
| 224 | +| `test_another.py::TestAnother.test_another_function` | 2.00ms | 1.50ms | 33.3%✅ | |
| 225 | +| `test_module.py::TestClass.test_function` | 1.00ms | 800μs | 25.0%✅ | |
223 | 226 | """ |
224 | 227 |
|
225 | 228 | assert result == expected |
@@ -247,7 +250,7 @@ def test_multiple_runtimes_uses_minimum(self): |
247 | 250 |
|
248 | 251 | expected = """| Test File::Test Function | Original ⏱️ | Optimized ⏱️ | Speedup | |
249 | 252 | |:------------------------------------------|:--------------|:---------------|:----------| |
250 | | -| `test_module.py::TestClass.test_function` | 800μs | 500μs | ✅60.0% | |
| 253 | +| `test_module.py::TestClass.test_function` | 800μs | 500μs | 60.0%✅ | |
251 | 254 | """ |
252 | 255 |
|
253 | 256 | assert result == expected |
@@ -284,7 +287,7 @@ def test_complex_module_path_conversion(self): |
284 | 287 |
|
285 | 288 | expected = """| Test File::Test Function | Original ⏱️ | Optimized ⏱️ | Speedup | |
286 | 289 | |:------------------------------------------------------------------------|:--------------|:---------------|:----------| |
287 | | -| `integration/test_complex_module.py::TestComplex.test_complex_function` | 1.00ms | 750μs | ✅33.3% | |
| 290 | +| `integration/test_complex_module.py::TestComplex.test_complex_function` | 1.00ms | 750μs | 33.3%✅ | |
288 | 291 | """ |
289 | 292 |
|
290 | 293 | assert result == expected |
@@ -350,9 +353,246 @@ def test_filters_out_generated_tests(self): |
350 | 353 | # Should only include the non-generated test |
351 | 354 | expected = """| Test File::Test Function | Original ⏱️ | Optimized ⏱️ | Speedup | |
352 | 355 | |:------------------------------------------|:--------------|:---------------|:----------| |
353 | | -| `test_module.py::TestClass.test_function` | 1.00ms | 800μs | ✅25.0% | |
| 356 | +| `test_module.py::TestClass.test_function` | 1.00ms | 800μs | 25.0%✅ | |
354 | 357 | """ |
355 | 358 |
|
356 | 359 | assert result == expected |
357 | 360 |
|
358 | | - |
| 361 | +@dataclass(frozen=True) |
| 362 | +class MockInvocationId: |
| 363 | + """Mocks codeflash.models.models.InvocationId""" |
| 364 | + test_module_path: str |
| 365 | + test_function_name: str |
| 366 | + test_class_name: Optional[str] = None |
| 367 | + |
| 368 | + |
| 369 | +@dataclass(frozen=True) |
| 370 | +class MockTestsInFile: |
| 371 | + """Mocks a part of codeflash.models.models.FunctionCalledInTest""" |
| 372 | + test_file: Path |
| 373 | + |
| 374 | + |
| 375 | +@dataclass(frozen=True) |
| 376 | +class MockFunctionCalledInTest: |
| 377 | + """Mocks codeflash.models.models.FunctionCalledInTest""" |
| 378 | + tests_in_file: MockTestsInFile |
| 379 | + |
| 380 | + |
| 381 | +@dataclass(frozen=True) |
| 382 | +class MockTestConfig: |
| 383 | + """Mocks codeflash.verification.verification_utils.TestConfig""" |
| 384 | + tests_root: Path |
| 385 | + |
| 386 | + |
| 387 | +@contextlib.contextmanager |
| 388 | +def temp_project_dir(): |
| 389 | + """A context manager to create and chdir into a temporary project directory.""" |
| 390 | + original_cwd = os.getcwd() |
| 391 | + # Use a unique name to avoid conflicts in /tmp |
| 392 | + project_root = Path(f"/tmp/test_project_{os.getpid()}").resolve() |
| 393 | + try: |
| 394 | + project_root.mkdir(exist_ok=True, parents=True) |
| 395 | + os.chdir(project_root) |
| 396 | + yield project_root |
| 397 | + finally: |
| 398 | + os.chdir(original_cwd) |
| 399 | + shutil.rmtree(project_root, ignore_errors=True) |
| 400 | + |
| 401 | + |
| 402 | +class ExistingTestsSourceForTests(unittest.TestCase): |
| 403 | + def setUp(self): |
| 404 | + self.func_qual_name = "my_module.my_function" |
| 405 | + # A default test_cfg for tests that don't rely on file system. |
| 406 | + self.test_cfg = MockTestConfig(tests_root=Path("/tmp/tests")) |
| 407 | + |
| 408 | + def test_no_tests_for_function(self): |
| 409 | + """Test case where no tests are found for the given function.""" |
| 410 | + existing, replay, concolic = existing_tests_source_for( |
| 411 | + function_qualified_name_with_modules_from_root=self.func_qual_name, |
| 412 | + function_to_tests={}, |
| 413 | + test_cfg=self.test_cfg, |
| 414 | + original_runtimes_all={}, |
| 415 | + optimized_runtimes_all={}, |
| 416 | + ) |
| 417 | + self.assertEqual(existing, "") |
| 418 | + self.assertEqual(replay, "") |
| 419 | + self.assertEqual(concolic, "") |
| 420 | + |
| 421 | + def test_no_runtime_data(self): |
| 422 | + """Test case where tests exist but there is no runtime data.""" |
| 423 | + with temp_project_dir() as project_root: |
| 424 | + tests_dir = project_root / "tests" |
| 425 | + tests_dir.mkdir(exist_ok=True) |
| 426 | + test_file_path = (tests_dir / "test_stuff.py").resolve() |
| 427 | + test_file_path.touch() |
| 428 | + |
| 429 | + test_cfg = MockTestConfig(tests_root=tests_dir.resolve()) |
| 430 | + function_to_tests = { |
| 431 | + self.func_qual_name: { |
| 432 | + MockFunctionCalledInTest( |
| 433 | + tests_in_file=MockTestsInFile(test_file=test_file_path) |
| 434 | + ) |
| 435 | + } |
| 436 | + } |
| 437 | + existing, replay, concolic = existing_tests_source_for( |
| 438 | + function_qualified_name_with_modules_from_root=self.func_qual_name, |
| 439 | + function_to_tests=function_to_tests, |
| 440 | + test_cfg=test_cfg, |
| 441 | + original_runtimes_all={}, |
| 442 | + optimized_runtimes_all={}, |
| 443 | + ) |
| 444 | + self.assertEqual(existing, "") |
| 445 | + self.assertEqual(replay, "") |
| 446 | + self.assertEqual(concolic, "") |
| 447 | + |
| 448 | + def test_with_existing_test_speedup(self): |
| 449 | + """Test with a single existing test that shows a speedup.""" |
| 450 | + with temp_project_dir() as project_root: |
| 451 | + tests_dir = project_root / "tests" |
| 452 | + tests_dir.mkdir(exist_ok=True) |
| 453 | + test_file_path = (tests_dir / "test_existing.py").resolve() |
| 454 | + test_file_path.touch() |
| 455 | + |
| 456 | + test_cfg = MockTestConfig(tests_root=tests_dir.resolve()) |
| 457 | + function_to_tests = { |
| 458 | + self.func_qual_name: { |
| 459 | + MockFunctionCalledInTest( |
| 460 | + tests_in_file=MockTestsInFile(test_file=test_file_path) |
| 461 | + ) |
| 462 | + } |
| 463 | + } |
| 464 | + |
| 465 | + invocation_id = MockInvocationId( |
| 466 | + test_module_path="tests.test_existing", |
| 467 | + test_class_name="TestMyStuff", |
| 468 | + test_function_name="test_one", |
| 469 | + ) |
| 470 | + |
| 471 | + original_runtimes = {invocation_id: [200_000_000]} |
| 472 | + optimized_runtimes = {invocation_id: [100_000_000]} |
| 473 | + |
| 474 | + existing, replay, concolic = existing_tests_source_for( |
| 475 | + function_qualified_name_with_modules_from_root=self.func_qual_name, |
| 476 | + function_to_tests=function_to_tests, |
| 477 | + test_cfg=test_cfg, |
| 478 | + original_runtimes_all=original_runtimes, |
| 479 | + optimized_runtimes_all=optimized_runtimes, |
| 480 | + ) |
| 481 | + |
| 482 | + self.assertIn("| Test File::Test Function", existing) |
| 483 | + self.assertIn("`test_existing.py::TestMyStuff.test_one`", existing) |
| 484 | + self.assertIn("200ms", existing) |
| 485 | + self.assertIn("100ms", existing) |
| 486 | + self.assertIn("100%✅", existing) |
| 487 | + self.assertEqual(replay, "") |
| 488 | + self.assertEqual(concolic, "") |
| 489 | + |
| 490 | + def test_with_replay_and_concolic_tests_slowdown(self): |
| 491 | + """Test with replay and concolic tests showing a slowdown.""" |
| 492 | + with temp_project_dir() as project_root: |
| 493 | + tests_dir = project_root / "tests" |
| 494 | + tests_dir.mkdir(exist_ok=True) |
| 495 | + replay_test_path = (tests_dir / "__replay_test_abc.py").resolve() |
| 496 | + replay_test_path.touch() |
| 497 | + concolic_test_path = (tests_dir / "codeflash_concolic_xyz.py").resolve() |
| 498 | + concolic_test_path.touch() |
| 499 | + |
| 500 | + test_cfg = MockTestConfig(tests_root=tests_dir.resolve()) |
| 501 | + function_to_tests = { |
| 502 | + self.func_qual_name: { |
| 503 | + MockFunctionCalledInTest( |
| 504 | + tests_in_file=MockTestsInFile(test_file=replay_test_path) |
| 505 | + ), |
| 506 | + MockFunctionCalledInTest( |
| 507 | + tests_in_file=MockTestsInFile(test_file=concolic_test_path) |
| 508 | + ), |
| 509 | + } |
| 510 | + } |
| 511 | + |
| 512 | + replay_inv_id = MockInvocationId( |
| 513 | + test_module_path="tests.__replay_test_abc", |
| 514 | + test_function_name="test_replay_one", |
| 515 | + ) |
| 516 | + concolic_inv_id = MockInvocationId( |
| 517 | + test_module_path="tests.codeflash_concolic_xyz", |
| 518 | + test_function_name="test_concolic_one", |
| 519 | + ) |
| 520 | + |
| 521 | + original_runtimes = { |
| 522 | + replay_inv_id: [100_000_000], |
| 523 | + concolic_inv_id: [150_000_000], |
| 524 | + } |
| 525 | + optimized_runtimes = { |
| 526 | + replay_inv_id: [200_000_000], |
| 527 | + concolic_inv_id: [300_000_000], |
| 528 | + } |
| 529 | + |
| 530 | + existing, replay, concolic = existing_tests_source_for( |
| 531 | + function_qualified_name_with_modules_from_root=self.func_qual_name, |
| 532 | + function_to_tests=function_to_tests, |
| 533 | + test_cfg=test_cfg, |
| 534 | + original_runtimes_all=original_runtimes, |
| 535 | + optimized_runtimes_all=optimized_runtimes, |
| 536 | + ) |
| 537 | + |
| 538 | + self.assertEqual(existing, "") |
| 539 | + self.assertIn("`__replay_test_abc.py::test_replay_one`", replay) |
| 540 | + self.assertIn("-50.0%⚠️", replay) |
| 541 | + self.assertIn("`codeflash_concolic_xyz.py::test_concolic_one`", concolic) |
| 542 | + self.assertIn("-50.0%⚠️", concolic) |
| 543 | + |
| 544 | + def test_mixed_results_and_min_runtime(self): |
| 545 | + """Test with mixed results and that min() of runtimes is used.""" |
| 546 | + with temp_project_dir() as project_root: |
| 547 | + tests_dir = project_root / "tests" |
| 548 | + tests_dir.mkdir(exist_ok=True) |
| 549 | + existing_test_path = (tests_dir / "test_existing.py").resolve() |
| 550 | + existing_test_path.touch() |
| 551 | + replay_test_path = (tests_dir / "__replay_test_mixed.py").resolve() |
| 552 | + replay_test_path.touch() |
| 553 | + |
| 554 | + test_cfg = MockTestConfig(tests_root=tests_dir.resolve()) |
| 555 | + function_to_tests = { |
| 556 | + self.func_qual_name: { |
| 557 | + MockFunctionCalledInTest( |
| 558 | + tests_in_file=MockTestsInFile(test_file=existing_test_path) |
| 559 | + ), |
| 560 | + MockFunctionCalledInTest( |
| 561 | + tests_in_file=MockTestsInFile(test_file=replay_test_path) |
| 562 | + ), |
| 563 | + } |
| 564 | + } |
| 565 | + |
| 566 | + existing_inv_id = MockInvocationId( |
| 567 | + "tests.test_existing", "test_speedup", "TestExisting" |
| 568 | + ) |
| 569 | + replay_inv_id = MockInvocationId( |
| 570 | + "tests.__replay_test_mixed", "test_slowdown" |
| 571 | + ) |
| 572 | + |
| 573 | + original_runtimes = { |
| 574 | + existing_inv_id: [400_000_000, 500_000_000], # min is 400ms |
| 575 | + replay_inv_id: [100_000_000, 110_000_000], # min is 100ms |
| 576 | + } |
| 577 | + optimized_runtimes = { |
| 578 | + existing_inv_id: [210_000_000, 200_000_000], # min is 200ms |
| 579 | + replay_inv_id: [300_000_000, 290_000_000], # min is 290ms |
| 580 | + } |
| 581 | + |
| 582 | + existing, replay, concolic = existing_tests_source_for( |
| 583 | + self.func_qual_name, |
| 584 | + function_to_tests, |
| 585 | + test_cfg, |
| 586 | + original_runtimes, |
| 587 | + optimized_runtimes, |
| 588 | + ) |
| 589 | + |
| 590 | + self.assertIn("`test_existing.py::TestExisting.test_speedup`", existing) |
| 591 | + self.assertIn("400ms", existing) |
| 592 | + self.assertIn("200ms", existing) |
| 593 | + self.assertIn("100%✅", existing) |
| 594 | + self.assertIn("`__replay_test_mixed.py::test_slowdown`", replay) |
| 595 | + self.assertIn("100ms", replay) |
| 596 | + self.assertIn("290ms", replay) |
| 597 | + self.assertIn("-65.5%⚠️", replay) |
| 598 | + self.assertEqual(concolic, "") |
0 commit comments