blob: 9e70d8a54dddd6ec655e7dfc45c6107a2451198d [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
10import sys
11import tempfile
12import time
13import unittest
14
15import mox
16
17import common
18import gs_offloader
19import job_directories
20
21from autotest_lib.scheduler import email_manager
22
23# Test value to use for `days_old`, if nothing else is required.
24_TEST_EXPIRATION_AGE = 7
25
26# When constructing sample time values for testing expiration,
27# allow this many seconds between the expiration time and the
28# current time.
29_MARGIN_SECS = 10.0
30
31
32def _get_options(argv):
33 """Helper function to exercise command line parsing.
34
35 @param argv Value of sys.argv to be parsed.
36
37 """
38 sys.argv = ['bogus.py'] + argv
39 return gs_offloader.parse_options()
40
41
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070042class OffloaderOptionsTests(unittest.TestCase):
43 """Tests for the `Offloader` constructor.
44
45 Tests that offloader instance fields are set as expected
46 for given command line options.
47
48 """
49
50 _REGULAR_ONLY = set([job_directories.RegularJobDirectory])
51 _SPECIAL_ONLY = set([job_directories.SpecialJobDirectory])
52 _BOTH = _REGULAR_ONLY | _SPECIAL_ONLY
J. Richard Barnetteea785362014-03-17 16:00:53 -070053
54 def test_process_no_options(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070055 """Test default offloader options."""
56 offloader = gs_offloader.Offloader(_get_options([]))
57 self.assertEqual(set(offloader._jobdir_classes),
58 self._REGULAR_ONLY)
59 self.assertEqual(offloader._processes, 1)
60 self.assertEqual(offloader._offload_func,
61 gs_offloader.offload_dir)
62 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -070063
64 def test_process_all_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070065 """Test offloader handling for the --all option."""
66 offloader = gs_offloader.Offloader(_get_options(['--all']))
67 self.assertEqual(set(offloader._jobdir_classes), self._BOTH)
68 self.assertEqual(offloader._processes, 1)
69 self.assertEqual(offloader._offload_func,
70 gs_offloader.offload_dir)
71 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -070072
73 def test_process_hosts_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070074 """Test offloader handling for the --hosts option."""
75 offloader = gs_offloader.Offloader(
76 _get_options(['--hosts']))
77 self.assertEqual(set(offloader._jobdir_classes),
78 self._SPECIAL_ONLY)
79 self.assertEqual(offloader._processes, 1)
80 self.assertEqual(offloader._offload_func,
81 gs_offloader.offload_dir)
82 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -070083
84 def test_parallelism_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070085 """Test offloader handling for the --parallelism option."""
86 offloader = gs_offloader.Offloader(
87 _get_options(['--parallelism', '2']))
88 self.assertEqual(set(offloader._jobdir_classes),
89 self._REGULAR_ONLY)
90 self.assertEqual(offloader._processes, 2)
91 self.assertEqual(offloader._offload_func,
92 gs_offloader.offload_dir)
93 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -070094
95 def test_delete_only_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070096 """Test offloader handling for the --delete_only option."""
97 offloader = gs_offloader.Offloader(
98 _get_options(['--delete_only']))
99 self.assertEqual(set(offloader._jobdir_classes),
100 self._REGULAR_ONLY)
101 self.assertEqual(offloader._processes, 1)
102 self.assertEqual(offloader._offload_func,
103 gs_offloader.delete_files)
104 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700105
106 def test_delete_only_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700107 """Test offloader handling for the --days_old option."""
108 offloader = gs_offloader.Offloader(
109 _get_options(['--days_old', '7']))
110 self.assertEqual(set(offloader._jobdir_classes),
111 self._REGULAR_ONLY)
112 self.assertEqual(offloader._processes, 1)
113 self.assertEqual(offloader._offload_func,
114 gs_offloader.offload_dir)
115 self.assertEqual(offloader._age_limit, 7)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700116
117
118def _make_timestamp(age_limit, is_expired):
119 """Create a timestamp for use by `job_directories._is_job_expired()`.
120
121 The timestamp will meet the syntactic requirements for
122 timestamps used as input to `_is_job_expired()`. If
123 `is_expired` is true, the timestamp will be older than
124 `age_limit` days before the current time; otherwise, the
125 date will be younger.
126
127 @param age_limit The number of days before expiration of the
128 target timestamp.
129 @param is_expired Whether the timestamp should be expired
130 relative to `age_limit`.
131
132 """
133 seconds = -_MARGIN_SECS
134 if is_expired:
135 seconds = -seconds
136 delta = datetime.timedelta(days=age_limit, seconds=seconds)
137 reference_time = datetime.datetime.now() - delta
138 return reference_time.strftime(job_directories.JOB_TIME_FORMAT)
139
140
141class JobExpirationTests(unittest.TestCase):
142 """Tests to exercise `job_directories._is_job_expired()`."""
143
144 def test_expired(self):
145 """Test detection of an expired job."""
146 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, True)
147 self.assertTrue(
148 job_directories._is_job_expired(
149 _TEST_EXPIRATION_AGE, timestamp))
150
151
152 def test_alive(self):
153 """Test detection of a job that's not expired."""
154 # N.B. This test may fail if its run time exceeds more than
155 # about _MARGIN_SECS seconds.
156 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, False)
157 self.assertFalse(
158 job_directories._is_job_expired(
159 _TEST_EXPIRATION_AGE, timestamp))
160
161
162class _MockJobDirectory(job_directories._JobDirectory):
163 """Subclass of `_JobDirectory` used as a helper for tests."""
164
165 GLOB_PATTERN = '[0-9]*-*'
166
167 def __init__(self, resultsdir):
168 """Create new job in initial state."""
169 super(_MockJobDirectory, self).__init__(resultsdir)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700170 self._timestamp = None
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700171 self.queue_args = [resultsdir, os.path.dirname(resultsdir)]
J. Richard Barnetteea785362014-03-17 16:00:53 -0700172
173 def get_timestamp_if_finished(self):
174 return self._timestamp
175
176 def set_finished(self, days_old):
177 """Make this job appear to be finished.
178
179 After calling this function, calls to `enqueue_offload()`
180 will find this job as finished, but not expired and ready
181 for offload. Note that when `days_old` is 0,
182 `enqueue_offload()` will treat a finished job as eligible
183 for offload.
184
185 @param days_old The value of the `days_old` parameter that
186 will be passed to `enqueue_offload()` for
187 testing.
188
189 """
190 self._timestamp = _make_timestamp(days_old, False)
191
192 def set_expired(self, days_old):
193 """Make this job eligible to be offloaded.
194
195 After calling this function, calls to `offload` will attempt
196 to offload this job.
197
198 @param days_old The value of the `days_old` parameter that
199 will be passed to `enqueue_offload()` for
200 testing.
201
202 """
203 self._timestamp = _make_timestamp(days_old, True)
204
205 def set_incomplete(self):
206 """Make this job appear to have failed offload just once."""
207 self._offload_count += 1
208 if not os.path.isdir(self._dirname):
209 os.mkdir(self._dirname)
210
211 def set_reportable(self):
212 """Make this job be reportable."""
213 self._offload_count += 1
214 self.set_incomplete()
215
216 def set_complete(self):
217 """Make this job be completed."""
218 self._offload_count += 1
219 if os.path.isdir(self._dirname):
220 os.rmdir(self._dirname)
221
222
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700223class CommandListTests(unittest.TestCase):
224 """Tests for `get_cmd_list()`."""
225
226 def _command_list_assertions(self, job):
227 """Call `get_cmd_list()` and check the return value.
228
229 Check the following assertions:
230 * The command name (argv[0]) is 'gsutil'.
231 * The arguments contain the 'cp' subcommand.
232 * The next-to-last argument (the source directory) is the
233 job's `queue_args[0]`.
234 * The last argument (the destination URL) ends with the
235 job's `queue_args[1]`.
236 * The last argument (the destination URL) starts with
237 `GS_URI`.
238
239 @param job A job with properly calculated arguments to
240 `get_cmd_list()`
241
242 """
243 command = gs_offloader.get_cmd_list(job.queue_args[0],
244 job.queue_args[1])
245 self.assertEqual(command[0], 'gsutil')
246 self.assertTrue('cp' in command)
247 self.assertEqual(command[-2], job.queue_args[0])
248 self.assertTrue(command[-1].endswith(job.queue_args[1]))
249 self.assertTrue(command[-1].startswith(gs_offloader.GS_URI))
250
251 def test_get_cmd_list_regular(self):
252 """Test `get_cmd_list()` as for a regular job."""
253 job = _MockJobDirectory('118-debug')
254 self._command_list_assertions(job)
255
256 def test_get_cmd_list_special(self):
257 """Test `get_cmd_list()` as for a special job."""
258 job = _MockJobDirectory('hosts/host1/118-reset')
259 self._command_list_assertions(job)
260
261
J. Richard Barnetteea785362014-03-17 16:00:53 -0700262# Below is partial sample of e-mail notification text. This text is
263# deliberately hard-coded and then parsed to create the test data;
264# the idea is to make sure the actual text format will be reviewed
265# by a human being.
266#
267# first offload count directory
268# --+----1----+---- ----+ ----+----1----+----2----+----3
269_SAMPLE_DIRECTORIES_REPORT = '''\
270=================== ====== ==============================
2712014-03-14 15:09:26 1 118-fubar
2722014-03-14 15:19:23 2 117-fubar
2732014-03-14 15:29:20 6 116-fubar
2742014-03-14 15:39:17 24 115-fubar
2752014-03-14 15:49:14 120 114-fubar
2762014-03-14 15:59:11 720 113-fubar
2772014-03-14 16:09:08 5040 112-fubar
2782014-03-14 16:19:05 40320 111-fubar
279'''
280
281
282class EmailTemplateTests(mox.MoxTestBase):
283 """Test the formatting of e-mail notifications."""
284
285 def setUp(self):
286 super(EmailTemplateTests, self).setUp()
287 self.mox.StubOutWithMock(email_manager.manager,
288 'send_email')
289 self._joblist = []
290 for line in _SAMPLE_DIRECTORIES_REPORT.split('\n')[1 : -1]:
291 date_, time_, count, dir_ = line.split()
292 job = _MockJobDirectory(dir_)
293 job._offload_count = int(count)
294 timestruct = time.strptime(
295 "%s %s" % (date_, time_),
296 gs_offloader.ERROR_EMAIL_TIME_FORMAT)
297 job._first_offload_start = time.mktime(timestruct)
298 # enter the jobs in reverse order, to make sure we
299 # test that the output will be sorted.
300 self._joblist.insert(0, job)
301
302 def test_email_template(self):
303 """Trigger an e-mail report and check its contents."""
304 # The last line of the report is a separator that we
305 # repeat in the first line of our expected result data.
306 # So, we remove that separator from the end of the of
307 # the e-mail report message.
308 #
309 # The last element in the list returned by split('\n')
310 # will be an empty string, so to remove the separator,
311 # we remove the next-to-last entry in the list.
312 report_lines = gs_offloader.ERROR_EMAIL_REPORT_FORMAT.split('\n')
313 expected_message = ('\n'.join(report_lines[: -2] +
314 report_lines[-1 :]) +
315 _SAMPLE_DIRECTORIES_REPORT)
316 email_manager.manager.send_email(
317 mox.IgnoreArg(), mox.IgnoreArg(), expected_message)
318 self.mox.ReplayAll()
319 gs_offloader.report_offload_failures(self._joblist)
320
321
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700322class JobDirectorySubclassTests(mox.MoxTestBase):
323 """Test specific to RegularJobDirectory and SpecialJobDirectory.
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700324
325 This provides coverage for the implementation in both
326 RegularJobDirectory and SpecialJobDirectory.
327
328 """
329
330 def setUp(self):
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700331 super(JobDirectorySubclassTests, self).setUp()
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700332 self.mox.StubOutWithMock(job_directories._AFE, 'run')
333
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700334 def test_regular_job_fields(self):
335 """Test the constructor for `RegularJobDirectory`.
336
337 Construct a regular job, and assert that the `_dirname`
338 and `_id` attributes are set as expected.
339
340 """
341 resultsdir = '118-fubar'
342 job = job_directories.RegularJobDirectory(resultsdir)
343 self.assertEqual(job._dirname, resultsdir)
344 self.assertEqual(job._id, '118')
345
346 def test_special_job_fields(self):
347 """Test the constructor for `SpecialJobDirectory`.
348
349 Construct a special job, and assert that the `_dirname`
350 and `_id` attributes are set as expected.
351
352 """
353 destdir = 'hosts/host1'
354 resultsdir = destdir + '/118-reset'
355 job = job_directories.SpecialJobDirectory(resultsdir)
356 self.assertEqual(job._dirname, resultsdir)
357 self.assertEqual(job._id, '118')
358
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700359 def test_finished_regular_job(self):
360 """Test getting the timestamp for a finished regular job.
361
362 Tests the return value for
363 `RegularJobDirectory.get_timestamp_if_finished()` when
364 the AFE indicates the job is finished.
365
366 """
367 job = job_directories.RegularJobDirectory('118-fubar')
368 timestamp = _make_timestamp(0, True)
369 job_directories._AFE.run(
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700370 'get_jobs', id=job._id, finished=True).AndReturn(
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700371 [{'created_on': timestamp}])
372 self.mox.ReplayAll()
373 self.assertEqual(timestamp,
374 job.get_timestamp_if_finished())
375
376 def test_unfinished_regular_job(self):
377 """Test getting the timestamp for an unfinished regular job.
378
379 Tests the return value for
380 `RegularJobDirectory.get_timestamp_if_finished()` when
381 the AFE indicates the job is not finished.
382
383 """
384 job = job_directories.RegularJobDirectory('118-fubar')
385 job_directories._AFE.run(
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700386 'get_jobs', id=job._id, finished=True).AndReturn(None)
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700387 self.mox.ReplayAll()
388 self.assertIsNone(job.get_timestamp_if_finished())
389
390 def test_finished_special_job(self):
391 """Test getting the timestamp for a finished special job.
392
393 Tests the return value for
394 `SpecialJobDirectory.get_timestamp_if_finished()` when
395 the AFE indicates the job is finished.
396
397 """
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700398 job = job_directories.SpecialJobDirectory(
399 'hosts/host1/118-reset')
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700400 timestamp = _make_timestamp(0, True)
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700401 job_directories._AFE.run('get_special_tasks',
402 id=job._id,
403 is_complete=True).AndReturn(
404 [{'time_started': timestamp}])
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700405 self.mox.ReplayAll()
406 self.assertEqual(timestamp,
407 job.get_timestamp_if_finished())
408
409 def test_unfinished_special_job(self):
410 """Test getting the timestamp for an unfinished special job.
411
412 Tests the return value for
413 `SpecialJobDirectory.get_timestamp_if_finished()` when
414 the AFE indicates the job is not finished.
415
416 """
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700417 job = job_directories.SpecialJobDirectory(
418 'hosts/host1/118-reset')
419 job_directories._AFE.run('get_special_tasks',
420 id=job._id,
421 is_complete=True).AndReturn(None)
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700422 self.mox.ReplayAll()
423 self.assertIsNone(job.get_timestamp_if_finished())
424
425
J. Richard Barnetteea785362014-03-17 16:00:53 -0700426class _TempResultsDirTestBase(mox.MoxTestBase):
427 """Base class for tests using a temporary results directory."""
428
J. Richard Barnette08800322014-05-16 14:49:46 -0700429 REGULAR_JOBLIST = [
430 '111-fubar', '112-fubar', '113-fubar', '114-snafu']
431 HOST_LIST = ['host1', 'host2', 'host3']
432 SPECIAL_JOBLIST = [
433 'hosts/host1/333-reset', 'hosts/host1/334-reset',
434 'hosts/host2/444-reset', 'hosts/host3/555-reset']
435
J. Richard Barnetteea785362014-03-17 16:00:53 -0700436 def setUp(self):
437 super(_TempResultsDirTestBase, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700438 self._resultsroot = tempfile.mkdtemp()
439 self._cwd = os.getcwd()
440 os.chdir(self._resultsroot)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700441
442 def tearDown(self):
J. Richard Barnette08800322014-05-16 14:49:46 -0700443 os.chdir(self._cwd)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700444 shutil.rmtree(self._resultsroot)
445 super(_TempResultsDirTestBase, self).tearDown()
446
447 def make_job(self, jobdir):
448 """Create a job with results in `self._resultsroot`.
449
450 @param jobdir Name of the subdirectory to be created in
451 `self._resultsroot`.
452
453 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700454 os.mkdir(jobdir)
455 return _MockJobDirectory(jobdir)
456
457 def make_job_hierarchy(self):
458 """Create a sample hierarchy of job directories.
459
460 `self.REGULAR_JOBLIST` is a list of directories for regular
461 jobs to be created; `self.SPECIAL_JOBLIST` is a list of
462 directories for special jobs to be created.
463
464 """
465 for d in self.REGULAR_JOBLIST:
466 os.mkdir(d)
467 hostsdir = 'hosts'
468 os.mkdir(hostsdir)
469 for host in self.HOST_LIST:
470 os.mkdir(os.path.join(hostsdir, host))
471 for d in self.SPECIAL_JOBLIST:
472 os.mkdir(d)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700473
474
475class JobDirectoryOffloadTests(_TempResultsDirTestBase):
476 """Tests for `_JobDirectory.enqueue_offload()`.
477
478 When testing with a `days_old` parameter of 0, we use
479 `set_finished()` instead of `set_expired()`. This causes the
480 job's timestamp to be set in the future. This is done so as
481 to test that when `days_old` is 0, the job is always treated
482 as eligible for offload, regardless of the timestamp's value.
483
484 Testing covers the following assertions:
485 A. Each time `enqueue_offload()` is called, a message that
486 includes the job's directory name will be logged using
487 `logging.debug()`, regardless of whether the job was
488 enqueued. Nothing else is allowed to be logged.
489 B. If the job is not eligible to be offloaded,
490 `_first_offload_start` and `_offload_count` are 0.
491 C. If the job is not eligible for offload, nothing is
492 enqueued in `queue`.
493 D. When the job is offloaded, `_offload_count` increments
494 each time.
495 E. When the job is offloaded, the appropriate parameters are
496 enqueued exactly once.
497 F. The first time a job is offloaded, `_first_offload_start` is
498 set to the current time.
499 G. `_first_offload_start` only changes the first time that the
500 job is offloaded.
501
502 The test cases below are designed to exercise all of the
503 meaningful state transitions at least once.
504
505 """
506
507 def setUp(self):
508 super(JobDirectoryOffloadTests, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700509 self._job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -0700510 self._queue = Queue.Queue()
511 self.mox.StubOutWithMock(logging, 'debug')
512
513 def _offload_once(self, days_old):
514 """Make one call to the `enqueue_offload()` method.
515
516 This method tests assertion A regarding message
517 logging.
518
519 """
520 logging.debug(mox.IgnoreArg(), self._job._dirname)
521 self.mox.ReplayAll()
522 self._job.enqueue_offload(self._queue, days_old)
523 self.mox.VerifyAll()
524 self.mox.ResetAll()
525
526 def _offload_unexpired_job(self, days_old):
527 """Make calls to `enqueue_offload()` for an unexpired job.
528
529 This method tests assertions B and C that calling
530 `enqueue_offload()` has no effect.
531
532 """
533 self.assertEqual(self._job._offload_count, 0)
534 self.assertEqual(self._job._first_offload_start, 0)
535 self._offload_once(days_old)
536 self._offload_once(days_old)
537 self.assertTrue(self._queue.empty())
538 self.assertEqual(self._job._offload_count, 0)
539 self.assertEqual(self._job._first_offload_start, 0)
540
541 def _offload_expired_once(self, days_old, count):
542 """Make one call to `enqueue_offload()` for an expired job.
543
544 This method tests assertions D and E regarding side-effects
545 expected when a job is offloaded.
546
547 """
548 self._offload_once(days_old)
549 self.assertEqual(self._job._offload_count, count)
550 self.assertFalse(self._queue.empty())
551 v = self._queue.get_nowait()
552 self.assertTrue(self._queue.empty())
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700553 self.assertEqual(v, self._job.queue_args)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700554
555 def _offload_expired_job(self, days_old):
556 """Make calls to `enqueue_offload()` for a just-expired job.
557
558 This method directly tests assertions F and G regarding
559 side-effects on `_first_offload_start`.
560
561 """
562 t0 = time.time()
563 self._offload_expired_once(days_old, 1)
564 t1 = self._job._first_offload_start
565 self.assertLessEqual(t1, time.time())
566 self.assertGreaterEqual(t1, t0)
567 self._offload_expired_once(days_old, 2)
568 self.assertEqual(self._job._first_offload_start, t1)
569 self._offload_expired_once(days_old, 3)
570 self.assertEqual(self._job._first_offload_start, t1)
571
572 def test_case_1_no_expiration(self):
573 """Test a series of `enqueue_offload()` calls with `days_old` of 0.
574
575 This tests that offload works as expected if calls are
576 made both before and after the job becomes expired.
577
578 """
579 self._offload_unexpired_job(0)
580 self._job.set_finished(0)
581 self._offload_expired_job(0)
582
583 def test_case_2_no_expiration(self):
584 """Test a series of `enqueue_offload()` calls with `days_old` of 0.
585
586 This tests that offload works as expected if calls are made
587 only after the job becomes expired.
588
589 """
590 self._job.set_finished(0)
591 self._offload_expired_job(0)
592
593 def test_case_1_with_expiration(self):
594 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
595
596 This tests that offload works as expected if calls are made
597 before the job finishes, before the job expires, and after
598 the job expires.
599
600 """
601 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
602 self._job.set_finished(_TEST_EXPIRATION_AGE)
603 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
604 self._job.set_expired(_TEST_EXPIRATION_AGE)
605 self._offload_expired_job(_TEST_EXPIRATION_AGE)
606
607 def test_case_2_with_expiration(self):
608 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
609
610 This tests that offload works as expected if calls are made
611 between finishing and expiration, and after the job expires.
612
613 """
614 self._job.set_finished(_TEST_EXPIRATION_AGE)
615 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
616 self._job.set_expired(_TEST_EXPIRATION_AGE)
617 self._offload_expired_job(_TEST_EXPIRATION_AGE)
618
619 def test_case_3_with_expiration(self):
620 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
621
622 This tests that offload works as expected if calls are made
623 only before finishing and after expiration.
624
625 """
626 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
627 self._job.set_expired(_TEST_EXPIRATION_AGE)
628 self._offload_expired_job(_TEST_EXPIRATION_AGE)
629
630 def test_case_4_with_expiration(self):
631 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
632
633 This tests that offload works as expected if calls are made
634 only after expiration.
635
636 """
637 self._job.set_expired(_TEST_EXPIRATION_AGE)
638 self._offload_expired_job(_TEST_EXPIRATION_AGE)
639
640
641class GetJobDirectoriesTests(_TempResultsDirTestBase):
642 """Tests for `_JobDirectory.get_job_directories()`."""
643
J. Richard Barnetteea785362014-03-17 16:00:53 -0700644 def setUp(self):
645 super(GetJobDirectoriesTests, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700646 self.make_job_hierarchy()
647 os.mkdir('not-a-job')
648 open('not-a-dir', 'w').close()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700649
650 def _run_get_directories(self, cls, expected_list):
651 """Test `get_job_directories()` for the given class.
652
653 Calls the method, and asserts that the returned list of
654 directories matches the expected return value.
655
656 @param expected_list Expected return value from the call.
657 """
J. Richard Barnetteea785362014-03-17 16:00:53 -0700658 dirlist = cls.get_job_directories()
659 self.assertEqual(set(dirlist), set(expected_list))
J. Richard Barnetteea785362014-03-17 16:00:53 -0700660
661 def test_get_regular_jobs(self):
662 """Test `RegularJobDirectory.get_job_directories()`."""
663 self._run_get_directories(job_directories.RegularJobDirectory,
J. Richard Barnette08800322014-05-16 14:49:46 -0700664 self.REGULAR_JOBLIST)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700665
666 def test_get_special_jobs(self):
667 """Test `SpecialJobDirectory.get_job_directories()`."""
668 self._run_get_directories(job_directories.SpecialJobDirectory,
J. Richard Barnette08800322014-05-16 14:49:46 -0700669 self.SPECIAL_JOBLIST)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700670
671
672class AddJobsTests(_TempResultsDirTestBase):
673 """Tests for `Offloader._add_new_jobs()`."""
674
J. Richard Barnette08800322014-05-16 14:49:46 -0700675 MOREJOBS = ['115-fubar', '116-fubar', '117-fubar', '118-snafu']
J. Richard Barnetteea785362014-03-17 16:00:53 -0700676
677 def setUp(self):
678 super(AddJobsTests, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700679 self.make_job_hierarchy()
680 self._offloader = gs_offloader.Offloader(_get_options(['-a']))
J. Richard Barnetteea785362014-03-17 16:00:53 -0700681
682 def _check_open_jobs(self, expected_key_set):
683 """Basic test assertions for `_add_new_jobs()`.
684
685 Asserts the following:
686 * The keys in the offloader's `_open_jobs` dictionary
687 matches the expected set of keys.
688 * For every job in `_open_jobs`, the job has the expected
689 directory name.
690
691 """
692 self.assertEqual(expected_key_set,
693 set(self._offloader._open_jobs.keys()))
694 for jobkey, job in self._offloader._open_jobs.items():
695 self.assertEqual(jobkey, job._dirname)
696
697 def test_add_jobs_empty(self):
698 """Test adding jobs to an empty dictionary.
699
700 Calls the offloader's `_add_new_jobs()`, then perform
701 the assertions of `self._check_open_jobs()`.
702
703 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700704 self._offloader._add_new_jobs()
705 self._check_open_jobs(set(self.REGULAR_JOBLIST) |
706 set(self.SPECIAL_JOBLIST))
J. Richard Barnetteea785362014-03-17 16:00:53 -0700707
708 def test_add_jobs_non_empty(self):
709 """Test adding jobs to a non-empty dictionary.
710
711 Calls the offloader's `_add_new_jobs()` twice; once from
712 initial conditions, and then again after adding more
713 directories. After the second call, perform the assertions
714 of `self._check_open_jobs()`. Additionally, assert that
715 keys added by the first call still map to their original
716 job object after the second call.
717
718 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700719 self._offloader._add_new_jobs()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700720 jobs_copy = self._offloader._open_jobs.copy()
J. Richard Barnette08800322014-05-16 14:49:46 -0700721 for d in self.MOREJOBS:
722 os.mkdir(d)
723 self._offloader._add_new_jobs()
724 self._check_open_jobs(set(self.REGULAR_JOBLIST) |
725 set(self.SPECIAL_JOBLIST) |
726 set(self.MOREJOBS))
J. Richard Barnetteea785362014-03-17 16:00:53 -0700727 for key in jobs_copy.keys():
728 self.assertIs(jobs_copy[key],
729 self._offloader._open_jobs[key])
730
731
732class JobStateTests(_TempResultsDirTestBase):
733 """Tests for job state predicates.
734
735 This tests for the expected results from the
736 `is_offloaded()` and `is_reportable()` predicate
737 methods.
738
739 """
740
741 def test_unfinished_job(self):
742 """Test that an unfinished job reports the correct state.
743
744 A job is "unfinished" if it isn't marked complete in the
745 database. A job in this state is neither "complete" nor
746 "reportable".
747
748 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700749 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -0700750 self.assertFalse(job.is_offloaded())
751 self.assertFalse(job.is_reportable())
752
753 def test_incomplete_job(self):
754 """Test that an incomplete job reports the correct state.
755
756 A job is "incomplete" if exactly one attempt has been made
757 to offload the job, but its results directory still exists.
758 A job in this state is neither "complete" nor "reportable".
759
760 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700761 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -0700762 job.set_incomplete()
763 self.assertFalse(job.is_offloaded())
764 self.assertFalse(job.is_reportable())
765
766 def test_reportable_job(self):
767 """Test that a reportable job reports the correct state.
768
769 A job is "reportable" if more than one attempt has been made
770 to offload the job, and its results directory still exists.
771 A job in this state is "reportable", but not "complete".
772
773 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700774 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -0700775 job.set_reportable()
776 self.assertFalse(job.is_offloaded())
777 self.assertTrue(job.is_reportable())
778
779 def test_completed_job(self):
780 """Test that a completed job reports the correct state.
781
782 A job is "completed" if at least one attempt has been made
783 to offload the job, and its results directory still exists.
784 A job in this state is "complete", and not "reportable".
785
786 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700787 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -0700788 job.set_complete()
789 self.assertTrue(job.is_offloaded())
790 self.assertFalse(job.is_reportable())
791
792
793class ReportingTests(_TempResultsDirTestBase):
794 """Tests for `Offloader._update_offload_results()`."""
795
J. Richard Barnetteea785362014-03-17 16:00:53 -0700796 def setUp(self):
797 super(ReportingTests, self).setUp()
798 self._offloader = gs_offloader.Offloader(_get_options([]))
799 self.mox.StubOutWithMock(email_manager.manager,
800 'send_email')
801
802 def _add_job(self, jobdir):
803 """Add a job to the dictionary of unfinished jobs."""
804 j = self.make_job(jobdir)
805 self._offloader._open_jobs[j._dirname] = j
806 return j
807
808 def _run_update_no_report(self, new_open_jobs):
809 """Call `_update_offload_results()` expecting no report.
810
811 Initial conditions are set up by the caller. This calls
812 `_update_offload_results()` once, and then checks these
813 assertions:
814 * The offloader's `_next_report_time` field is unchanged.
815 * The offloader's new `_open_jobs` field contains only
816 the entries in `new_open_jobs`.
817 * The email_manager's `send_email` stub wasn't called.
818
819 @param new_open_jobs A dictionary representing the expected
820 new value of the offloader's
821 `_open_jobs` field.
822 """
823 self.mox.ReplayAll()
824 next_report_time = self._offloader._next_report_time
825 self._offloader._update_offload_results()
826 self.assertEqual(next_report_time,
827 self._offloader._next_report_time)
828 self.assertEqual(self._offloader._open_jobs, new_open_jobs)
829 self.mox.VerifyAll()
830 self.mox.ResetAll()
831
832 def _run_update_with_report(self, new_open_jobs):
833 """Call `_update_offload_results()` expecting an e-mail report.
834
835 Initial conditions are set up by the caller. This calls
836 `_update_offload_results()` once, and then checks these
837 assertions:
838 * The offloader's `_next_report_time` field is updated
839 to an appropriate new time.
840 * The offloader's new `_open_jobs` field contains only
841 the entries in `new_open_jobs`.
842 * The email_manager's `send_email` stub was called.
843
844 @param new_open_jobs A dictionary representing the expected
845 new value of the offloader's
846 `_open_jobs` field.
847 """
848 email_manager.manager.send_email(
849 mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg())
850 self.mox.ReplayAll()
851 t0 = time.time() + gs_offloader.REPORT_INTERVAL_SECS
852 self._offloader._update_offload_results()
853 t1 = time.time() + gs_offloader.REPORT_INTERVAL_SECS
854 next_report_time = self._offloader._next_report_time
855 self.assertGreaterEqual(next_report_time, t0)
856 self.assertLessEqual(next_report_time, t1)
857 self.assertEqual(self._offloader._open_jobs, new_open_jobs)
858 self.mox.VerifyAll()
859 self.mox.ResetAll()
860
861 def test_no_jobs(self):
862 """Test `_update_offload_results()` with no open jobs.
863
864 Initial conditions are an empty `_open_jobs` list and
865 `_next_report_time` in the past. Expected result is no
866 e-mail report, and an empty `_open_jobs` list.
867
868 """
869 self._run_update_no_report({})
870
871 def test_all_completed(self):
872 """Test `_update_offload_results()` with only complete jobs.
873
874 Initial conditions are an `_open_jobs` list consisting of
875 only completed jobs and `_next_report_time` in the past.
876 Expected result is no e-mail report, and an empty
877 `_open_jobs` list.
878
879 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700880 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -0700881 self._add_job(d).set_complete()
882 self._run_update_no_report({})
883
884 def test_none_finished(self):
885 """Test `_update_offload_results()` with only unfinished jobs.
886
887 Initial conditions are an `_open_jobs` list consisting of
888 only unfinished jobs and `_next_report_time` in the past.
889 Expected result is no e-mail report, and no change to the
890 `_open_jobs` list.
891
892 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700893 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -0700894 self._add_job(d)
895 self._run_update_no_report(self._offloader._open_jobs.copy())
896
897 def test_none_reportable(self):
898 """Test `_update_offload_results()` with only incomplete jobs.
899
900 Initial conditions are an `_open_jobs` list consisting of
901 only incomplete jobs and `_next_report_time` in the past.
902 Expected result is no e-mail report, and no change to the
903 `_open_jobs` list.
904
905 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700906 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -0700907 self._add_job(d).set_incomplete()
908 self._run_update_no_report(self._offloader._open_jobs.copy())
909
910 def test_report_not_ready(self):
911 """Test `_update_offload_results()` e-mail throttling.
912
913 Initial conditions are an `_open_jobs` list consisting of
914 only reportable jobs but with `_next_report_time` in
915 the future. Expected result is no e-mail report, and no
916 change to the `_open_jobs` list.
917
918 """
919 # N.B. This test may fail if its run time exceeds more than
920 # about _MARGIN_SECS seconds.
J. Richard Barnette08800322014-05-16 14:49:46 -0700921 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -0700922 self._add_job(d).set_reportable()
923 self._offloader._next_report_time += _MARGIN_SECS
924 self._run_update_no_report(self._offloader._open_jobs.copy())
925
926 def test_reportable(self):
927 """Test `_update_offload_results()` with reportable jobs.
928
929 Initial conditions are an `_open_jobs` list consisting of
930 only reportable jobs and with `_next_report_time` in
931 the past. Expected result is an e-mail report, and no
932 change to the `_open_jobs` list.
933
934 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700935 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -0700936 self._add_job(d).set_reportable()
937 self._run_update_with_report(self._offloader._open_jobs.copy())
938
939
940if __name__ == '__main__':
941 unittest.main()