|
5 | 5 | import os |
6 | 6 | import re |
7 | 7 | import sys |
| 8 | +import tempfile |
| 9 | +from datetime import datetime, timezone |
8 | 10 | from pathlib import Path |
9 | 11 |
|
| 12 | +import polars as pl |
10 | 13 | import pytest |
11 | 14 |
|
12 | 15 | # Add parent directory to path |
@@ -402,3 +405,162 @@ def test_parse_gres_multiple_sockets(self) -> None: |
402 | 405 | cleaned_gres = re.sub(r"\(S:[0-9-]+\)", "", gres) |
403 | 406 | gpu_parts = cleaned_gres.split(":") |
404 | 407 | assert int(gpu_parts[-1]) == expected_count |
| 408 | + |
| 409 | + |
| 410 | +class TestDatetimeSchemaConsistency: |
| 411 | + """Test datetime schema consistency when loading and concatenating data.""" |
| 412 | + |
| 413 | + def test_load_recent_data_with_mixed_datetime_schemas(self) -> None: |
| 414 | + """Test that _load_recent_data handles mixed datetime schemas correctly.""" |
| 415 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 416 | + # Create config with temp directory |
| 417 | + config = slurm_usage.Config( |
| 418 | + data_dir=Path(tmpdir), |
| 419 | + groups={}, |
| 420 | + user_to_group={}, |
| 421 | + ) |
| 422 | + |
| 423 | + # Create processed subdirectory |
| 424 | + processed_dir = Path(tmpdir) / "processed" |
| 425 | + processed_dir.mkdir(parents=True, exist_ok=True) |
| 426 | + |
| 427 | + # Create test data with different datetime schemas |
| 428 | + # DataFrame 1: UTC timezone-aware datetime |
| 429 | + df1 = pl.DataFrame({ |
| 430 | + "job_id": ["job1", "job2"], |
| 431 | + "user": ["alice", "bob"], |
| 432 | + "cpu_hours_used": [1.0, 2.0], |
| 433 | + "processed_date": [ |
| 434 | + datetime(2025, 9, 20, 10, 0, 0, tzinfo=timezone.utc), |
| 435 | + datetime(2025, 9, 20, 11, 0, 0, tzinfo=timezone.utc), |
| 436 | + ], |
| 437 | + "is_complete": [True, True], |
| 438 | + }) |
| 439 | + |
| 440 | + # DataFrame 2: Naive datetime (no timezone) |
| 441 | + df2 = pl.DataFrame({ |
| 442 | + "job_id": ["job3", "job4"], |
| 443 | + "user": ["charlie", "dave"], |
| 444 | + "cpu_hours_used": [3.0, 4.0], |
| 445 | + "processed_date": [ |
| 446 | + datetime(2025, 9, 21, 10, 0, 0), |
| 447 | + datetime(2025, 9, 21, 11, 0, 0), |
| 448 | + ], |
| 449 | + "is_complete": [True, True], |
| 450 | + }) |
| 451 | + |
| 452 | + # Save DataFrames as parquet files |
| 453 | + df1.write_parquet(processed_dir / "2025-09-20.parquet") |
| 454 | + df2.write_parquet(processed_dir / "2025-09-21.parquet") |
| 455 | + |
| 456 | + # Load data using the function |
| 457 | + result = slurm_usage._load_recent_data(config, days=2) |
| 458 | + |
| 459 | + # Verify the data was loaded and concatenated successfully |
| 460 | + assert result is not None |
| 461 | + assert len(result) == 4 |
| 462 | + assert "job_id" in result.columns |
| 463 | + assert "processed_date" in result.columns |
| 464 | + |
| 465 | + # Check that all datetime columns are now UTC timezone-aware |
| 466 | + date_col_type = result["processed_date"].dtype |
| 467 | + assert isinstance(date_col_type, pl.Datetime) |
| 468 | + assert date_col_type.time_zone == "UTC" |
| 469 | + |
| 470 | + def test_load_recent_data_handles_empty_directory(self) -> None: |
| 471 | + """Test that _load_recent_data handles empty directory gracefully.""" |
| 472 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 473 | + config = slurm_usage.Config( |
| 474 | + data_dir=Path(tmpdir), |
| 475 | + groups={}, |
| 476 | + user_to_group={}, |
| 477 | + ) |
| 478 | + |
| 479 | + processed_dir = Path(tmpdir) / "processed" |
| 480 | + processed_dir.mkdir(parents=True, exist_ok=True) |
| 481 | + |
| 482 | + # Should return None for empty directory |
| 483 | + result = slurm_usage._load_recent_data(config, days=1) |
| 484 | + assert result is None |
| 485 | + |
| 486 | + def test_diagonal_concat_handles_schema_differences(self) -> None: |
| 487 | + """Test that diagonal_relaxed concat handles schema differences gracefully.""" |
| 488 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 489 | + config = slurm_usage.Config( |
| 490 | + data_dir=Path(tmpdir), |
| 491 | + groups={}, |
| 492 | + user_to_group={}, |
| 493 | + ) |
| 494 | + |
| 495 | + processed_dir = Path(tmpdir) / "processed" |
| 496 | + processed_dir.mkdir(parents=True, exist_ok=True) |
| 497 | + |
| 498 | + # DataFrame with different columns |
| 499 | + df1 = pl.DataFrame({ |
| 500 | + "job_id": ["job1"], |
| 501 | + "user": ["alice"], |
| 502 | + "cpu_hours_used": [1.0], |
| 503 | + "processed_date": [datetime(2025, 9, 20, 10, 0, 0)], |
| 504 | + "is_complete": [True], |
| 505 | + }) |
| 506 | + |
| 507 | + # DataFrame with additional column |
| 508 | + df2 = pl.DataFrame({ |
| 509 | + "job_id": ["job2"], |
| 510 | + "user": ["bob"], |
| 511 | + "cpu_hours_used": [2.0], |
| 512 | + "gpu_hours_used": [0.5], # Additional column |
| 513 | + "processed_date": [datetime(2025, 9, 21, 10, 0, 0)], |
| 514 | + "is_complete": [True], |
| 515 | + }) |
| 516 | + |
| 517 | + df1.write_parquet(processed_dir / "2025-09-20.parquet") |
| 518 | + df2.write_parquet(processed_dir / "2025-09-21.parquet") |
| 519 | + |
| 520 | + # Should handle schema differences with diagonal_relaxed |
| 521 | + result = slurm_usage._load_recent_data(config, days=2) |
| 522 | + |
| 523 | + assert result is not None |
| 524 | + assert len(result) == 2 |
| 525 | + # The gpu_hours_used column should exist with null for first row |
| 526 | + assert "gpu_hours_used" in result.columns |
| 527 | + |
| 528 | + def test_multiple_datetime_columns_converted(self) -> None: |
| 529 | + """Test that all datetime columns are converted to UTC.""" |
| 530 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 531 | + config = slurm_usage.Config( |
| 532 | + data_dir=Path(tmpdir), |
| 533 | + groups={}, |
| 534 | + user_to_group={}, |
| 535 | + ) |
| 536 | + |
| 537 | + processed_dir = Path(tmpdir) / "processed" |
| 538 | + processed_dir.mkdir(parents=True, exist_ok=True) |
| 539 | + |
| 540 | + # Use today's date for the filename |
| 541 | + from datetime import date |
| 542 | + today = date.today() |
| 543 | + |
| 544 | + # DataFrame with multiple datetime columns |
| 545 | + df = pl.DataFrame({ |
| 546 | + "job_id": ["job1"], |
| 547 | + "user": ["alice"], |
| 548 | + "submit_time": [datetime(2025, 9, 20, 9, 0, 0)], # Naive |
| 549 | + "start_time": [datetime(2025, 9, 20, 10, 0, 0, tzinfo=timezone.utc)], # UTC |
| 550 | + "end_time": [datetime(2025, 9, 20, 11, 0, 0)], # Naive |
| 551 | + "processed_date": [datetime(2025, 9, 20, 12, 0, 0)], # Naive |
| 552 | + "is_complete": [True], |
| 553 | + }) |
| 554 | + |
| 555 | + df.write_parquet(processed_dir / f"{today}.parquet") |
| 556 | + |
| 557 | + result = slurm_usage._load_recent_data(config, days=1) |
| 558 | + |
| 559 | + assert result is not None |
| 560 | + |
| 561 | + # Check all datetime columns are UTC |
| 562 | + for col in ["submit_time", "start_time", "end_time", "processed_date"]: |
| 563 | + if col in result.columns: |
| 564 | + col_type = result[col].dtype |
| 565 | + if isinstance(col_type, pl.Datetime): |
| 566 | + assert col_type.time_zone == "UTC", f"Column {col} should be UTC" |
0 commit comments