blob: c4fa8709284ae3815319431a6db29c22ba30efc0 [file] [log] [blame]
J. Richard Barnetteea785362014-03-17 16:00:53 -07001# Copyright 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import Queue
6import datetime
7import logging
8import os
9import shutil
J. Richard Barnette2e443ef2014-05-20 12:31:35 -070010import signal
J. Richard Barnetteea785362014-03-17 16:00:53 -070011import sys
12import tempfile
13import time
14import unittest
15
16import mox
17
18import common
19import gs_offloader
20import job_directories
21
Simran Basidd129972014-09-11 14:34:49 -070022from autotest_lib.client.common_lib import utils, time_utils
23from autotest_lib.client.common_lib import global_config
J. Richard Barnetteea785362014-03-17 16:00:53 -070024from autotest_lib.scheduler import email_manager
25
Jakob Juelich24f22c22014-09-26 11:46:11 -070026
J. Richard Barnetteea785362014-03-17 16:00:53 -070027# Test value to use for `days_old`, if nothing else is required.
28_TEST_EXPIRATION_AGE = 7
29
30# When constructing sample time values for testing expiration,
31# allow this many seconds between the expiration time and the
32# current time.
33_MARGIN_SECS = 10.0
34
35
36def _get_options(argv):
37 """Helper function to exercise command line parsing.
38
39 @param argv Value of sys.argv to be parsed.
40
41 """
42 sys.argv = ['bogus.py'] + argv
43 return gs_offloader.parse_options()
44
45
Simran Basidd129972014-09-11 14:34:49 -070046class OffloaderOptionsTests(mox.MoxTestBase):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070047 """Tests for the `Offloader` constructor.
48
49 Tests that offloader instance fields are set as expected
50 for given command line options.
51
52 """
53
54 _REGULAR_ONLY = set([job_directories.RegularJobDirectory])
55 _SPECIAL_ONLY = set([job_directories.SpecialJobDirectory])
56 _BOTH = _REGULAR_ONLY | _SPECIAL_ONLY
J. Richard Barnetteea785362014-03-17 16:00:53 -070057
Jakob Juelich24f22c22014-09-26 11:46:11 -070058
Simran Basidd129972014-09-11 14:34:49 -070059 def setUp(self):
60 super(OffloaderOptionsTests, self).setUp()
61 self.mox.StubOutWithMock(utils, 'get_offload_gsuri')
Simran Basif3e305f2014-10-03 14:43:53 -070062 gs_offloader.GS_OFFLOADING_ENABLED = True
Simran Basidd129972014-09-11 14:34:49 -070063
Jakob Juelich24f22c22014-09-26 11:46:11 -070064
Simran Basidd129972014-09-11 14:34:49 -070065 def _mock_get_offload_func(self, is_moblab):
66 """Mock the process of getting the offload_dir function."""
67 if is_moblab:
68 expected_gsuri = '%sresults/%s/%s/' % (
69 global_config.global_config.get_config_value(
70 'CROS', 'image_storage_server'),
71 'Fa:ke:ma:c0:12:34', 'rand0m-uu1d')
72 else:
73 expected_gsuri = utils.DEFAULT_OFFLOAD_GSURI
74 utils.get_offload_gsuri().AndReturn(expected_gsuri)
75 offload_func = gs_offloader.get_offload_dir_func(expected_gsuri)
76 self.mox.StubOutWithMock(gs_offloader, 'get_offload_dir_func')
77 gs_offloader.get_offload_dir_func(expected_gsuri).AndReturn(
78 offload_func)
79 self.mox.ReplayAll()
80 return offload_func
81
Jakob Juelich24f22c22014-09-26 11:46:11 -070082
J. Richard Barnetteea785362014-03-17 16:00:53 -070083 def test_process_no_options(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070084 """Test default offloader options."""
Simran Basidd129972014-09-11 14:34:49 -070085 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070086 offloader = gs_offloader.Offloader(_get_options([]))
87 self.assertEqual(set(offloader._jobdir_classes),
88 self._REGULAR_ONLY)
89 self.assertEqual(offloader._processes, 1)
90 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -070091 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070092 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -070093
Jakob Juelich24f22c22014-09-26 11:46:11 -070094
J. Richard Barnetteea785362014-03-17 16:00:53 -070095 def test_process_all_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070096 """Test offloader handling for the --all option."""
Simran Basidd129972014-09-11 14:34:49 -070097 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070098 offloader = gs_offloader.Offloader(_get_options(['--all']))
99 self.assertEqual(set(offloader._jobdir_classes), self._BOTH)
100 self.assertEqual(offloader._processes, 1)
101 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700102 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700103 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700104
Jakob Juelich24f22c22014-09-26 11:46:11 -0700105
J. Richard Barnetteea785362014-03-17 16:00:53 -0700106 def test_process_hosts_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700107 """Test offloader handling for the --hosts option."""
Simran Basidd129972014-09-11 14:34:49 -0700108 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700109 offloader = gs_offloader.Offloader(
110 _get_options(['--hosts']))
111 self.assertEqual(set(offloader._jobdir_classes),
112 self._SPECIAL_ONLY)
113 self.assertEqual(offloader._processes, 1)
114 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700115 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700116 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700117
Jakob Juelich24f22c22014-09-26 11:46:11 -0700118
J. Richard Barnetteea785362014-03-17 16:00:53 -0700119 def test_parallelism_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700120 """Test offloader handling for the --parallelism option."""
Simran Basidd129972014-09-11 14:34:49 -0700121 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700122 offloader = gs_offloader.Offloader(
123 _get_options(['--parallelism', '2']))
124 self.assertEqual(set(offloader._jobdir_classes),
125 self._REGULAR_ONLY)
126 self.assertEqual(offloader._processes, 2)
127 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700128 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700129 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700130
Jakob Juelich24f22c22014-09-26 11:46:11 -0700131
J. Richard Barnetteea785362014-03-17 16:00:53 -0700132 def test_delete_only_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700133 """Test offloader handling for the --delete_only option."""
134 offloader = gs_offloader.Offloader(
135 _get_options(['--delete_only']))
136 self.assertEqual(set(offloader._jobdir_classes),
137 self._REGULAR_ONLY)
138 self.assertEqual(offloader._processes, 1)
139 self.assertEqual(offloader._offload_func,
140 gs_offloader.delete_files)
141 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700142
Jakob Juelich24f22c22014-09-26 11:46:11 -0700143
Simran Basidf4751e2014-10-10 14:19:22 -0700144 def test_days_old_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700145 """Test offloader handling for the --days_old option."""
Simran Basidd129972014-09-11 14:34:49 -0700146 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700147 offloader = gs_offloader.Offloader(
148 _get_options(['--days_old', '7']))
149 self.assertEqual(set(offloader._jobdir_classes),
150 self._REGULAR_ONLY)
151 self.assertEqual(offloader._processes, 1)
152 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700153 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700154 self.assertEqual(offloader._age_limit, 7)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700155
Jakob Juelich24f22c22014-09-26 11:46:11 -0700156
Simran Basidd129972014-09-11 14:34:49 -0700157 def test_moblab_gsuri_generation(self):
158 """Test offloader construction for Moblab."""
159 offload_func = self._mock_get_offload_func(True)
160 offloader = gs_offloader.Offloader(_get_options([]))
161 self.assertEqual(set(offloader._jobdir_classes),
162 self._REGULAR_ONLY)
163 self.assertEqual(offloader._processes, 1)
164 self.assertEqual(offloader._offload_func,
165 offload_func)
166 self.assertEqual(offloader._age_limit, 0)
167
J. Richard Barnetteea785362014-03-17 16:00:53 -0700168
Simran Basif3e305f2014-10-03 14:43:53 -0700169 def test_globalconfig_offloading_flag(self):
170 """Test enabling of --delete_only via global_config."""
171 gs_offloader.GS_OFFLOADING_ENABLED = False
172 offloader = gs_offloader.Offloader(
173 _get_options([]))
174 self.assertEqual(offloader._offload_func,
175 gs_offloader.delete_files)
176
177
J. Richard Barnetteea785362014-03-17 16:00:53 -0700178def _make_timestamp(age_limit, is_expired):
179 """Create a timestamp for use by `job_directories._is_job_expired()`.
180
181 The timestamp will meet the syntactic requirements for
182 timestamps used as input to `_is_job_expired()`. If
183 `is_expired` is true, the timestamp will be older than
184 `age_limit` days before the current time; otherwise, the
185 date will be younger.
186
187 @param age_limit The number of days before expiration of the
188 target timestamp.
189 @param is_expired Whether the timestamp should be expired
190 relative to `age_limit`.
191
192 """
193 seconds = -_MARGIN_SECS
194 if is_expired:
195 seconds = -seconds
196 delta = datetime.timedelta(days=age_limit, seconds=seconds)
197 reference_time = datetime.datetime.now() - delta
Dan Shidfea3682014-08-10 23:38:40 -0700198 return reference_time.strftime(time_utils.TIME_FMT)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700199
200
201class JobExpirationTests(unittest.TestCase):
202 """Tests to exercise `job_directories._is_job_expired()`."""
203
204 def test_expired(self):
205 """Test detection of an expired job."""
206 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, True)
207 self.assertTrue(
208 job_directories._is_job_expired(
209 _TEST_EXPIRATION_AGE, timestamp))
210
211
212 def test_alive(self):
213 """Test detection of a job that's not expired."""
214 # N.B. This test may fail if its run time exceeds more than
215 # about _MARGIN_SECS seconds.
216 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, False)
217 self.assertFalse(
218 job_directories._is_job_expired(
219 _TEST_EXPIRATION_AGE, timestamp))
220
221
222class _MockJobDirectory(job_directories._JobDirectory):
223 """Subclass of `_JobDirectory` used as a helper for tests."""
224
225 GLOB_PATTERN = '[0-9]*-*'
226
Jakob Juelich24f22c22014-09-26 11:46:11 -0700227
J. Richard Barnetteea785362014-03-17 16:00:53 -0700228 def __init__(self, resultsdir):
229 """Create new job in initial state."""
230 super(_MockJobDirectory, self).__init__(resultsdir)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700231 self._timestamp = None
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700232 self.queue_args = [resultsdir, os.path.dirname(resultsdir)]
J. Richard Barnetteea785362014-03-17 16:00:53 -0700233
Jakob Juelich24f22c22014-09-26 11:46:11 -0700234
J. Richard Barnetteea785362014-03-17 16:00:53 -0700235 def get_timestamp_if_finished(self):
236 return self._timestamp
237
Jakob Juelich24f22c22014-09-26 11:46:11 -0700238
J. Richard Barnetteea785362014-03-17 16:00:53 -0700239 def set_finished(self, days_old):
240 """Make this job appear to be finished.
241
242 After calling this function, calls to `enqueue_offload()`
243 will find this job as finished, but not expired and ready
244 for offload. Note that when `days_old` is 0,
245 `enqueue_offload()` will treat a finished job as eligible
246 for offload.
247
248 @param days_old The value of the `days_old` parameter that
249 will be passed to `enqueue_offload()` for
250 testing.
251
252 """
253 self._timestamp = _make_timestamp(days_old, False)
254
Jakob Juelich24f22c22014-09-26 11:46:11 -0700255
J. Richard Barnetteea785362014-03-17 16:00:53 -0700256 def set_expired(self, days_old):
257 """Make this job eligible to be offloaded.
258
259 After calling this function, calls to `offload` will attempt
260 to offload this job.
261
262 @param days_old The value of the `days_old` parameter that
263 will be passed to `enqueue_offload()` for
264 testing.
265
266 """
267 self._timestamp = _make_timestamp(days_old, True)
268
Jakob Juelich24f22c22014-09-26 11:46:11 -0700269
J. Richard Barnetteea785362014-03-17 16:00:53 -0700270 def set_incomplete(self):
271 """Make this job appear to have failed offload just once."""
272 self._offload_count += 1
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700273 self._first_offload_start = time.time()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700274 if not os.path.isdir(self._dirname):
275 os.mkdir(self._dirname)
276
Jakob Juelich24f22c22014-09-26 11:46:11 -0700277
J. Richard Barnetteea785362014-03-17 16:00:53 -0700278 def set_reportable(self):
279 """Make this job be reportable."""
J. Richard Barnetteea785362014-03-17 16:00:53 -0700280 self.set_incomplete()
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700281 self._offload_count += 1
J. Richard Barnetteea785362014-03-17 16:00:53 -0700282
Jakob Juelich24f22c22014-09-26 11:46:11 -0700283
J. Richard Barnetteea785362014-03-17 16:00:53 -0700284 def set_complete(self):
285 """Make this job be completed."""
286 self._offload_count += 1
287 if os.path.isdir(self._dirname):
288 os.rmdir(self._dirname)
289
290
Simran Basi1e10e922015-04-16 15:09:56 -0700291 def process_gs_instructions(self):
292 """Always still offload the job directory."""
293 return True
294
295
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700296class CommandListTests(unittest.TestCase):
297 """Tests for `get_cmd_list()`."""
298
Jakob Juelich24f22c22014-09-26 11:46:11 -0700299 def _command_list_assertions(self, job, use_rsync=True):
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700300 """Call `get_cmd_list()` and check the return value.
301
302 Check the following assertions:
303 * The command name (argv[0]) is 'gsutil'.
304 * The arguments contain the 'cp' subcommand.
305 * The next-to-last argument (the source directory) is the
306 job's `queue_args[0]`.
Simran Basidd129972014-09-11 14:34:49 -0700307 * The last argument (the destination URL) is the job's
308 'queue_args[1]'.
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700309
310 @param job A job with properly calculated arguments to
311 `get_cmd_list()`
312
313 """
Jakob Juelich24f22c22014-09-26 11:46:11 -0700314 test_bucket_uri = 'gs://a-test-bucket'
315
316 gs_offloader.USE_RSYNC_ENABLED = use_rsync
317
318 command = gs_offloader.get_cmd_list(
319 job.queue_args[0],
320 os.path.join(test_bucket_uri, job.queue_args[1]))
321
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700322 self.assertEqual(command[0], 'gsutil')
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700323 self.assertEqual(command[-2], job.queue_args[0])
Jakob Juelich24f22c22014-09-26 11:46:11 -0700324
325 if use_rsync:
326 self.assertTrue('rsync' in command)
327 self.assertEqual(command[-1],
328 os.path.join(test_bucket_uri, job.queue_args[0]))
329 else:
330 self.assertTrue('cp' in command)
331 self.assertEqual(command[-1],
332 os.path.join(test_bucket_uri, job.queue_args[1]))
333
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700334
335 def test_get_cmd_list_regular(self):
336 """Test `get_cmd_list()` as for a regular job."""
337 job = _MockJobDirectory('118-debug')
338 self._command_list_assertions(job)
339
Jakob Juelich24f22c22014-09-26 11:46:11 -0700340
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700341 def test_get_cmd_list_special(self):
342 """Test `get_cmd_list()` as for a special job."""
343 job = _MockJobDirectory('hosts/host1/118-reset')
344 self._command_list_assertions(job)
345
346
Jakob Juelich24f22c22014-09-26 11:46:11 -0700347 def test_get_cmd_list_regular_no_rsync(self):
348 """Test `get_cmd_list()` as for a regular job."""
349 job = _MockJobDirectory('118-debug')
350 self._command_list_assertions(job, use_rsync=False)
351
352
353 def test_get_cmd_list_special_no_rsync(self):
354 """Test `get_cmd_list()` as for a special job."""
355 job = _MockJobDirectory('hosts/host1/118-reset')
356 self._command_list_assertions(job, use_rsync=False)
357
358
J. Richard Barnetteea785362014-03-17 16:00:53 -0700359# Below is partial sample of e-mail notification text. This text is
360# deliberately hard-coded and then parsed to create the test data;
361# the idea is to make sure the actual text format will be reviewed
362# by a human being.
363#
364# first offload count directory
365# --+----1----+---- ----+ ----+----1----+----2----+----3
366_SAMPLE_DIRECTORIES_REPORT = '''\
367=================== ====== ==============================
3682014-03-14 15:09:26 1 118-fubar
3692014-03-14 15:19:23 2 117-fubar
3702014-03-14 15:29:20 6 116-fubar
3712014-03-14 15:39:17 24 115-fubar
3722014-03-14 15:49:14 120 114-fubar
3732014-03-14 15:59:11 720 113-fubar
3742014-03-14 16:09:08 5040 112-fubar
3752014-03-14 16:19:05 40320 111-fubar
376'''
377
378
379class EmailTemplateTests(mox.MoxTestBase):
380 """Test the formatting of e-mail notifications."""
381
382 def setUp(self):
383 super(EmailTemplateTests, self).setUp()
384 self.mox.StubOutWithMock(email_manager.manager,
385 'send_email')
386 self._joblist = []
387 for line in _SAMPLE_DIRECTORIES_REPORT.split('\n')[1 : -1]:
388 date_, time_, count, dir_ = line.split()
389 job = _MockJobDirectory(dir_)
390 job._offload_count = int(count)
Dan Shidfea3682014-08-10 23:38:40 -0700391 timestruct = time.strptime("%s %s" % (date_, time_),
392 gs_offloader.ERROR_EMAIL_TIME_FORMAT)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700393 job._first_offload_start = time.mktime(timestruct)
394 # enter the jobs in reverse order, to make sure we
395 # test that the output will be sorted.
396 self._joblist.insert(0, job)
397
Jakob Juelich24f22c22014-09-26 11:46:11 -0700398
J. Richard Barnetteea785362014-03-17 16:00:53 -0700399 def test_email_template(self):
400 """Trigger an e-mail report and check its contents."""
401 # The last line of the report is a separator that we
402 # repeat in the first line of our expected result data.
403 # So, we remove that separator from the end of the of
404 # the e-mail report message.
405 #
406 # The last element in the list returned by split('\n')
407 # will be an empty string, so to remove the separator,
408 # we remove the next-to-last entry in the list.
409 report_lines = gs_offloader.ERROR_EMAIL_REPORT_FORMAT.split('\n')
410 expected_message = ('\n'.join(report_lines[: -2] +
411 report_lines[-1 :]) +
412 _SAMPLE_DIRECTORIES_REPORT)
413 email_manager.manager.send_email(
414 mox.IgnoreArg(), mox.IgnoreArg(), expected_message)
415 self.mox.ReplayAll()
416 gs_offloader.report_offload_failures(self._joblist)
417
418
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700419class _MockJob(object):
420 """Class to mock the return value of `AFE.get_jobs()`."""
421 def __init__(self, created):
422 self.created_on = created
423
424
425class _MockHostQueueEntry(object):
426 """Class to mock the return value of `AFE.get_host_queue_entries()`."""
427 def __init__(self, finished):
428 self.finished_on = finished
429
430
431class _MockSpecialTask(object):
432 """Class to mock the return value of `AFE.get_special_tasks()`."""
433 def __init__(self, finished):
434 self.time_finished = finished
435
436
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700437class JobDirectorySubclassTests(mox.MoxTestBase):
438 """Test specific to RegularJobDirectory and SpecialJobDirectory.
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700439
440 This provides coverage for the implementation in both
441 RegularJobDirectory and SpecialJobDirectory.
442
443 """
444
445 def setUp(self):
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700446 super(JobDirectorySubclassTests, self).setUp()
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700447 self.mox.StubOutWithMock(job_directories._AFE, 'get_jobs')
448 self.mox.StubOutWithMock(job_directories._AFE,
449 'get_host_queue_entries')
450 self.mox.StubOutWithMock(job_directories._AFE,
451 'get_special_tasks')
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700452
Jakob Juelich24f22c22014-09-26 11:46:11 -0700453
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700454 def test_regular_job_fields(self):
455 """Test the constructor for `RegularJobDirectory`.
456
457 Construct a regular job, and assert that the `_dirname`
458 and `_id` attributes are set as expected.
459
460 """
461 resultsdir = '118-fubar'
462 job = job_directories.RegularJobDirectory(resultsdir)
463 self.assertEqual(job._dirname, resultsdir)
Dan Shicf4d2032015-03-12 15:04:21 -0700464 self.assertEqual(job._id, 118)
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700465
Jakob Juelich24f22c22014-09-26 11:46:11 -0700466
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700467 def test_special_job_fields(self):
468 """Test the constructor for `SpecialJobDirectory`.
469
470 Construct a special job, and assert that the `_dirname`
471 and `_id` attributes are set as expected.
472
473 """
474 destdir = 'hosts/host1'
475 resultsdir = destdir + '/118-reset'
476 job = job_directories.SpecialJobDirectory(resultsdir)
477 self.assertEqual(job._dirname, resultsdir)
Dan Shicf4d2032015-03-12 15:04:21 -0700478 self.assertEqual(job._id, 118)
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700479
Jakob Juelich24f22c22014-09-26 11:46:11 -0700480
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700481 def _check_finished_job(self, jobtime, hqetimes, expected):
482 """Mock and test behavior of a finished job.
483
484 Initialize the mocks for a call to
485 `get_timestamp_if_finished()`, then simulate one call.
486 Assert that the returned timestamp matches the passed
487 in expected value.
488
489 @param jobtime Time used to construct a _MockJob object.
490 @param hqetimes List of times used to construct
491 _MockHostQueueEntry objects.
492 @param expected Expected time to be returned by
493 get_timestamp_if_finished
494
495 """
496 job = job_directories.RegularJobDirectory('118-fubar')
497 job_directories._AFE.get_jobs(
498 id=job._id, finished=True).AndReturn(
499 [_MockJob(jobtime)])
500 job_directories._AFE.get_host_queue_entries(
501 finished_on__isnull=False,
502 job_id=job._id).AndReturn(
503 [_MockHostQueueEntry(t) for t in hqetimes])
504 self.mox.ReplayAll()
505 self.assertEqual(expected, job.get_timestamp_if_finished())
506 self.mox.VerifyAll()
507
508
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700509 def test_finished_regular_job(self):
510 """Test getting the timestamp for a finished regular job.
511
512 Tests the return value for
513 `RegularJobDirectory.get_timestamp_if_finished()` when
514 the AFE indicates the job is finished.
515
516 """
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700517 created_timestamp = _make_timestamp(1, True)
518 hqe_timestamp = _make_timestamp(0, True)
519 self._check_finished_job(created_timestamp,
520 [hqe_timestamp],
521 hqe_timestamp)
Simran Basifb98e462014-08-18 12:35:44 -0700522
Jakob Juelich24f22c22014-09-26 11:46:11 -0700523
Simran Basifb98e462014-08-18 12:35:44 -0700524 def test_finished_regular_job_multiple_hqes(self):
525 """Test getting the timestamp for a regular job with multiple hqes.
526
527 Tests the return value for
528 `RegularJobDirectory.get_timestamp_if_finished()` when
529 the AFE indicates the job is finished and the job has multiple host
530 queue entries.
531
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700532 Tests that the returned timestamp is the latest timestamp in
533 the list of HQEs, regardless of the returned order.
534
Simran Basifb98e462014-08-18 12:35:44 -0700535 """
Simran Basifb98e462014-08-18 12:35:44 -0700536 created_timestamp = _make_timestamp(2, True)
537 older_hqe_timestamp = _make_timestamp(1, True)
538 newer_hqe_timestamp = _make_timestamp(0, True)
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700539 hqe_list = [older_hqe_timestamp,
540 newer_hqe_timestamp]
541 self._check_finished_job(created_timestamp,
542 hqe_list,
543 newer_hqe_timestamp)
544 self.mox.ResetAll()
545 hqe_list.reverse()
546 self._check_finished_job(created_timestamp,
547 hqe_list,
548 newer_hqe_timestamp)
Simran Basifb98e462014-08-18 12:35:44 -0700549
Jakob Juelich24f22c22014-09-26 11:46:11 -0700550
Simran Basifb98e462014-08-18 12:35:44 -0700551 def test_finished_regular_job_null_finished_times(self):
552 """Test getting the timestamp for an aborted regular job.
553
554 Tests the return value for
555 `RegularJobDirectory.get_timestamp_if_finished()` when
556 the AFE indicates the job is finished and the job has aborted host
557 queue entries.
558
559 """
Simran Basifb98e462014-08-18 12:35:44 -0700560 timestamp = _make_timestamp(0, True)
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700561 self._check_finished_job(timestamp, [], timestamp)
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700562
Jakob Juelich24f22c22014-09-26 11:46:11 -0700563
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700564 def test_unfinished_regular_job(self):
565 """Test getting the timestamp for an unfinished regular job.
566
567 Tests the return value for
568 `RegularJobDirectory.get_timestamp_if_finished()` when
569 the AFE indicates the job is not finished.
570
571 """
572 job = job_directories.RegularJobDirectory('118-fubar')
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700573 job_directories._AFE.get_jobs(
574 id=job._id, finished=True).AndReturn([])
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700575 self.mox.ReplayAll()
576 self.assertIsNone(job.get_timestamp_if_finished())
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700577 self.mox.VerifyAll()
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700578
Jakob Juelich24f22c22014-09-26 11:46:11 -0700579
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700580 def test_finished_special_job(self):
581 """Test getting the timestamp for a finished special job.
582
583 Tests the return value for
584 `SpecialJobDirectory.get_timestamp_if_finished()` when
585 the AFE indicates the job is finished.
586
587 """
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700588 job = job_directories.SpecialJobDirectory(
589 'hosts/host1/118-reset')
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700590 timestamp = _make_timestamp(0, True)
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700591 job_directories._AFE.get_special_tasks(
592 id=job._id, is_complete=True).AndReturn(
593 [_MockSpecialTask(timestamp)])
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700594 self.mox.ReplayAll()
595 self.assertEqual(timestamp,
596 job.get_timestamp_if_finished())
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700597 self.mox.VerifyAll()
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700598
Jakob Juelich24f22c22014-09-26 11:46:11 -0700599
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700600 def test_unfinished_special_job(self):
601 """Test getting the timestamp for an unfinished special job.
602
603 Tests the return value for
604 `SpecialJobDirectory.get_timestamp_if_finished()` when
605 the AFE indicates the job is not finished.
606
607 """
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700608 job = job_directories.SpecialJobDirectory(
609 'hosts/host1/118-reset')
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700610 job_directories._AFE.get_special_tasks(
611 id=job._id, is_complete=True).AndReturn([])
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700612 self.mox.ReplayAll()
613 self.assertIsNone(job.get_timestamp_if_finished())
J. Richard Barnettedd0227d2015-04-10 15:18:48 -0700614 self.mox.VerifyAll()
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700615
616
J. Richard Barnetteea785362014-03-17 16:00:53 -0700617class _TempResultsDirTestBase(mox.MoxTestBase):
618 """Base class for tests using a temporary results directory."""
619
J. Richard Barnette08800322014-05-16 14:49:46 -0700620 REGULAR_JOBLIST = [
621 '111-fubar', '112-fubar', '113-fubar', '114-snafu']
622 HOST_LIST = ['host1', 'host2', 'host3']
623 SPECIAL_JOBLIST = [
624 'hosts/host1/333-reset', 'hosts/host1/334-reset',
625 'hosts/host2/444-reset', 'hosts/host3/555-reset']
626
Jakob Juelich24f22c22014-09-26 11:46:11 -0700627
J. Richard Barnetteea785362014-03-17 16:00:53 -0700628 def setUp(self):
629 super(_TempResultsDirTestBase, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700630 self._resultsroot = tempfile.mkdtemp()
631 self._cwd = os.getcwd()
632 os.chdir(self._resultsroot)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700633
Jakob Juelich24f22c22014-09-26 11:46:11 -0700634
J. Richard Barnetteea785362014-03-17 16:00:53 -0700635 def tearDown(self):
J. Richard Barnette08800322014-05-16 14:49:46 -0700636 os.chdir(self._cwd)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700637 shutil.rmtree(self._resultsroot)
638 super(_TempResultsDirTestBase, self).tearDown()
639
Jakob Juelich24f22c22014-09-26 11:46:11 -0700640
J. Richard Barnetteea785362014-03-17 16:00:53 -0700641 def make_job(self, jobdir):
642 """Create a job with results in `self._resultsroot`.
643
644 @param jobdir Name of the subdirectory to be created in
645 `self._resultsroot`.
646
647 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700648 os.mkdir(jobdir)
649 return _MockJobDirectory(jobdir)
650
Jakob Juelich24f22c22014-09-26 11:46:11 -0700651
J. Richard Barnette08800322014-05-16 14:49:46 -0700652 def make_job_hierarchy(self):
653 """Create a sample hierarchy of job directories.
654
655 `self.REGULAR_JOBLIST` is a list of directories for regular
656 jobs to be created; `self.SPECIAL_JOBLIST` is a list of
657 directories for special jobs to be created.
658
659 """
660 for d in self.REGULAR_JOBLIST:
661 os.mkdir(d)
662 hostsdir = 'hosts'
663 os.mkdir(hostsdir)
664 for host in self.HOST_LIST:
665 os.mkdir(os.path.join(hostsdir, host))
666 for d in self.SPECIAL_JOBLIST:
667 os.mkdir(d)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700668
669
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700670class OffloadDirectoryTests(_TempResultsDirTestBase):
671 """Tests for `offload_dir()`."""
672
673 def setUp(self):
674 super(OffloadDirectoryTests, self).setUp()
675 # offload_dir() logs messages; silence them.
676 self._saved_loglevel = logging.getLogger().getEffectiveLevel()
677 logging.getLogger().setLevel(logging.CRITICAL+1)
678 self._job = self.make_job(self.REGULAR_JOBLIST[0])
679 self.mox.StubOutWithMock(gs_offloader, 'get_cmd_list')
680 self.mox.StubOutWithMock(signal, 'alarm')
681
Jakob Juelich24f22c22014-09-26 11:46:11 -0700682
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700683 def tearDown(self):
684 logging.getLogger().setLevel(self._saved_loglevel)
685 super(OffloadDirectoryTests, self).tearDown()
686
Jakob Juelich24f22c22014-09-26 11:46:11 -0700687
Simran Basidd129972014-09-11 14:34:49 -0700688 def _mock_offload_dir_calls(self, command, queue_args):
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700689 """Mock out the calls needed by `offload_dir()`.
690
691 This covers only the calls made when there is no timeout.
692
693 @param command Command list to be returned by the mocked
694 call to `get_cmd_list()`.
695
696 """
697 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS)
Simran Basidd129972014-09-11 14:34:49 -0700698 command.append(queue_args[0])
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700699 gs_offloader.get_cmd_list(
Simran Basidd129972014-09-11 14:34:49 -0700700 queue_args[0], '%s%s' % (utils.DEFAULT_OFFLOAD_GSURI,
701 queue_args[1])).AndReturn(
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700702 command)
703 signal.alarm(0)
704 signal.alarm(0)
705
Jakob Juelich24f22c22014-09-26 11:46:11 -0700706
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700707 def _run_offload_dir(self, should_succeed):
708 """Make one call to `offload_dir()`.
709
710 The caller ensures all mocks are set up already.
711
712 @param should_succeed True iff the call to `offload_dir()`
713 is expected to succeed and remove the
714 offloaded job directory.
715
716 """
717 self.mox.ReplayAll()
Simran Basidd129972014-09-11 14:34:49 -0700718 gs_offloader.get_offload_dir_func(
719 utils.DEFAULT_OFFLOAD_GSURI)(self._job.queue_args[0],
720 self._job.queue_args[1])
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700721 self.mox.VerifyAll()
722 self.assertEqual(not should_succeed,
723 os.path.isdir(self._job.queue_args[0]))
724
Jakob Juelich24f22c22014-09-26 11:46:11 -0700725
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700726 def test_offload_success(self):
727 """Test that `offload_dir()` can succeed correctly."""
Simran Basidd129972014-09-11 14:34:49 -0700728 self._mock_offload_dir_calls(['test', '-d'],
729 self._job.queue_args)
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700730 self._run_offload_dir(True)
731
Jakob Juelich24f22c22014-09-26 11:46:11 -0700732
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700733 def test_offload_failure(self):
734 """Test that `offload_dir()` can fail correctly."""
Simran Basidd129972014-09-11 14:34:49 -0700735 self._mock_offload_dir_calls(['test', '!', '-d'],
736 self._job.queue_args)
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700737 self._run_offload_dir(False)
738
Jakob Juelich24f22c22014-09-26 11:46:11 -0700739
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700740 def test_offload_timeout_early(self):
741 """Test that `offload_dir()` times out correctly.
742
743 This test triggers timeout at the earliest possible moment,
744 at the first call to set the timeout alarm.
745
746 """
747 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS).AndRaise(
748 gs_offloader.TimeoutException('fubar'))
749 signal.alarm(0)
750 self._run_offload_dir(False)
751
Jakob Juelich24f22c22014-09-26 11:46:11 -0700752
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700753 def test_offload_timeout_late(self):
754 """Test that `offload_dir()` times out correctly.
755
756 This test triggers timeout at the latest possible moment, at
757 the call to clear the timeout alarm.
758
759 """
760 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS)
761 gs_offloader.get_cmd_list(
762 mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
763 ['test', '-d', self._job.queue_args[0]])
764 signal.alarm(0).AndRaise(
765 gs_offloader.TimeoutException('fubar'))
766 signal.alarm(0)
767 self._run_offload_dir(False)
768
769
J. Richard Barnetteea785362014-03-17 16:00:53 -0700770class JobDirectoryOffloadTests(_TempResultsDirTestBase):
771 """Tests for `_JobDirectory.enqueue_offload()`.
772
773 When testing with a `days_old` parameter of 0, we use
774 `set_finished()` instead of `set_expired()`. This causes the
775 job's timestamp to be set in the future. This is done so as
776 to test that when `days_old` is 0, the job is always treated
777 as eligible for offload, regardless of the timestamp's value.
778
779 Testing covers the following assertions:
780 A. Each time `enqueue_offload()` is called, a message that
781 includes the job's directory name will be logged using
782 `logging.debug()`, regardless of whether the job was
783 enqueued. Nothing else is allowed to be logged.
784 B. If the job is not eligible to be offloaded,
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700785 `get_failure_time()` and `get_failure_count()` are 0.
J. Richard Barnetteea785362014-03-17 16:00:53 -0700786 C. If the job is not eligible for offload, nothing is
787 enqueued in `queue`.
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700788 D. When the job is offloaded, `get_failure_count()` increments
J. Richard Barnetteea785362014-03-17 16:00:53 -0700789 each time.
790 E. When the job is offloaded, the appropriate parameters are
791 enqueued exactly once.
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700792 F. The first time a job is offloaded, `get_failure_time()` is
J. Richard Barnetteea785362014-03-17 16:00:53 -0700793 set to the current time.
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700794 G. `get_failure_time()` only changes the first time that the
J. Richard Barnetteea785362014-03-17 16:00:53 -0700795 job is offloaded.
796
797 The test cases below are designed to exercise all of the
798 meaningful state transitions at least once.
799
800 """
801
802 def setUp(self):
803 super(JobDirectoryOffloadTests, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700804 self._job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -0700805 self._queue = Queue.Queue()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700806
Jakob Juelich24f22c22014-09-26 11:46:11 -0700807
J. Richard Barnetteea785362014-03-17 16:00:53 -0700808 def _offload_unexpired_job(self, days_old):
809 """Make calls to `enqueue_offload()` for an unexpired job.
810
811 This method tests assertions B and C that calling
812 `enqueue_offload()` has no effect.
813
814 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700815 self.assertEqual(self._job.get_failure_count(), 0)
816 self.assertEqual(self._job.get_failure_time(), 0)
817 self._job.enqueue_offload(self._queue, days_old)
818 self._job.enqueue_offload(self._queue, days_old)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700819 self.assertTrue(self._queue.empty())
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700820 self.assertEqual(self._job.get_failure_count(), 0)
821 self.assertEqual(self._job.get_failure_time(), 0)
822 self.assertFalse(self._job.is_reportable())
J. Richard Barnetteea785362014-03-17 16:00:53 -0700823
Jakob Juelich24f22c22014-09-26 11:46:11 -0700824
J. Richard Barnetteea785362014-03-17 16:00:53 -0700825 def _offload_expired_once(self, days_old, count):
826 """Make one call to `enqueue_offload()` for an expired job.
827
828 This method tests assertions D and E regarding side-effects
829 expected when a job is offloaded.
830
831 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700832 self._job.enqueue_offload(self._queue, days_old)
833 self.assertEqual(self._job.get_failure_count(), count)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700834 self.assertFalse(self._queue.empty())
835 v = self._queue.get_nowait()
836 self.assertTrue(self._queue.empty())
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700837 self.assertEqual(v, self._job.queue_args)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700838
Jakob Juelich24f22c22014-09-26 11:46:11 -0700839
J. Richard Barnetteea785362014-03-17 16:00:53 -0700840 def _offload_expired_job(self, days_old):
841 """Make calls to `enqueue_offload()` for a just-expired job.
842
843 This method directly tests assertions F and G regarding
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700844 side-effects on `get_failure_time()`.
J. Richard Barnetteea785362014-03-17 16:00:53 -0700845
846 """
847 t0 = time.time()
848 self._offload_expired_once(days_old, 1)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700849 self.assertFalse(self._job.is_reportable())
850 t1 = self._job.get_failure_time()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700851 self.assertLessEqual(t1, time.time())
852 self.assertGreaterEqual(t1, t0)
853 self._offload_expired_once(days_old, 2)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700854 self.assertTrue(self._job.is_reportable())
855 self.assertEqual(self._job.get_failure_time(), t1)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700856 self._offload_expired_once(days_old, 3)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700857 self.assertTrue(self._job.is_reportable())
858 self.assertEqual(self._job.get_failure_time(), t1)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700859
Jakob Juelich24f22c22014-09-26 11:46:11 -0700860
J. Richard Barnetteea785362014-03-17 16:00:53 -0700861 def test_case_1_no_expiration(self):
862 """Test a series of `enqueue_offload()` calls with `days_old` of 0.
863
864 This tests that offload works as expected if calls are
865 made both before and after the job becomes expired.
866
867 """
868 self._offload_unexpired_job(0)
869 self._job.set_finished(0)
870 self._offload_expired_job(0)
871
Jakob Juelich24f22c22014-09-26 11:46:11 -0700872
J. Richard Barnetteea785362014-03-17 16:00:53 -0700873 def test_case_2_no_expiration(self):
874 """Test a series of `enqueue_offload()` calls with `days_old` of 0.
875
876 This tests that offload works as expected if calls are made
877 only after the job becomes expired.
878
879 """
880 self._job.set_finished(0)
881 self._offload_expired_job(0)
882
Jakob Juelich24f22c22014-09-26 11:46:11 -0700883
J. Richard Barnetteea785362014-03-17 16:00:53 -0700884 def test_case_1_with_expiration(self):
885 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
886
887 This tests that offload works as expected if calls are made
888 before the job finishes, before the job expires, and after
889 the job expires.
890
891 """
892 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
893 self._job.set_finished(_TEST_EXPIRATION_AGE)
894 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
895 self._job.set_expired(_TEST_EXPIRATION_AGE)
896 self._offload_expired_job(_TEST_EXPIRATION_AGE)
897
Jakob Juelich24f22c22014-09-26 11:46:11 -0700898
J. Richard Barnetteea785362014-03-17 16:00:53 -0700899 def test_case_2_with_expiration(self):
900 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
901
902 This tests that offload works as expected if calls are made
903 between finishing and expiration, and after the job expires.
904
905 """
906 self._job.set_finished(_TEST_EXPIRATION_AGE)
907 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
908 self._job.set_expired(_TEST_EXPIRATION_AGE)
909 self._offload_expired_job(_TEST_EXPIRATION_AGE)
910
Jakob Juelich24f22c22014-09-26 11:46:11 -0700911
J. Richard Barnetteea785362014-03-17 16:00:53 -0700912 def test_case_3_with_expiration(self):
913 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
914
915 This tests that offload works as expected if calls are made
916 only before finishing and after expiration.
917
918 """
919 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
920 self._job.set_expired(_TEST_EXPIRATION_AGE)
921 self._offload_expired_job(_TEST_EXPIRATION_AGE)
922
Jakob Juelich24f22c22014-09-26 11:46:11 -0700923
J. Richard Barnetteea785362014-03-17 16:00:53 -0700924 def test_case_4_with_expiration(self):
925 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
926
927 This tests that offload works as expected if calls are made
928 only after expiration.
929
930 """
931 self._job.set_expired(_TEST_EXPIRATION_AGE)
932 self._offload_expired_job(_TEST_EXPIRATION_AGE)
933
934
935class GetJobDirectoriesTests(_TempResultsDirTestBase):
936 """Tests for `_JobDirectory.get_job_directories()`."""
937
J. Richard Barnetteea785362014-03-17 16:00:53 -0700938 def setUp(self):
939 super(GetJobDirectoriesTests, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700940 self.make_job_hierarchy()
941 os.mkdir('not-a-job')
942 open('not-a-dir', 'w').close()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700943
Jakob Juelich24f22c22014-09-26 11:46:11 -0700944
J. Richard Barnetteea785362014-03-17 16:00:53 -0700945 def _run_get_directories(self, cls, expected_list):
946 """Test `get_job_directories()` for the given class.
947
948 Calls the method, and asserts that the returned list of
949 directories matches the expected return value.
950
951 @param expected_list Expected return value from the call.
952 """
J. Richard Barnetteea785362014-03-17 16:00:53 -0700953 dirlist = cls.get_job_directories()
954 self.assertEqual(set(dirlist), set(expected_list))
J. Richard Barnetteea785362014-03-17 16:00:53 -0700955
Jakob Juelich24f22c22014-09-26 11:46:11 -0700956
J. Richard Barnetteea785362014-03-17 16:00:53 -0700957 def test_get_regular_jobs(self):
958 """Test `RegularJobDirectory.get_job_directories()`."""
959 self._run_get_directories(job_directories.RegularJobDirectory,
J. Richard Barnette08800322014-05-16 14:49:46 -0700960 self.REGULAR_JOBLIST)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700961
Jakob Juelich24f22c22014-09-26 11:46:11 -0700962
J. Richard Barnetteea785362014-03-17 16:00:53 -0700963 def test_get_special_jobs(self):
964 """Test `SpecialJobDirectory.get_job_directories()`."""
965 self._run_get_directories(job_directories.SpecialJobDirectory,
J. Richard Barnette08800322014-05-16 14:49:46 -0700966 self.SPECIAL_JOBLIST)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700967
968
969class AddJobsTests(_TempResultsDirTestBase):
970 """Tests for `Offloader._add_new_jobs()`."""
971
J. Richard Barnette08800322014-05-16 14:49:46 -0700972 MOREJOBS = ['115-fubar', '116-fubar', '117-fubar', '118-snafu']
J. Richard Barnetteea785362014-03-17 16:00:53 -0700973
974 def setUp(self):
975 super(AddJobsTests, self).setUp()
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700976 self._initial_job_names = (
977 set(self.REGULAR_JOBLIST) | set(self.SPECIAL_JOBLIST))
J. Richard Barnette08800322014-05-16 14:49:46 -0700978 self.make_job_hierarchy()
979 self._offloader = gs_offloader.Offloader(_get_options(['-a']))
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700980 self.mox.StubOutWithMock(logging, 'debug')
J. Richard Barnetteea785362014-03-17 16:00:53 -0700981
Jakob Juelich24f22c22014-09-26 11:46:11 -0700982
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700983 def _run_add_new_jobs(self, expected_key_set):
J. Richard Barnetteea785362014-03-17 16:00:53 -0700984 """Basic test assertions for `_add_new_jobs()`.
985
986 Asserts the following:
987 * The keys in the offloader's `_open_jobs` dictionary
988 matches the expected set of keys.
989 * For every job in `_open_jobs`, the job has the expected
990 directory name.
991
992 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700993 count = len(expected_key_set) - len(self._offloader._open_jobs)
994 logging.debug(mox.IgnoreArg(), count)
995 self.mox.ReplayAll()
996 self._offloader._add_new_jobs()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700997 self.assertEqual(expected_key_set,
998 set(self._offloader._open_jobs.keys()))
999 for jobkey, job in self._offloader._open_jobs.items():
1000 self.assertEqual(jobkey, job._dirname)
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001001 self.mox.VerifyAll()
1002 self.mox.ResetAll()
J. Richard Barnetteea785362014-03-17 16:00:53 -07001003
Jakob Juelich24f22c22014-09-26 11:46:11 -07001004
J. Richard Barnetteea785362014-03-17 16:00:53 -07001005 def test_add_jobs_empty(self):
1006 """Test adding jobs to an empty dictionary.
1007
1008 Calls the offloader's `_add_new_jobs()`, then perform
1009 the assertions of `self._check_open_jobs()`.
1010
1011 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001012 self._run_add_new_jobs(self._initial_job_names)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001013
Jakob Juelich24f22c22014-09-26 11:46:11 -07001014
J. Richard Barnetteea785362014-03-17 16:00:53 -07001015 def test_add_jobs_non_empty(self):
1016 """Test adding jobs to a non-empty dictionary.
1017
1018 Calls the offloader's `_add_new_jobs()` twice; once from
1019 initial conditions, and then again after adding more
1020 directories. After the second call, perform the assertions
1021 of `self._check_open_jobs()`. Additionally, assert that
1022 keys added by the first call still map to their original
1023 job object after the second call.
1024
1025 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001026 self._run_add_new_jobs(self._initial_job_names)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001027 jobs_copy = self._offloader._open_jobs.copy()
J. Richard Barnette08800322014-05-16 14:49:46 -07001028 for d in self.MOREJOBS:
1029 os.mkdir(d)
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001030 self._run_add_new_jobs(self._initial_job_names |
1031 set(self.MOREJOBS))
J. Richard Barnetteea785362014-03-17 16:00:53 -07001032 for key in jobs_copy.keys():
1033 self.assertIs(jobs_copy[key],
1034 self._offloader._open_jobs[key])
1035
1036
1037class JobStateTests(_TempResultsDirTestBase):
1038 """Tests for job state predicates.
1039
1040 This tests for the expected results from the
1041 `is_offloaded()` and `is_reportable()` predicate
1042 methods.
1043
1044 """
1045
1046 def test_unfinished_job(self):
1047 """Test that an unfinished job reports the correct state.
1048
1049 A job is "unfinished" if it isn't marked complete in the
1050 database. A job in this state is neither "complete" nor
1051 "reportable".
1052
1053 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001054 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001055 self.assertFalse(job.is_offloaded())
1056 self.assertFalse(job.is_reportable())
1057
Jakob Juelich24f22c22014-09-26 11:46:11 -07001058
J. Richard Barnetteea785362014-03-17 16:00:53 -07001059 def test_incomplete_job(self):
1060 """Test that an incomplete job reports the correct state.
1061
1062 A job is "incomplete" if exactly one attempt has been made
1063 to offload the job, but its results directory still exists.
1064 A job in this state is neither "complete" nor "reportable".
1065
1066 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001067 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001068 job.set_incomplete()
1069 self.assertFalse(job.is_offloaded())
1070 self.assertFalse(job.is_reportable())
1071
Jakob Juelich24f22c22014-09-26 11:46:11 -07001072
J. Richard Barnetteea785362014-03-17 16:00:53 -07001073 def test_reportable_job(self):
1074 """Test that a reportable job reports the correct state.
1075
1076 A job is "reportable" if more than one attempt has been made
1077 to offload the job, and its results directory still exists.
1078 A job in this state is "reportable", but not "complete".
1079
1080 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001081 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001082 job.set_reportable()
1083 self.assertFalse(job.is_offloaded())
1084 self.assertTrue(job.is_reportable())
1085
Jakob Juelich24f22c22014-09-26 11:46:11 -07001086
J. Richard Barnetteea785362014-03-17 16:00:53 -07001087 def test_completed_job(self):
1088 """Test that a completed job reports the correct state.
1089
1090 A job is "completed" if at least one attempt has been made
1091 to offload the job, and its results directory still exists.
1092 A job in this state is "complete", and not "reportable".
1093
1094 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001095 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001096 job.set_complete()
1097 self.assertTrue(job.is_offloaded())
1098 self.assertFalse(job.is_reportable())
1099
1100
1101class ReportingTests(_TempResultsDirTestBase):
1102 """Tests for `Offloader._update_offload_results()`."""
1103
J. Richard Barnetteea785362014-03-17 16:00:53 -07001104 def setUp(self):
1105 super(ReportingTests, self).setUp()
1106 self._offloader = gs_offloader.Offloader(_get_options([]))
1107 self.mox.StubOutWithMock(email_manager.manager,
1108 'send_email')
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001109 self.mox.StubOutWithMock(logging, 'debug')
J. Richard Barnetteea785362014-03-17 16:00:53 -07001110
Jakob Juelich24f22c22014-09-26 11:46:11 -07001111
J. Richard Barnetteea785362014-03-17 16:00:53 -07001112 def _add_job(self, jobdir):
1113 """Add a job to the dictionary of unfinished jobs."""
1114 j = self.make_job(jobdir)
1115 self._offloader._open_jobs[j._dirname] = j
1116 return j
1117
Jakob Juelich24f22c22014-09-26 11:46:11 -07001118
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001119 def _expect_log_message(self, new_open_jobs, with_failures):
1120 """Mock expected logging calls.
1121
1122 `_update_offload_results()` logs one message with the number
1123 of jobs removed from the open job set and the number of jobs
1124 still remaining. Additionally, if there are reportable
1125 jobs, then it logs the number of jobs that haven't yet
1126 offloaded.
1127
1128 This sets up the logging calls using `new_open_jobs` to
1129 figure the job counts. If `with_failures` is true, then
1130 the log message is set up assuming that all jobs in
1131 `new_open_jobs` have offload failures.
1132
1133 @param new_open_jobs New job set for calculating counts
1134 in the messages.
1135 @param with_failures Whether the log message with a
1136 failure count is expected.
1137
1138 """
1139 count = len(self._offloader._open_jobs) - len(new_open_jobs)
1140 logging.debug(mox.IgnoreArg(), count, len(new_open_jobs))
1141 if with_failures:
1142 logging.debug(mox.IgnoreArg(), len(new_open_jobs))
1143
Jakob Juelich24f22c22014-09-26 11:46:11 -07001144
J. Richard Barnetteea785362014-03-17 16:00:53 -07001145 def _run_update_no_report(self, new_open_jobs):
1146 """Call `_update_offload_results()` expecting no report.
1147
1148 Initial conditions are set up by the caller. This calls
1149 `_update_offload_results()` once, and then checks these
1150 assertions:
1151 * The offloader's `_next_report_time` field is unchanged.
1152 * The offloader's new `_open_jobs` field contains only
1153 the entries in `new_open_jobs`.
1154 * The email_manager's `send_email` stub wasn't called.
1155
1156 @param new_open_jobs A dictionary representing the expected
1157 new value of the offloader's
1158 `_open_jobs` field.
1159 """
1160 self.mox.ReplayAll()
1161 next_report_time = self._offloader._next_report_time
1162 self._offloader._update_offload_results()
1163 self.assertEqual(next_report_time,
1164 self._offloader._next_report_time)
1165 self.assertEqual(self._offloader._open_jobs, new_open_jobs)
1166 self.mox.VerifyAll()
1167 self.mox.ResetAll()
1168
Jakob Juelich24f22c22014-09-26 11:46:11 -07001169
J. Richard Barnetteea785362014-03-17 16:00:53 -07001170 def _run_update_with_report(self, new_open_jobs):
1171 """Call `_update_offload_results()` expecting an e-mail report.
1172
1173 Initial conditions are set up by the caller. This calls
1174 `_update_offload_results()` once, and then checks these
1175 assertions:
1176 * The offloader's `_next_report_time` field is updated
1177 to an appropriate new time.
1178 * The offloader's new `_open_jobs` field contains only
1179 the entries in `new_open_jobs`.
1180 * The email_manager's `send_email` stub was called.
1181
1182 @param new_open_jobs A dictionary representing the expected
1183 new value of the offloader's
1184 `_open_jobs` field.
1185 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001186 logging.debug(mox.IgnoreArg())
J. Richard Barnetteea785362014-03-17 16:00:53 -07001187 email_manager.manager.send_email(
1188 mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg())
1189 self.mox.ReplayAll()
1190 t0 = time.time() + gs_offloader.REPORT_INTERVAL_SECS
1191 self._offloader._update_offload_results()
1192 t1 = time.time() + gs_offloader.REPORT_INTERVAL_SECS
1193 next_report_time = self._offloader._next_report_time
1194 self.assertGreaterEqual(next_report_time, t0)
1195 self.assertLessEqual(next_report_time, t1)
1196 self.assertEqual(self._offloader._open_jobs, new_open_jobs)
1197 self.mox.VerifyAll()
1198 self.mox.ResetAll()
1199
Jakob Juelich24f22c22014-09-26 11:46:11 -07001200
J. Richard Barnetteea785362014-03-17 16:00:53 -07001201 def test_no_jobs(self):
1202 """Test `_update_offload_results()` with no open jobs.
1203
1204 Initial conditions are an empty `_open_jobs` list and
1205 `_next_report_time` in the past. Expected result is no
1206 e-mail report, and an empty `_open_jobs` list.
1207
1208 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001209 self._expect_log_message({}, False)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001210 self._run_update_no_report({})
1211
Jakob Juelich24f22c22014-09-26 11:46:11 -07001212
J. Richard Barnetteea785362014-03-17 16:00:53 -07001213 def test_all_completed(self):
1214 """Test `_update_offload_results()` with only complete jobs.
1215
1216 Initial conditions are an `_open_jobs` list consisting of
1217 only completed jobs and `_next_report_time` in the past.
1218 Expected result is no e-mail report, and an empty
1219 `_open_jobs` list.
1220
1221 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001222 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001223 self._add_job(d).set_complete()
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001224 self._expect_log_message({}, False)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001225 self._run_update_no_report({})
1226
Jakob Juelich24f22c22014-09-26 11:46:11 -07001227
J. Richard Barnetteea785362014-03-17 16:00:53 -07001228 def test_none_finished(self):
1229 """Test `_update_offload_results()` with only unfinished jobs.
1230
1231 Initial conditions are an `_open_jobs` list consisting of
1232 only unfinished jobs and `_next_report_time` in the past.
1233 Expected result is no e-mail report, and no change to the
1234 `_open_jobs` list.
1235
1236 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001237 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001238 self._add_job(d)
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001239 new_jobs = self._offloader._open_jobs.copy()
1240 self._expect_log_message(new_jobs, False)
1241 self._run_update_no_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001242
Jakob Juelich24f22c22014-09-26 11:46:11 -07001243
J. Richard Barnetteea785362014-03-17 16:00:53 -07001244 def test_none_reportable(self):
1245 """Test `_update_offload_results()` with only incomplete jobs.
1246
1247 Initial conditions are an `_open_jobs` list consisting of
1248 only incomplete jobs and `_next_report_time` in the past.
1249 Expected result is no e-mail report, and no change to the
1250 `_open_jobs` list.
1251
1252 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001253 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001254 self._add_job(d).set_incomplete()
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001255 new_jobs = self._offloader._open_jobs.copy()
1256 self._expect_log_message(new_jobs, False)
1257 self._run_update_no_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001258
Jakob Juelich24f22c22014-09-26 11:46:11 -07001259
J. Richard Barnetteea785362014-03-17 16:00:53 -07001260 def test_report_not_ready(self):
1261 """Test `_update_offload_results()` e-mail throttling.
1262
1263 Initial conditions are an `_open_jobs` list consisting of
1264 only reportable jobs but with `_next_report_time` in
1265 the future. Expected result is no e-mail report, and no
1266 change to the `_open_jobs` list.
1267
1268 """
1269 # N.B. This test may fail if its run time exceeds more than
1270 # about _MARGIN_SECS seconds.
J. Richard Barnette08800322014-05-16 14:49:46 -07001271 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001272 self._add_job(d).set_reportable()
1273 self._offloader._next_report_time += _MARGIN_SECS
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001274 new_jobs = self._offloader._open_jobs.copy()
1275 self._expect_log_message(new_jobs, True)
1276 self._run_update_no_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001277
Jakob Juelich24f22c22014-09-26 11:46:11 -07001278
J. Richard Barnetteea785362014-03-17 16:00:53 -07001279 def test_reportable(self):
1280 """Test `_update_offload_results()` with reportable jobs.
1281
1282 Initial conditions are an `_open_jobs` list consisting of
1283 only reportable jobs and with `_next_report_time` in
1284 the past. Expected result is an e-mail report, and no
1285 change to the `_open_jobs` list.
1286
1287 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001288 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001289 self._add_job(d).set_reportable()
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001290 new_jobs = self._offloader._open_jobs.copy()
1291 self._expect_log_message(new_jobs, True)
1292 self._run_update_with_report(new_jobs)
1293
Jakob Juelich24f22c22014-09-26 11:46:11 -07001294
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001295 def test_reportable_mixed(self):
1296 """Test `_update_offload_results()` with a mixture of jobs.
1297
1298 Initial conditions are an `_open_jobs` list consisting of
1299 one reportable jobs and the remainder of the jobs
1300 incomplete. The value of `_next_report_time` is in the
1301 past. Expected result is an e-mail report that includes
1302 both the reportable and the incomplete jobs, and no change
1303 to the `_open_jobs` list.
1304
1305 """
1306 self._add_job(self.REGULAR_JOBLIST[0]).set_reportable()
1307 for d in self.REGULAR_JOBLIST[1:]:
1308 self._add_job(d).set_incomplete()
1309 new_jobs = self._offloader._open_jobs.copy()
1310 self._expect_log_message(new_jobs, True)
1311 self._run_update_with_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001312
1313
1314if __name__ == '__main__':
1315 unittest.main()