|
4 | 4 |
|
5 | 5 | import datetime |
6 | 6 | from collections.abc import Sequence |
| 7 | +from unittest import mock |
7 | 8 |
|
8 | 9 | from absl.testing import parameterized |
9 | 10 |
|
|
15 | 16 | UnschedulableCleaner, |
16 | 17 | ) |
17 | 18 | from axlearn.cloud.common.quota import QuotaInfo |
18 | | -from axlearn.cloud.common.scheduler import JobMetadata, JobScheduler |
| 19 | +from axlearn.cloud.common.scheduler import BaseScheduler, JobMetadata, JobScheduler, JobVerdict |
19 | 20 | from axlearn.cloud.common.types import JobSpec |
20 | 21 | from axlearn.common.config import REQUIRED, Required, config_class, config_for_function |
21 | 22 |
|
@@ -116,3 +117,99 @@ def mock_jobs(*, prefix: str, resource_count: int): |
116 | 117 | cleaner = cfg.instantiate() |
117 | 118 | result = cleaner.sweep(jobs) |
118 | 119 | self.assertSequenceEqual(result, list(unschedulable_jobs.keys())) |
| 120 | + |
| 121 | + def test_sweep_missing_job_in_verdicts(self): |
| 122 | + """Test that sweep handles the case when job is not in job_verdicts.""" |
| 123 | + # Create a mock schedule result without the job being scheduled |
| 124 | + mock_schedule_result = BaseScheduler.ScheduleResults( |
| 125 | + project_limits={}, |
| 126 | + project_usages={}, |
| 127 | + job_verdicts={}, # empty job verdicts |
| 128 | + unused_limits=None, |
| 129 | + ) |
| 130 | + |
| 131 | + # Create a simple job |
| 132 | + job = new_jobspec( |
| 133 | + name="test_job", |
| 134 | + command="echo hello", |
| 135 | + metadata=JobMetadata( |
| 136 | + user_id="user_1", |
| 137 | + project_id="default", |
| 138 | + creation_time=datetime.datetime(1900, 1, 1, 0, 0, 0), |
| 139 | + resources={"aws_4:p5.48xlarge": 1}, |
| 140 | + ), |
| 141 | + ) |
| 142 | + jobs = {"test_job": job} |
| 143 | + |
| 144 | + # Create cleaner |
| 145 | + quota = QuotaInfo( |
| 146 | + total_resources=[{"aws_4:p5.48xlarge": 10}], |
| 147 | + project_resources={}, |
| 148 | + project_membership={}, |
| 149 | + ) |
| 150 | + cfg = UnschedulableCleaner.default_config().set( |
| 151 | + scheduler=JobScheduler.default_config().set( |
| 152 | + quota=config_for_function(lambda: lambda: quota) |
| 153 | + ) |
| 154 | + ) |
| 155 | + cleaner = cfg.instantiate() |
| 156 | + |
| 157 | + # Patch the scheduler.schedule method to return our mock result |
| 158 | + # and verify the logging |
| 159 | + with self.assertLogs(level="INFO") as log_context: |
| 160 | + with mock.patch.object(JobScheduler, "schedule", return_value=mock_schedule_result): |
| 161 | + result = cleaner.sweep(jobs) |
| 162 | + |
| 163 | + # Should return empty list since "my_job" is not in job_verdicts |
| 164 | + self.assertSequenceEqual(result, []) |
| 165 | + |
| 166 | + # Verify the log message was emitted |
| 167 | + self.assertTrue( |
| 168 | + any( |
| 169 | + "job test_job does not exist in the verdicts, skipping" in log |
| 170 | + for log in log_context.output |
| 171 | + ) |
| 172 | + ) |
| 173 | + |
| 174 | + def test_sweep_job_with_over_limits(self): |
| 175 | + """Test that sweep correctly identifies jobs with over_limits.""" |
| 176 | + # Create a mock schedule result with a verdict that has over_limits |
| 177 | + mock_schedule_result = BaseScheduler.ScheduleResults( |
| 178 | + project_limits={}, |
| 179 | + project_usages={}, |
| 180 | + job_verdicts={"my_job": JobVerdict(over_limits={"aws_4:p5.48xlarge"})}, |
| 181 | + unused_limits=None, |
| 182 | + ) |
| 183 | + |
| 184 | + # Create a simple job |
| 185 | + job = new_jobspec( |
| 186 | + name="test_job", |
| 187 | + command="echo hello", |
| 188 | + metadata=JobMetadata( |
| 189 | + user_id="user_1", |
| 190 | + project_id="default", |
| 191 | + creation_time=datetime.datetime(1900, 1, 1, 0, 0, 0), |
| 192 | + resources={"aws_4:p5.48xlarge": 100}, |
| 193 | + ), |
| 194 | + ) |
| 195 | + jobs = {"test_job": job} |
| 196 | + |
| 197 | + # Create cleaner |
| 198 | + quota = QuotaInfo( |
| 199 | + total_resources=[{"aws_4:p5.48xlarge": 10}], |
| 200 | + project_resources={}, |
| 201 | + project_membership={}, |
| 202 | + ) |
| 203 | + cfg = UnschedulableCleaner.default_config().set( |
| 204 | + scheduler=JobScheduler.default_config().set( |
| 205 | + quota=config_for_function(lambda: lambda: quota) |
| 206 | + ) |
| 207 | + ) |
| 208 | + cleaner = cfg.instantiate() |
| 209 | + |
| 210 | + # Patch the scheduler.schedule method to return our mock result |
| 211 | + with mock.patch.object(JobScheduler, "schedule", return_value=mock_schedule_result): |
| 212 | + result = cleaner.sweep(jobs) |
| 213 | + |
| 214 | + # Should return the job since it has over_limits |
| 215 | + self.assertSequenceEqual(result, ["test_job"]) |
0 commit comments