|
1 | 1 | from zoneinfo import ZoneInfo |
2 | 2 |
|
| 3 | +from django.utils import timezone |
3 | 4 | import pytest |
4 | 5 | from django.core import mail |
5 | | -from django.utils.timezone import datetime, timedelta |
| 6 | +from django.utils.timezone import datetime, now, timedelta |
6 | 7 |
|
| 8 | +from grandchallenge.algorithms.models import Job |
7 | 9 | from grandchallenge.challenges.models import ( |
8 | 10 | Challenge, |
9 | 11 | ChallengeRequest, |
|
15 | 17 | update_challenge_compute_costs, |
16 | 18 | update_challenge_results_cache, |
17 | 19 | ) |
18 | | -from grandchallenge.invoices.models import PaymentStatusChoices |
| 20 | +from grandchallenge.invoices.models import ( |
| 21 | + PaymentStatusChoices, |
| 22 | + PaymentTypeChoices, |
| 23 | +) |
| 24 | +from grandchallenge.utilization.models import JobWarmPoolUtilization |
| 25 | +from grandchallenge.utilization.tasks import create_job_warm_pool_utilizations |
| 26 | +from tests.algorithms_tests.factories import AlgorithmJobFactory |
19 | 27 | from tests.evaluation_tests.factories import EvaluationFactory, PhaseFactory |
20 | 28 | from tests.factories import ( |
21 | 29 | ChallengeFactory, |
@@ -444,3 +452,136 @@ def test_challenge_request_draft_reminder_emails( |
444 | 452 | ] |
445 | 453 | assert len(user_emails) == 1 |
446 | 454 | assert requests[i].title in user_emails[0].body |
| 455 | + |
| 456 | + |
| 457 | +@pytest.mark.django_db |
| 458 | +def test_update_challenge_compute_costs_no_utilization(): |
| 459 | + challenge = ChallengeFactory() |
| 460 | + invoice = InvoiceFactory(challenge=challenge) |
| 461 | + |
| 462 | + assert invoice.compute_cost_euro_millicents == 0 |
| 463 | + update_challenge_compute_costs() |
| 464 | + invoice.refresh_from_db() |
| 465 | + assert invoice.compute_cost_euro_millicents == 0 |
| 466 | + |
| 467 | + |
| 468 | +@pytest.mark.django_db |
| 469 | +def test_update_challenge_compute_costs(settings): |
| 470 | + |
| 471 | + settings.COMPONENTS_DEFAULT_BACKEND = ( |
| 472 | + "tests.utilization_tests.test_tasks.UtilizationExecutor" |
| 473 | + ) |
| 474 | + |
| 475 | + challenge = ChallengeFactory() |
| 476 | + invoice = InvoiceFactory(challenge=challenge) |
| 477 | + phase = PhaseFactory(challenge=challenge) |
| 478 | + |
| 479 | + evaluation = EvaluationFactory( |
| 480 | + submission__phase=phase, |
| 481 | + time_limit=60, |
| 482 | + ) |
| 483 | + evaluation_utilization = evaluation.evaluation_utilization |
| 484 | + evaluation_utilization.invoice = invoice |
| 485 | + evaluation_utilization.compute_cost_euro_millicents = 1 |
| 486 | + evaluation_utilization.save() |
| 487 | + |
| 488 | + job = AlgorithmJobFactory( |
| 489 | + status=Job.SUCCESS, |
| 490 | + use_warm_pool=True, |
| 491 | + time_limit=60, |
| 492 | + ) |
| 493 | + |
| 494 | + job_utilization = job.job_utilization |
| 495 | + job_utilization.phase = phase |
| 496 | + job_utilization.challenge = challenge |
| 497 | + job_utilization.invoice = invoice |
| 498 | + job_utilization.compute_cost_euro_millicents = 2 |
| 499 | + job_utilization.save() |
| 500 | + |
| 501 | + job2 = AlgorithmJobFactory( |
| 502 | + status=Job.SUCCESS, |
| 503 | + use_warm_pool=True, |
| 504 | + time_limit=60, |
| 505 | + ) |
| 506 | + |
| 507 | + job2_utilization = job2.job_utilization |
| 508 | + job2_utilization.phase = phase |
| 509 | + job2_utilization.challenge = challenge |
| 510 | + job2_utilization.invoice = invoice |
| 511 | + job2_utilization.compute_cost_euro_millicents = 4 |
| 512 | + job2_utilization.save() |
| 513 | + |
| 514 | + assert not JobWarmPoolUtilization.objects.exists() |
| 515 | + create_job_warm_pool_utilizations() |
| 516 | + for job_warm_pool_utilization in JobWarmPoolUtilization.objects.all(): |
| 517 | + job_warm_pool_utilization.compute_cost_euro_millicents = 8 |
| 518 | + job_warm_pool_utilization.save() |
| 519 | + |
| 520 | + assert invoice.compute_cost_euro_millicents == 0 |
| 521 | + |
| 522 | + update_challenge_compute_costs() |
| 523 | + |
| 524 | + invoice.refresh_from_db() |
| 525 | + assert invoice.compute_cost_euro_millicents == 1 + 2 + 4 + 8 + 8 |
| 526 | + |
| 527 | + |
| 528 | +@pytest.mark.django_db |
| 529 | +def test_update_challenge_compute_costs_expired_utilization(): |
| 530 | + |
| 531 | + challenge = ChallengeFactory() |
| 532 | + |
| 533 | + invoice_last_year = InvoiceFactory( |
| 534 | + challenge=challenge, |
| 535 | + compute_costs_euros=10, |
| 536 | + payment_status=PaymentStatusChoices.PAID, |
| 537 | + payment_type=PaymentTypeChoices.PREPAID, |
| 538 | + expires_on=now().date() - timedelta(days=365), |
| 539 | + ) |
| 540 | + |
| 541 | + invoice_this_year = InvoiceFactory( |
| 542 | + challenge=challenge, |
| 543 | + compute_costs_euros=20, |
| 544 | + payment_status=PaymentStatusChoices.PAID, |
| 545 | + payment_type=PaymentTypeChoices.PREPAID, |
| 546 | + expires_on=now().date(), |
| 547 | + ) |
| 548 | + |
| 549 | + invoice_later_this_year = InvoiceFactory( |
| 550 | + challenge=challenge, |
| 551 | + compute_costs_euros=40, |
| 552 | + payment_status=PaymentStatusChoices.PAID, |
| 553 | + payment_type=PaymentTypeChoices.PREPAID, |
| 554 | + expires_on=now().date() + timedelta(days=365), |
| 555 | + ) |
| 556 | + |
| 557 | + job_last_year = AlgorithmJobFactory( |
| 558 | + status=Job.SUCCESS, |
| 559 | + time_limit=60, |
| 560 | + ) |
| 561 | + job_utilization = job_last_year.job_utilization |
| 562 | + job_utilization.challenge = challenge |
| 563 | + job_utilization.created = now() - timedelta(days=365) |
| 564 | + job_utilization.compute_cost_euro_millicents = ( |
| 565 | + 100 * 1000 * 100 # Note: Huge |
| 566 | + ) |
| 567 | + job_utilization.save() |
| 568 | + |
| 569 | + assert invoice_last_year.compute_cost_euro_millicents == 0 |
| 570 | + assert invoice_this_year.compute_cost_euro_millicents == 0 |
| 571 | + assert invoice_later_this_year.compute_cost_euro_millicents == 0 |
| 572 | + |
| 573 | + update_challenge_compute_costs() |
| 574 | + |
| 575 | + invoice_last_year.refresh_from_db() |
| 576 | + assert ( # Fully utilized |
| 577 | + invoice_last_year.compute_cost_euro_millicents == 10 * 1000 * 100 |
| 578 | + ) |
| 579 | + assert ( # Fully utilized |
| 580 | + invoice_this_year.compute_cost_euro_millicents == 20 * 1000 * 100 |
| 581 | + ) |
| 582 | + assert ( # Utilization from earlier bubble up to this one |
| 583 | + invoice_later_this_year.compute_cost_euro_millicents == 70 * 1000 * 100 |
| 584 | + ) |
| 585 | + |
| 586 | + |
| 587 | +# TODO: add test that checks if we filter on authorized budget when calculating compute costs for invoices |
0 commit comments