blob: 7d675c72ffd1717498cffa89eb2efc255e79697c [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')
62
Jakob Juelich24f22c22014-09-26 11:46:11 -070063
Simran Basidd129972014-09-11 14:34:49 -070064 def _mock_get_offload_func(self, is_moblab):
65 """Mock the process of getting the offload_dir function."""
66 if is_moblab:
67 expected_gsuri = '%sresults/%s/%s/' % (
68 global_config.global_config.get_config_value(
69 'CROS', 'image_storage_server'),
70 'Fa:ke:ma:c0:12:34', 'rand0m-uu1d')
71 else:
72 expected_gsuri = utils.DEFAULT_OFFLOAD_GSURI
73 utils.get_offload_gsuri().AndReturn(expected_gsuri)
74 offload_func = gs_offloader.get_offload_dir_func(expected_gsuri)
75 self.mox.StubOutWithMock(gs_offloader, 'get_offload_dir_func')
76 gs_offloader.get_offload_dir_func(expected_gsuri).AndReturn(
77 offload_func)
78 self.mox.ReplayAll()
79 return offload_func
80
Jakob Juelich24f22c22014-09-26 11:46:11 -070081
J. Richard Barnetteea785362014-03-17 16:00:53 -070082 def test_process_no_options(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070083 """Test default offloader options."""
Simran Basidd129972014-09-11 14:34:49 -070084 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070085 offloader = gs_offloader.Offloader(_get_options([]))
86 self.assertEqual(set(offloader._jobdir_classes),
87 self._REGULAR_ONLY)
88 self.assertEqual(offloader._processes, 1)
89 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -070090 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070091 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -070092
Jakob Juelich24f22c22014-09-26 11:46:11 -070093
J. Richard Barnetteea785362014-03-17 16:00:53 -070094 def test_process_all_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070095 """Test offloader handling for the --all option."""
Simran Basidd129972014-09-11 14:34:49 -070096 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -070097 offloader = gs_offloader.Offloader(_get_options(['--all']))
98 self.assertEqual(set(offloader._jobdir_classes), self._BOTH)
99 self.assertEqual(offloader._processes, 1)
100 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700101 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700102 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700103
Jakob Juelich24f22c22014-09-26 11:46:11 -0700104
J. Richard Barnetteea785362014-03-17 16:00:53 -0700105 def test_process_hosts_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700106 """Test offloader handling for the --hosts option."""
Simran Basidd129972014-09-11 14:34:49 -0700107 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700108 offloader = gs_offloader.Offloader(
109 _get_options(['--hosts']))
110 self.assertEqual(set(offloader._jobdir_classes),
111 self._SPECIAL_ONLY)
112 self.assertEqual(offloader._processes, 1)
113 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700114 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700115 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700116
Jakob Juelich24f22c22014-09-26 11:46:11 -0700117
J. Richard Barnetteea785362014-03-17 16:00:53 -0700118 def test_parallelism_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700119 """Test offloader handling for the --parallelism option."""
Simran Basidd129972014-09-11 14:34:49 -0700120 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700121 offloader = gs_offloader.Offloader(
122 _get_options(['--parallelism', '2']))
123 self.assertEqual(set(offloader._jobdir_classes),
124 self._REGULAR_ONLY)
125 self.assertEqual(offloader._processes, 2)
126 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700127 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700128 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700129
Jakob Juelich24f22c22014-09-26 11:46:11 -0700130
J. Richard Barnetteea785362014-03-17 16:00:53 -0700131 def test_delete_only_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700132 """Test offloader handling for the --delete_only option."""
133 offloader = gs_offloader.Offloader(
134 _get_options(['--delete_only']))
135 self.assertEqual(set(offloader._jobdir_classes),
136 self._REGULAR_ONLY)
137 self.assertEqual(offloader._processes, 1)
138 self.assertEqual(offloader._offload_func,
139 gs_offloader.delete_files)
140 self.assertEqual(offloader._age_limit, 0)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700141
Jakob Juelich24f22c22014-09-26 11:46:11 -0700142
J. Richard Barnetteea785362014-03-17 16:00:53 -0700143 def test_delete_only_option(self):
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700144 """Test offloader handling for the --days_old option."""
Simran Basidd129972014-09-11 14:34:49 -0700145 offload_func = self._mock_get_offload_func(False)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700146 offloader = gs_offloader.Offloader(
147 _get_options(['--days_old', '7']))
148 self.assertEqual(set(offloader._jobdir_classes),
149 self._REGULAR_ONLY)
150 self.assertEqual(offloader._processes, 1)
151 self.assertEqual(offloader._offload_func,
Simran Basidd129972014-09-11 14:34:49 -0700152 offload_func)
J. Richard Barnetteef4b47d2014-05-19 07:52:08 -0700153 self.assertEqual(offloader._age_limit, 7)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700154
Jakob Juelich24f22c22014-09-26 11:46:11 -0700155
Simran Basidd129972014-09-11 14:34:49 -0700156 def test_moblab_gsuri_generation(self):
157 """Test offloader construction for Moblab."""
158 offload_func = self._mock_get_offload_func(True)
159 offloader = gs_offloader.Offloader(_get_options([]))
160 self.assertEqual(set(offloader._jobdir_classes),
161 self._REGULAR_ONLY)
162 self.assertEqual(offloader._processes, 1)
163 self.assertEqual(offloader._offload_func,
164 offload_func)
165 self.assertEqual(offloader._age_limit, 0)
166
J. Richard Barnetteea785362014-03-17 16:00:53 -0700167
168def _make_timestamp(age_limit, is_expired):
169 """Create a timestamp for use by `job_directories._is_job_expired()`.
170
171 The timestamp will meet the syntactic requirements for
172 timestamps used as input to `_is_job_expired()`. If
173 `is_expired` is true, the timestamp will be older than
174 `age_limit` days before the current time; otherwise, the
175 date will be younger.
176
177 @param age_limit The number of days before expiration of the
178 target timestamp.
179 @param is_expired Whether the timestamp should be expired
180 relative to `age_limit`.
181
182 """
183 seconds = -_MARGIN_SECS
184 if is_expired:
185 seconds = -seconds
186 delta = datetime.timedelta(days=age_limit, seconds=seconds)
187 reference_time = datetime.datetime.now() - delta
Dan Shidfea3682014-08-10 23:38:40 -0700188 return reference_time.strftime(time_utils.TIME_FMT)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700189
190
191class JobExpirationTests(unittest.TestCase):
192 """Tests to exercise `job_directories._is_job_expired()`."""
193
194 def test_expired(self):
195 """Test detection of an expired job."""
196 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, True)
197 self.assertTrue(
198 job_directories._is_job_expired(
199 _TEST_EXPIRATION_AGE, timestamp))
200
201
202 def test_alive(self):
203 """Test detection of a job that's not expired."""
204 # N.B. This test may fail if its run time exceeds more than
205 # about _MARGIN_SECS seconds.
206 timestamp = _make_timestamp(_TEST_EXPIRATION_AGE, False)
207 self.assertFalse(
208 job_directories._is_job_expired(
209 _TEST_EXPIRATION_AGE, timestamp))
210
211
212class _MockJobDirectory(job_directories._JobDirectory):
213 """Subclass of `_JobDirectory` used as a helper for tests."""
214
215 GLOB_PATTERN = '[0-9]*-*'
216
Jakob Juelich24f22c22014-09-26 11:46:11 -0700217
J. Richard Barnetteea785362014-03-17 16:00:53 -0700218 def __init__(self, resultsdir):
219 """Create new job in initial state."""
220 super(_MockJobDirectory, self).__init__(resultsdir)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700221 self._timestamp = None
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700222 self.queue_args = [resultsdir, os.path.dirname(resultsdir)]
J. Richard Barnetteea785362014-03-17 16:00:53 -0700223
Jakob Juelich24f22c22014-09-26 11:46:11 -0700224
J. Richard Barnetteea785362014-03-17 16:00:53 -0700225 def get_timestamp_if_finished(self):
226 return self._timestamp
227
Jakob Juelich24f22c22014-09-26 11:46:11 -0700228
J. Richard Barnetteea785362014-03-17 16:00:53 -0700229 def set_finished(self, days_old):
230 """Make this job appear to be finished.
231
232 After calling this function, calls to `enqueue_offload()`
233 will find this job as finished, but not expired and ready
234 for offload. Note that when `days_old` is 0,
235 `enqueue_offload()` will treat a finished job as eligible
236 for offload.
237
238 @param days_old The value of the `days_old` parameter that
239 will be passed to `enqueue_offload()` for
240 testing.
241
242 """
243 self._timestamp = _make_timestamp(days_old, False)
244
Jakob Juelich24f22c22014-09-26 11:46:11 -0700245
J. Richard Barnetteea785362014-03-17 16:00:53 -0700246 def set_expired(self, days_old):
247 """Make this job eligible to be offloaded.
248
249 After calling this function, calls to `offload` will attempt
250 to offload this job.
251
252 @param days_old The value of the `days_old` parameter that
253 will be passed to `enqueue_offload()` for
254 testing.
255
256 """
257 self._timestamp = _make_timestamp(days_old, True)
258
Jakob Juelich24f22c22014-09-26 11:46:11 -0700259
J. Richard Barnetteea785362014-03-17 16:00:53 -0700260 def set_incomplete(self):
261 """Make this job appear to have failed offload just once."""
262 self._offload_count += 1
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700263 self._first_offload_start = time.time()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700264 if not os.path.isdir(self._dirname):
265 os.mkdir(self._dirname)
266
Jakob Juelich24f22c22014-09-26 11:46:11 -0700267
J. Richard Barnetteea785362014-03-17 16:00:53 -0700268 def set_reportable(self):
269 """Make this job be reportable."""
J. Richard Barnetteea785362014-03-17 16:00:53 -0700270 self.set_incomplete()
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700271 self._offload_count += 1
J. Richard Barnetteea785362014-03-17 16:00:53 -0700272
Jakob Juelich24f22c22014-09-26 11:46:11 -0700273
J. Richard Barnetteea785362014-03-17 16:00:53 -0700274 def set_complete(self):
275 """Make this job be completed."""
276 self._offload_count += 1
277 if os.path.isdir(self._dirname):
278 os.rmdir(self._dirname)
279
280
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700281class CommandListTests(unittest.TestCase):
282 """Tests for `get_cmd_list()`."""
283
Jakob Juelich24f22c22014-09-26 11:46:11 -0700284 def _command_list_assertions(self, job, use_rsync=True):
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700285 """Call `get_cmd_list()` and check the return value.
286
287 Check the following assertions:
288 * The command name (argv[0]) is 'gsutil'.
289 * The arguments contain the 'cp' subcommand.
290 * The next-to-last argument (the source directory) is the
291 job's `queue_args[0]`.
Simran Basidd129972014-09-11 14:34:49 -0700292 * The last argument (the destination URL) is the job's
293 'queue_args[1]'.
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700294
295 @param job A job with properly calculated arguments to
296 `get_cmd_list()`
297
298 """
Jakob Juelich24f22c22014-09-26 11:46:11 -0700299 test_bucket_uri = 'gs://a-test-bucket'
300
301 gs_offloader.USE_RSYNC_ENABLED = use_rsync
302
303 command = gs_offloader.get_cmd_list(
304 job.queue_args[0],
305 os.path.join(test_bucket_uri, job.queue_args[1]))
306
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700307 self.assertEqual(command[0], 'gsutil')
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700308 self.assertEqual(command[-2], job.queue_args[0])
Jakob Juelich24f22c22014-09-26 11:46:11 -0700309
310 if use_rsync:
311 self.assertTrue('rsync' in command)
312 self.assertEqual(command[-1],
313 os.path.join(test_bucket_uri, job.queue_args[0]))
314 else:
315 self.assertTrue('cp' in command)
316 self.assertEqual(command[-1],
317 os.path.join(test_bucket_uri, job.queue_args[1]))
318
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700319
320 def test_get_cmd_list_regular(self):
321 """Test `get_cmd_list()` as for a regular job."""
322 job = _MockJobDirectory('118-debug')
323 self._command_list_assertions(job)
324
Jakob Juelich24f22c22014-09-26 11:46:11 -0700325
J. Richard Barnette9f4be0d2014-05-20 12:28:31 -0700326 def test_get_cmd_list_special(self):
327 """Test `get_cmd_list()` as for a special job."""
328 job = _MockJobDirectory('hosts/host1/118-reset')
329 self._command_list_assertions(job)
330
331
Jakob Juelich24f22c22014-09-26 11:46:11 -0700332 def test_get_cmd_list_regular_no_rsync(self):
333 """Test `get_cmd_list()` as for a regular job."""
334 job = _MockJobDirectory('118-debug')
335 self._command_list_assertions(job, use_rsync=False)
336
337
338 def test_get_cmd_list_special_no_rsync(self):
339 """Test `get_cmd_list()` as for a special job."""
340 job = _MockJobDirectory('hosts/host1/118-reset')
341 self._command_list_assertions(job, use_rsync=False)
342
343
J. Richard Barnetteea785362014-03-17 16:00:53 -0700344# Below is partial sample of e-mail notification text. This text is
345# deliberately hard-coded and then parsed to create the test data;
346# the idea is to make sure the actual text format will be reviewed
347# by a human being.
348#
349# first offload count directory
350# --+----1----+---- ----+ ----+----1----+----2----+----3
351_SAMPLE_DIRECTORIES_REPORT = '''\
352=================== ====== ==============================
3532014-03-14 15:09:26 1 118-fubar
3542014-03-14 15:19:23 2 117-fubar
3552014-03-14 15:29:20 6 116-fubar
3562014-03-14 15:39:17 24 115-fubar
3572014-03-14 15:49:14 120 114-fubar
3582014-03-14 15:59:11 720 113-fubar
3592014-03-14 16:09:08 5040 112-fubar
3602014-03-14 16:19:05 40320 111-fubar
361'''
362
363
364class EmailTemplateTests(mox.MoxTestBase):
365 """Test the formatting of e-mail notifications."""
366
367 def setUp(self):
368 super(EmailTemplateTests, self).setUp()
369 self.mox.StubOutWithMock(email_manager.manager,
370 'send_email')
371 self._joblist = []
372 for line in _SAMPLE_DIRECTORIES_REPORT.split('\n')[1 : -1]:
373 date_, time_, count, dir_ = line.split()
374 job = _MockJobDirectory(dir_)
375 job._offload_count = int(count)
Dan Shidfea3682014-08-10 23:38:40 -0700376 timestruct = time.strptime("%s %s" % (date_, time_),
377 gs_offloader.ERROR_EMAIL_TIME_FORMAT)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700378 job._first_offload_start = time.mktime(timestruct)
379 # enter the jobs in reverse order, to make sure we
380 # test that the output will be sorted.
381 self._joblist.insert(0, job)
382
Jakob Juelich24f22c22014-09-26 11:46:11 -0700383
J. Richard Barnetteea785362014-03-17 16:00:53 -0700384 def test_email_template(self):
385 """Trigger an e-mail report and check its contents."""
386 # The last line of the report is a separator that we
387 # repeat in the first line of our expected result data.
388 # So, we remove that separator from the end of the of
389 # the e-mail report message.
390 #
391 # The last element in the list returned by split('\n')
392 # will be an empty string, so to remove the separator,
393 # we remove the next-to-last entry in the list.
394 report_lines = gs_offloader.ERROR_EMAIL_REPORT_FORMAT.split('\n')
395 expected_message = ('\n'.join(report_lines[: -2] +
396 report_lines[-1 :]) +
397 _SAMPLE_DIRECTORIES_REPORT)
398 email_manager.manager.send_email(
399 mox.IgnoreArg(), mox.IgnoreArg(), expected_message)
400 self.mox.ReplayAll()
401 gs_offloader.report_offload_failures(self._joblist)
402
403
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700404class JobDirectorySubclassTests(mox.MoxTestBase):
405 """Test specific to RegularJobDirectory and SpecialJobDirectory.
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700406
407 This provides coverage for the implementation in both
408 RegularJobDirectory and SpecialJobDirectory.
409
410 """
411
412 def setUp(self):
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700413 super(JobDirectorySubclassTests, self).setUp()
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700414 self.mox.StubOutWithMock(job_directories._AFE, 'run')
415
Jakob Juelich24f22c22014-09-26 11:46:11 -0700416
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700417 def test_regular_job_fields(self):
418 """Test the constructor for `RegularJobDirectory`.
419
420 Construct a regular job, and assert that the `_dirname`
421 and `_id` attributes are set as expected.
422
423 """
424 resultsdir = '118-fubar'
425 job = job_directories.RegularJobDirectory(resultsdir)
426 self.assertEqual(job._dirname, resultsdir)
427 self.assertEqual(job._id, '118')
428
Jakob Juelich24f22c22014-09-26 11:46:11 -0700429
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700430 def test_special_job_fields(self):
431 """Test the constructor for `SpecialJobDirectory`.
432
433 Construct a special job, and assert that the `_dirname`
434 and `_id` attributes are set as expected.
435
436 """
437 destdir = 'hosts/host1'
438 resultsdir = destdir + '/118-reset'
439 job = job_directories.SpecialJobDirectory(resultsdir)
440 self.assertEqual(job._dirname, resultsdir)
441 self.assertEqual(job._id, '118')
442
Jakob Juelich24f22c22014-09-26 11:46:11 -0700443
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700444 def test_finished_regular_job(self):
445 """Test getting the timestamp for a finished regular job.
446
447 Tests the return value for
448 `RegularJobDirectory.get_timestamp_if_finished()` when
449 the AFE indicates the job is finished.
450
451 """
452 job = job_directories.RegularJobDirectory('118-fubar')
453 timestamp = _make_timestamp(0, True)
454 job_directories._AFE.run(
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700455 'get_jobs', id=job._id, finished=True).AndReturn(
Simran Basifb98e462014-08-18 12:35:44 -0700456 [{'created_on': _make_timestamp(0, True)}])
457 job_directories._AFE.run(
458 'get_host_queue_entries', finished_on__isnull=False,
459 job_id=job._id).AndReturn(
460 [{'finished_on': timestamp}])
461 self.mox.ReplayAll()
462 self.assertEqual(timestamp,
463 job.get_timestamp_if_finished())
464
Jakob Juelich24f22c22014-09-26 11:46:11 -0700465
Simran Basifb98e462014-08-18 12:35:44 -0700466 def test_finished_regular_job_multiple_hqes(self):
467 """Test getting the timestamp for a regular job with multiple hqes.
468
469 Tests the return value for
470 `RegularJobDirectory.get_timestamp_if_finished()` when
471 the AFE indicates the job is finished and the job has multiple host
472 queue entries.
473
474 If there is more than one HQE, this test guarantees latest timestamp is
475 the one returned.
476 """
477 job = job_directories.RegularJobDirectory('118-fubar')
478 created_timestamp = _make_timestamp(2, True)
479 older_hqe_timestamp = _make_timestamp(1, True)
480 newer_hqe_timestamp = _make_timestamp(0, True)
481 job_directories._AFE.run(
482 'get_jobs', id=job._id, finished=True).AndReturn(
483 [{'created_on': created_timestamp}])
484 job_directories._AFE.run(
485 'get_host_queue_entries', finished_on__isnull=False,
486 job_id=job._id).AndReturn(
487 [{'finished_on': older_hqe_timestamp},
488 {'finished_on': newer_hqe_timestamp}])
489 # For the second call return the latest hqe last.
490 job_directories._AFE.run(
491 'get_jobs', id=job._id, finished=True).AndReturn(
492 [{'created_on': created_timestamp}])
493 job_directories._AFE.run(
494 'get_host_queue_entries', finished_on__isnull=False,
495 job_id=job._id).AndReturn(
496 [{'finished_on': newer_hqe_timestamp},
497 {'finished_on': older_hqe_timestamp}])
498 self.mox.ReplayAll()
499 self.assertEqual(newer_hqe_timestamp,
500 job.get_timestamp_if_finished())
501 self.assertEqual(newer_hqe_timestamp,
502 job.get_timestamp_if_finished())
503
Jakob Juelich24f22c22014-09-26 11:46:11 -0700504
Simran Basifb98e462014-08-18 12:35:44 -0700505 def test_finished_regular_job_null_finished_times(self):
506 """Test getting the timestamp for an aborted regular job.
507
508 Tests the return value for
509 `RegularJobDirectory.get_timestamp_if_finished()` when
510 the AFE indicates the job is finished and the job has aborted host
511 queue entries.
512
513 """
514 job = job_directories.RegularJobDirectory('118-fubar')
515 timestamp = _make_timestamp(0, True)
516 job_directories._AFE.run(
517 'get_jobs', id=job._id, finished=True).AndReturn(
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700518 [{'created_on': timestamp}])
Simran Basifb98e462014-08-18 12:35:44 -0700519 job_directories._AFE.run(
520 'get_host_queue_entries', finished_on__isnull=False,
521 job_id=job._id).AndReturn([])
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700522 self.mox.ReplayAll()
523 self.assertEqual(timestamp,
524 job.get_timestamp_if_finished())
525
Jakob Juelich24f22c22014-09-26 11:46:11 -0700526
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700527 def test_unfinished_regular_job(self):
528 """Test getting the timestamp for an unfinished regular job.
529
530 Tests the return value for
531 `RegularJobDirectory.get_timestamp_if_finished()` when
532 the AFE indicates the job is not finished.
533
534 """
535 job = job_directories.RegularJobDirectory('118-fubar')
536 job_directories._AFE.run(
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700537 'get_jobs', id=job._id, finished=True).AndReturn(None)
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700538 self.mox.ReplayAll()
539 self.assertIsNone(job.get_timestamp_if_finished())
540
Jakob Juelich24f22c22014-09-26 11:46:11 -0700541
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700542 def test_finished_special_job(self):
543 """Test getting the timestamp for a finished special job.
544
545 Tests the return value for
546 `SpecialJobDirectory.get_timestamp_if_finished()` when
547 the AFE indicates the job is finished.
548
549 """
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700550 job = job_directories.SpecialJobDirectory(
551 'hosts/host1/118-reset')
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700552 timestamp = _make_timestamp(0, True)
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700553 job_directories._AFE.run('get_special_tasks',
554 id=job._id,
555 is_complete=True).AndReturn(
Simran Basifb98e462014-08-18 12:35:44 -0700556 [{'time_finished': timestamp}])
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700557 self.mox.ReplayAll()
558 self.assertEqual(timestamp,
559 job.get_timestamp_if_finished())
560
Jakob Juelich24f22c22014-09-26 11:46:11 -0700561
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700562 def test_unfinished_special_job(self):
563 """Test getting the timestamp for an unfinished special job.
564
565 Tests the return value for
566 `SpecialJobDirectory.get_timestamp_if_finished()` when
567 the AFE indicates the job is not finished.
568
569 """
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700570 job = job_directories.SpecialJobDirectory(
571 'hosts/host1/118-reset')
572 job_directories._AFE.run('get_special_tasks',
573 id=job._id,
574 is_complete=True).AndReturn(None)
J. Richard Barnette3e3ed6a2014-05-19 07:59:00 -0700575 self.mox.ReplayAll()
576 self.assertIsNone(job.get_timestamp_if_finished())
577
578
J. Richard Barnetteea785362014-03-17 16:00:53 -0700579class _TempResultsDirTestBase(mox.MoxTestBase):
580 """Base class for tests using a temporary results directory."""
581
J. Richard Barnette08800322014-05-16 14:49:46 -0700582 REGULAR_JOBLIST = [
583 '111-fubar', '112-fubar', '113-fubar', '114-snafu']
584 HOST_LIST = ['host1', 'host2', 'host3']
585 SPECIAL_JOBLIST = [
586 'hosts/host1/333-reset', 'hosts/host1/334-reset',
587 'hosts/host2/444-reset', 'hosts/host3/555-reset']
588
Jakob Juelich24f22c22014-09-26 11:46:11 -0700589
J. Richard Barnetteea785362014-03-17 16:00:53 -0700590 def setUp(self):
591 super(_TempResultsDirTestBase, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700592 self._resultsroot = tempfile.mkdtemp()
593 self._cwd = os.getcwd()
594 os.chdir(self._resultsroot)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700595
Jakob Juelich24f22c22014-09-26 11:46:11 -0700596
J. Richard Barnetteea785362014-03-17 16:00:53 -0700597 def tearDown(self):
J. Richard Barnette08800322014-05-16 14:49:46 -0700598 os.chdir(self._cwd)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700599 shutil.rmtree(self._resultsroot)
600 super(_TempResultsDirTestBase, self).tearDown()
601
Jakob Juelich24f22c22014-09-26 11:46:11 -0700602
J. Richard Barnetteea785362014-03-17 16:00:53 -0700603 def make_job(self, jobdir):
604 """Create a job with results in `self._resultsroot`.
605
606 @param jobdir Name of the subdirectory to be created in
607 `self._resultsroot`.
608
609 """
J. Richard Barnette08800322014-05-16 14:49:46 -0700610 os.mkdir(jobdir)
611 return _MockJobDirectory(jobdir)
612
Jakob Juelich24f22c22014-09-26 11:46:11 -0700613
J. Richard Barnette08800322014-05-16 14:49:46 -0700614 def make_job_hierarchy(self):
615 """Create a sample hierarchy of job directories.
616
617 `self.REGULAR_JOBLIST` is a list of directories for regular
618 jobs to be created; `self.SPECIAL_JOBLIST` is a list of
619 directories for special jobs to be created.
620
621 """
622 for d in self.REGULAR_JOBLIST:
623 os.mkdir(d)
624 hostsdir = 'hosts'
625 os.mkdir(hostsdir)
626 for host in self.HOST_LIST:
627 os.mkdir(os.path.join(hostsdir, host))
628 for d in self.SPECIAL_JOBLIST:
629 os.mkdir(d)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700630
631
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700632class OffloadDirectoryTests(_TempResultsDirTestBase):
633 """Tests for `offload_dir()`."""
634
635 def setUp(self):
636 super(OffloadDirectoryTests, self).setUp()
637 # offload_dir() logs messages; silence them.
638 self._saved_loglevel = logging.getLogger().getEffectiveLevel()
639 logging.getLogger().setLevel(logging.CRITICAL+1)
640 self._job = self.make_job(self.REGULAR_JOBLIST[0])
641 self.mox.StubOutWithMock(gs_offloader, 'get_cmd_list')
642 self.mox.StubOutWithMock(signal, 'alarm')
643
Jakob Juelich24f22c22014-09-26 11:46:11 -0700644
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700645 def tearDown(self):
646 logging.getLogger().setLevel(self._saved_loglevel)
647 super(OffloadDirectoryTests, self).tearDown()
648
Jakob Juelich24f22c22014-09-26 11:46:11 -0700649
Simran Basidd129972014-09-11 14:34:49 -0700650 def _mock_offload_dir_calls(self, command, queue_args):
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700651 """Mock out the calls needed by `offload_dir()`.
652
653 This covers only the calls made when there is no timeout.
654
655 @param command Command list to be returned by the mocked
656 call to `get_cmd_list()`.
657
658 """
659 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS)
Simran Basidd129972014-09-11 14:34:49 -0700660 command.append(queue_args[0])
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700661 gs_offloader.get_cmd_list(
Simran Basidd129972014-09-11 14:34:49 -0700662 queue_args[0], '%s%s' % (utils.DEFAULT_OFFLOAD_GSURI,
663 queue_args[1])).AndReturn(
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700664 command)
665 signal.alarm(0)
666 signal.alarm(0)
667
Jakob Juelich24f22c22014-09-26 11:46:11 -0700668
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700669 def _run_offload_dir(self, should_succeed):
670 """Make one call to `offload_dir()`.
671
672 The caller ensures all mocks are set up already.
673
674 @param should_succeed True iff the call to `offload_dir()`
675 is expected to succeed and remove the
676 offloaded job directory.
677
678 """
679 self.mox.ReplayAll()
Simran Basidd129972014-09-11 14:34:49 -0700680 gs_offloader.get_offload_dir_func(
681 utils.DEFAULT_OFFLOAD_GSURI)(self._job.queue_args[0],
682 self._job.queue_args[1])
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700683 self.mox.VerifyAll()
684 self.assertEqual(not should_succeed,
685 os.path.isdir(self._job.queue_args[0]))
686
Jakob Juelich24f22c22014-09-26 11:46:11 -0700687
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700688 def test_offload_success(self):
689 """Test that `offload_dir()` can succeed correctly."""
Simran Basidd129972014-09-11 14:34:49 -0700690 self._mock_offload_dir_calls(['test', '-d'],
691 self._job.queue_args)
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700692 self._run_offload_dir(True)
693
Jakob Juelich24f22c22014-09-26 11:46:11 -0700694
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700695 def test_offload_failure(self):
696 """Test that `offload_dir()` can fail correctly."""
Simran Basidd129972014-09-11 14:34:49 -0700697 self._mock_offload_dir_calls(['test', '!', '-d'],
698 self._job.queue_args)
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700699 self._run_offload_dir(False)
700
Jakob Juelich24f22c22014-09-26 11:46:11 -0700701
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700702 def test_offload_timeout_early(self):
703 """Test that `offload_dir()` times out correctly.
704
705 This test triggers timeout at the earliest possible moment,
706 at the first call to set the timeout alarm.
707
708 """
709 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS).AndRaise(
710 gs_offloader.TimeoutException('fubar'))
711 signal.alarm(0)
712 self._run_offload_dir(False)
713
Jakob Juelich24f22c22014-09-26 11:46:11 -0700714
J. Richard Barnette2e443ef2014-05-20 12:31:35 -0700715 def test_offload_timeout_late(self):
716 """Test that `offload_dir()` times out correctly.
717
718 This test triggers timeout at the latest possible moment, at
719 the call to clear the timeout alarm.
720
721 """
722 signal.alarm(gs_offloader.OFFLOAD_TIMEOUT_SECS)
723 gs_offloader.get_cmd_list(
724 mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
725 ['test', '-d', self._job.queue_args[0]])
726 signal.alarm(0).AndRaise(
727 gs_offloader.TimeoutException('fubar'))
728 signal.alarm(0)
729 self._run_offload_dir(False)
730
731
J. Richard Barnetteea785362014-03-17 16:00:53 -0700732class JobDirectoryOffloadTests(_TempResultsDirTestBase):
733 """Tests for `_JobDirectory.enqueue_offload()`.
734
735 When testing with a `days_old` parameter of 0, we use
736 `set_finished()` instead of `set_expired()`. This causes the
737 job's timestamp to be set in the future. This is done so as
738 to test that when `days_old` is 0, the job is always treated
739 as eligible for offload, regardless of the timestamp's value.
740
741 Testing covers the following assertions:
742 A. Each time `enqueue_offload()` is called, a message that
743 includes the job's directory name will be logged using
744 `logging.debug()`, regardless of whether the job was
745 enqueued. Nothing else is allowed to be logged.
746 B. If the job is not eligible to be offloaded,
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700747 `get_failure_time()` and `get_failure_count()` are 0.
J. Richard Barnetteea785362014-03-17 16:00:53 -0700748 C. If the job is not eligible for offload, nothing is
749 enqueued in `queue`.
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700750 D. When the job is offloaded, `get_failure_count()` increments
J. Richard Barnetteea785362014-03-17 16:00:53 -0700751 each time.
752 E. When the job is offloaded, the appropriate parameters are
753 enqueued exactly once.
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700754 F. The first time a job is offloaded, `get_failure_time()` is
J. Richard Barnetteea785362014-03-17 16:00:53 -0700755 set to the current time.
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700756 G. `get_failure_time()` only changes the first time that the
J. Richard Barnetteea785362014-03-17 16:00:53 -0700757 job is offloaded.
758
759 The test cases below are designed to exercise all of the
760 meaningful state transitions at least once.
761
762 """
763
764 def setUp(self):
765 super(JobDirectoryOffloadTests, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700766 self._job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -0700767 self._queue = Queue.Queue()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700768
Jakob Juelich24f22c22014-09-26 11:46:11 -0700769
J. Richard Barnetteea785362014-03-17 16:00:53 -0700770 def _offload_unexpired_job(self, days_old):
771 """Make calls to `enqueue_offload()` for an unexpired job.
772
773 This method tests assertions B and C that calling
774 `enqueue_offload()` has no effect.
775
776 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700777 self.assertEqual(self._job.get_failure_count(), 0)
778 self.assertEqual(self._job.get_failure_time(), 0)
779 self._job.enqueue_offload(self._queue, days_old)
780 self._job.enqueue_offload(self._queue, days_old)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700781 self.assertTrue(self._queue.empty())
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700782 self.assertEqual(self._job.get_failure_count(), 0)
783 self.assertEqual(self._job.get_failure_time(), 0)
784 self.assertFalse(self._job.is_reportable())
J. Richard Barnetteea785362014-03-17 16:00:53 -0700785
Jakob Juelich24f22c22014-09-26 11:46:11 -0700786
J. Richard Barnetteea785362014-03-17 16:00:53 -0700787 def _offload_expired_once(self, days_old, count):
788 """Make one call to `enqueue_offload()` for an expired job.
789
790 This method tests assertions D and E regarding side-effects
791 expected when a job is offloaded.
792
793 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700794 self._job.enqueue_offload(self._queue, days_old)
795 self.assertEqual(self._job.get_failure_count(), count)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700796 self.assertFalse(self._queue.empty())
797 v = self._queue.get_nowait()
798 self.assertTrue(self._queue.empty())
J. Richard Barnette2c72ddd2014-05-20 12:17:37 -0700799 self.assertEqual(v, self._job.queue_args)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700800
Jakob Juelich24f22c22014-09-26 11:46:11 -0700801
J. Richard Barnetteea785362014-03-17 16:00:53 -0700802 def _offload_expired_job(self, days_old):
803 """Make calls to `enqueue_offload()` for a just-expired job.
804
805 This method directly tests assertions F and G regarding
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700806 side-effects on `get_failure_time()`.
J. Richard Barnetteea785362014-03-17 16:00:53 -0700807
808 """
809 t0 = time.time()
810 self._offload_expired_once(days_old, 1)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700811 self.assertFalse(self._job.is_reportable())
812 t1 = self._job.get_failure_time()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700813 self.assertLessEqual(t1, time.time())
814 self.assertGreaterEqual(t1, t0)
815 self._offload_expired_once(days_old, 2)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700816 self.assertTrue(self._job.is_reportable())
817 self.assertEqual(self._job.get_failure_time(), t1)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700818 self._offload_expired_once(days_old, 3)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700819 self.assertTrue(self._job.is_reportable())
820 self.assertEqual(self._job.get_failure_time(), t1)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700821
Jakob Juelich24f22c22014-09-26 11:46:11 -0700822
J. Richard Barnetteea785362014-03-17 16:00:53 -0700823 def test_case_1_no_expiration(self):
824 """Test a series of `enqueue_offload()` calls with `days_old` of 0.
825
826 This tests that offload works as expected if calls are
827 made both before and after the job becomes expired.
828
829 """
830 self._offload_unexpired_job(0)
831 self._job.set_finished(0)
832 self._offload_expired_job(0)
833
Jakob Juelich24f22c22014-09-26 11:46:11 -0700834
J. Richard Barnetteea785362014-03-17 16:00:53 -0700835 def test_case_2_no_expiration(self):
836 """Test a series of `enqueue_offload()` calls with `days_old` of 0.
837
838 This tests that offload works as expected if calls are made
839 only after the job becomes expired.
840
841 """
842 self._job.set_finished(0)
843 self._offload_expired_job(0)
844
Jakob Juelich24f22c22014-09-26 11:46:11 -0700845
J. Richard Barnetteea785362014-03-17 16:00:53 -0700846 def test_case_1_with_expiration(self):
847 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
848
849 This tests that offload works as expected if calls are made
850 before the job finishes, before the job expires, and after
851 the job expires.
852
853 """
854 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
855 self._job.set_finished(_TEST_EXPIRATION_AGE)
856 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
857 self._job.set_expired(_TEST_EXPIRATION_AGE)
858 self._offload_expired_job(_TEST_EXPIRATION_AGE)
859
Jakob Juelich24f22c22014-09-26 11:46:11 -0700860
J. Richard Barnetteea785362014-03-17 16:00:53 -0700861 def test_case_2_with_expiration(self):
862 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
863
864 This tests that offload works as expected if calls are made
865 between finishing and expiration, and after the job expires.
866
867 """
868 self._job.set_finished(_TEST_EXPIRATION_AGE)
869 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
870 self._job.set_expired(_TEST_EXPIRATION_AGE)
871 self._offload_expired_job(_TEST_EXPIRATION_AGE)
872
Jakob Juelich24f22c22014-09-26 11:46:11 -0700873
J. Richard Barnetteea785362014-03-17 16:00:53 -0700874 def test_case_3_with_expiration(self):
875 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
876
877 This tests that offload works as expected if calls are made
878 only before finishing and after expiration.
879
880 """
881 self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
882 self._job.set_expired(_TEST_EXPIRATION_AGE)
883 self._offload_expired_job(_TEST_EXPIRATION_AGE)
884
Jakob Juelich24f22c22014-09-26 11:46:11 -0700885
J. Richard Barnetteea785362014-03-17 16:00:53 -0700886 def test_case_4_with_expiration(self):
887 """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
888
889 This tests that offload works as expected if calls are made
890 only after expiration.
891
892 """
893 self._job.set_expired(_TEST_EXPIRATION_AGE)
894 self._offload_expired_job(_TEST_EXPIRATION_AGE)
895
896
897class GetJobDirectoriesTests(_TempResultsDirTestBase):
898 """Tests for `_JobDirectory.get_job_directories()`."""
899
J. Richard Barnetteea785362014-03-17 16:00:53 -0700900 def setUp(self):
901 super(GetJobDirectoriesTests, self).setUp()
J. Richard Barnette08800322014-05-16 14:49:46 -0700902 self.make_job_hierarchy()
903 os.mkdir('not-a-job')
904 open('not-a-dir', 'w').close()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700905
Jakob Juelich24f22c22014-09-26 11:46:11 -0700906
J. Richard Barnetteea785362014-03-17 16:00:53 -0700907 def _run_get_directories(self, cls, expected_list):
908 """Test `get_job_directories()` for the given class.
909
910 Calls the method, and asserts that the returned list of
911 directories matches the expected return value.
912
913 @param expected_list Expected return value from the call.
914 """
J. Richard Barnetteea785362014-03-17 16:00:53 -0700915 dirlist = cls.get_job_directories()
916 self.assertEqual(set(dirlist), set(expected_list))
J. Richard Barnetteea785362014-03-17 16:00:53 -0700917
Jakob Juelich24f22c22014-09-26 11:46:11 -0700918
J. Richard Barnetteea785362014-03-17 16:00:53 -0700919 def test_get_regular_jobs(self):
920 """Test `RegularJobDirectory.get_job_directories()`."""
921 self._run_get_directories(job_directories.RegularJobDirectory,
J. Richard Barnette08800322014-05-16 14:49:46 -0700922 self.REGULAR_JOBLIST)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700923
Jakob Juelich24f22c22014-09-26 11:46:11 -0700924
J. Richard Barnetteea785362014-03-17 16:00:53 -0700925 def test_get_special_jobs(self):
926 """Test `SpecialJobDirectory.get_job_directories()`."""
927 self._run_get_directories(job_directories.SpecialJobDirectory,
J. Richard Barnette08800322014-05-16 14:49:46 -0700928 self.SPECIAL_JOBLIST)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700929
930
931class AddJobsTests(_TempResultsDirTestBase):
932 """Tests for `Offloader._add_new_jobs()`."""
933
J. Richard Barnette08800322014-05-16 14:49:46 -0700934 MOREJOBS = ['115-fubar', '116-fubar', '117-fubar', '118-snafu']
J. Richard Barnetteea785362014-03-17 16:00:53 -0700935
936 def setUp(self):
937 super(AddJobsTests, self).setUp()
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700938 self._initial_job_names = (
939 set(self.REGULAR_JOBLIST) | set(self.SPECIAL_JOBLIST))
J. Richard Barnette08800322014-05-16 14:49:46 -0700940 self.make_job_hierarchy()
941 self._offloader = gs_offloader.Offloader(_get_options(['-a']))
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700942 self.mox.StubOutWithMock(logging, 'debug')
J. Richard Barnetteea785362014-03-17 16:00:53 -0700943
Jakob Juelich24f22c22014-09-26 11:46:11 -0700944
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700945 def _run_add_new_jobs(self, expected_key_set):
J. Richard Barnetteea785362014-03-17 16:00:53 -0700946 """Basic test assertions for `_add_new_jobs()`.
947
948 Asserts the following:
949 * The keys in the offloader's `_open_jobs` dictionary
950 matches the expected set of keys.
951 * For every job in `_open_jobs`, the job has the expected
952 directory name.
953
954 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700955 count = len(expected_key_set) - len(self._offloader._open_jobs)
956 logging.debug(mox.IgnoreArg(), count)
957 self.mox.ReplayAll()
958 self._offloader._add_new_jobs()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700959 self.assertEqual(expected_key_set,
960 set(self._offloader._open_jobs.keys()))
961 for jobkey, job in self._offloader._open_jobs.items():
962 self.assertEqual(jobkey, job._dirname)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700963 self.mox.VerifyAll()
964 self.mox.ResetAll()
J. Richard Barnetteea785362014-03-17 16:00:53 -0700965
Jakob Juelich24f22c22014-09-26 11:46:11 -0700966
J. Richard Barnetteea785362014-03-17 16:00:53 -0700967 def test_add_jobs_empty(self):
968 """Test adding jobs to an empty dictionary.
969
970 Calls the offloader's `_add_new_jobs()`, then perform
971 the assertions of `self._check_open_jobs()`.
972
973 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700974 self._run_add_new_jobs(self._initial_job_names)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700975
Jakob Juelich24f22c22014-09-26 11:46:11 -0700976
J. Richard Barnetteea785362014-03-17 16:00:53 -0700977 def test_add_jobs_non_empty(self):
978 """Test adding jobs to a non-empty dictionary.
979
980 Calls the offloader's `_add_new_jobs()` twice; once from
981 initial conditions, and then again after adding more
982 directories. After the second call, perform the assertions
983 of `self._check_open_jobs()`. Additionally, assert that
984 keys added by the first call still map to their original
985 job object after the second call.
986
987 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700988 self._run_add_new_jobs(self._initial_job_names)
J. Richard Barnetteea785362014-03-17 16:00:53 -0700989 jobs_copy = self._offloader._open_jobs.copy()
J. Richard Barnette08800322014-05-16 14:49:46 -0700990 for d in self.MOREJOBS:
991 os.mkdir(d)
J. Richard Barnette22dd7482014-06-23 12:25:02 -0700992 self._run_add_new_jobs(self._initial_job_names |
993 set(self.MOREJOBS))
J. Richard Barnetteea785362014-03-17 16:00:53 -0700994 for key in jobs_copy.keys():
995 self.assertIs(jobs_copy[key],
996 self._offloader._open_jobs[key])
997
998
999class JobStateTests(_TempResultsDirTestBase):
1000 """Tests for job state predicates.
1001
1002 This tests for the expected results from the
1003 `is_offloaded()` and `is_reportable()` predicate
1004 methods.
1005
1006 """
1007
1008 def test_unfinished_job(self):
1009 """Test that an unfinished job reports the correct state.
1010
1011 A job is "unfinished" if it isn't marked complete in the
1012 database. A job in this state is neither "complete" nor
1013 "reportable".
1014
1015 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001016 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001017 self.assertFalse(job.is_offloaded())
1018 self.assertFalse(job.is_reportable())
1019
Jakob Juelich24f22c22014-09-26 11:46:11 -07001020
J. Richard Barnetteea785362014-03-17 16:00:53 -07001021 def test_incomplete_job(self):
1022 """Test that an incomplete job reports the correct state.
1023
1024 A job is "incomplete" if exactly one attempt has been made
1025 to offload the job, but its results directory still exists.
1026 A job in this state is neither "complete" nor "reportable".
1027
1028 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001029 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001030 job.set_incomplete()
1031 self.assertFalse(job.is_offloaded())
1032 self.assertFalse(job.is_reportable())
1033
Jakob Juelich24f22c22014-09-26 11:46:11 -07001034
J. Richard Barnetteea785362014-03-17 16:00:53 -07001035 def test_reportable_job(self):
1036 """Test that a reportable job reports the correct state.
1037
1038 A job is "reportable" if more than one attempt has been made
1039 to offload the job, and its results directory still exists.
1040 A job in this state is "reportable", but not "complete".
1041
1042 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001043 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001044 job.set_reportable()
1045 self.assertFalse(job.is_offloaded())
1046 self.assertTrue(job.is_reportable())
1047
Jakob Juelich24f22c22014-09-26 11:46:11 -07001048
J. Richard Barnetteea785362014-03-17 16:00:53 -07001049 def test_completed_job(self):
1050 """Test that a completed job reports the correct state.
1051
1052 A job is "completed" if at least one attempt has been made
1053 to offload the job, and its results directory still exists.
1054 A job in this state is "complete", and not "reportable".
1055
1056 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001057 job = self.make_job(self.REGULAR_JOBLIST[0])
J. Richard Barnetteea785362014-03-17 16:00:53 -07001058 job.set_complete()
1059 self.assertTrue(job.is_offloaded())
1060 self.assertFalse(job.is_reportable())
1061
1062
1063class ReportingTests(_TempResultsDirTestBase):
1064 """Tests for `Offloader._update_offload_results()`."""
1065
J. Richard Barnetteea785362014-03-17 16:00:53 -07001066 def setUp(self):
1067 super(ReportingTests, self).setUp()
1068 self._offloader = gs_offloader.Offloader(_get_options([]))
1069 self.mox.StubOutWithMock(email_manager.manager,
1070 'send_email')
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001071 self.mox.StubOutWithMock(logging, 'debug')
J. Richard Barnetteea785362014-03-17 16:00:53 -07001072
Jakob Juelich24f22c22014-09-26 11:46:11 -07001073
J. Richard Barnetteea785362014-03-17 16:00:53 -07001074 def _add_job(self, jobdir):
1075 """Add a job to the dictionary of unfinished jobs."""
1076 j = self.make_job(jobdir)
1077 self._offloader._open_jobs[j._dirname] = j
1078 return j
1079
Jakob Juelich24f22c22014-09-26 11:46:11 -07001080
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001081 def _expect_log_message(self, new_open_jobs, with_failures):
1082 """Mock expected logging calls.
1083
1084 `_update_offload_results()` logs one message with the number
1085 of jobs removed from the open job set and the number of jobs
1086 still remaining. Additionally, if there are reportable
1087 jobs, then it logs the number of jobs that haven't yet
1088 offloaded.
1089
1090 This sets up the logging calls using `new_open_jobs` to
1091 figure the job counts. If `with_failures` is true, then
1092 the log message is set up assuming that all jobs in
1093 `new_open_jobs` have offload failures.
1094
1095 @param new_open_jobs New job set for calculating counts
1096 in the messages.
1097 @param with_failures Whether the log message with a
1098 failure count is expected.
1099
1100 """
1101 count = len(self._offloader._open_jobs) - len(new_open_jobs)
1102 logging.debug(mox.IgnoreArg(), count, len(new_open_jobs))
1103 if with_failures:
1104 logging.debug(mox.IgnoreArg(), len(new_open_jobs))
1105
Jakob Juelich24f22c22014-09-26 11:46:11 -07001106
J. Richard Barnetteea785362014-03-17 16:00:53 -07001107 def _run_update_no_report(self, new_open_jobs):
1108 """Call `_update_offload_results()` expecting no report.
1109
1110 Initial conditions are set up by the caller. This calls
1111 `_update_offload_results()` once, and then checks these
1112 assertions:
1113 * The offloader's `_next_report_time` field is unchanged.
1114 * The offloader's new `_open_jobs` field contains only
1115 the entries in `new_open_jobs`.
1116 * The email_manager's `send_email` stub wasn't called.
1117
1118 @param new_open_jobs A dictionary representing the expected
1119 new value of the offloader's
1120 `_open_jobs` field.
1121 """
1122 self.mox.ReplayAll()
1123 next_report_time = self._offloader._next_report_time
1124 self._offloader._update_offload_results()
1125 self.assertEqual(next_report_time,
1126 self._offloader._next_report_time)
1127 self.assertEqual(self._offloader._open_jobs, new_open_jobs)
1128 self.mox.VerifyAll()
1129 self.mox.ResetAll()
1130
Jakob Juelich24f22c22014-09-26 11:46:11 -07001131
J. Richard Barnetteea785362014-03-17 16:00:53 -07001132 def _run_update_with_report(self, new_open_jobs):
1133 """Call `_update_offload_results()` expecting an e-mail report.
1134
1135 Initial conditions are set up by the caller. This calls
1136 `_update_offload_results()` once, and then checks these
1137 assertions:
1138 * The offloader's `_next_report_time` field is updated
1139 to an appropriate new time.
1140 * The offloader's new `_open_jobs` field contains only
1141 the entries in `new_open_jobs`.
1142 * The email_manager's `send_email` stub was called.
1143
1144 @param new_open_jobs A dictionary representing the expected
1145 new value of the offloader's
1146 `_open_jobs` field.
1147 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001148 logging.debug(mox.IgnoreArg())
J. Richard Barnetteea785362014-03-17 16:00:53 -07001149 email_manager.manager.send_email(
1150 mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg())
1151 self.mox.ReplayAll()
1152 t0 = time.time() + gs_offloader.REPORT_INTERVAL_SECS
1153 self._offloader._update_offload_results()
1154 t1 = time.time() + gs_offloader.REPORT_INTERVAL_SECS
1155 next_report_time = self._offloader._next_report_time
1156 self.assertGreaterEqual(next_report_time, t0)
1157 self.assertLessEqual(next_report_time, t1)
1158 self.assertEqual(self._offloader._open_jobs, new_open_jobs)
1159 self.mox.VerifyAll()
1160 self.mox.ResetAll()
1161
Jakob Juelich24f22c22014-09-26 11:46:11 -07001162
J. Richard Barnetteea785362014-03-17 16:00:53 -07001163 def test_no_jobs(self):
1164 """Test `_update_offload_results()` with no open jobs.
1165
1166 Initial conditions are an empty `_open_jobs` list and
1167 `_next_report_time` in the past. Expected result is no
1168 e-mail report, and an empty `_open_jobs` list.
1169
1170 """
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001171 self._expect_log_message({}, False)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001172 self._run_update_no_report({})
1173
Jakob Juelich24f22c22014-09-26 11:46:11 -07001174
J. Richard Barnetteea785362014-03-17 16:00:53 -07001175 def test_all_completed(self):
1176 """Test `_update_offload_results()` with only complete jobs.
1177
1178 Initial conditions are an `_open_jobs` list consisting of
1179 only completed jobs and `_next_report_time` in the past.
1180 Expected result is no e-mail report, and an empty
1181 `_open_jobs` list.
1182
1183 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001184 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001185 self._add_job(d).set_complete()
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001186 self._expect_log_message({}, False)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001187 self._run_update_no_report({})
1188
Jakob Juelich24f22c22014-09-26 11:46:11 -07001189
J. Richard Barnetteea785362014-03-17 16:00:53 -07001190 def test_none_finished(self):
1191 """Test `_update_offload_results()` with only unfinished jobs.
1192
1193 Initial conditions are an `_open_jobs` list consisting of
1194 only unfinished jobs and `_next_report_time` in the past.
1195 Expected result is no e-mail report, and no change to the
1196 `_open_jobs` list.
1197
1198 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001199 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001200 self._add_job(d)
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001201 new_jobs = self._offloader._open_jobs.copy()
1202 self._expect_log_message(new_jobs, False)
1203 self._run_update_no_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001204
Jakob Juelich24f22c22014-09-26 11:46:11 -07001205
J. Richard Barnetteea785362014-03-17 16:00:53 -07001206 def test_none_reportable(self):
1207 """Test `_update_offload_results()` with only incomplete jobs.
1208
1209 Initial conditions are an `_open_jobs` list consisting of
1210 only incomplete jobs and `_next_report_time` in the past.
1211 Expected result is no e-mail report, and no change to the
1212 `_open_jobs` list.
1213
1214 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001215 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001216 self._add_job(d).set_incomplete()
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001217 new_jobs = self._offloader._open_jobs.copy()
1218 self._expect_log_message(new_jobs, False)
1219 self._run_update_no_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001220
Jakob Juelich24f22c22014-09-26 11:46:11 -07001221
J. Richard Barnetteea785362014-03-17 16:00:53 -07001222 def test_report_not_ready(self):
1223 """Test `_update_offload_results()` e-mail throttling.
1224
1225 Initial conditions are an `_open_jobs` list consisting of
1226 only reportable jobs but with `_next_report_time` in
1227 the future. Expected result is no e-mail report, and no
1228 change to the `_open_jobs` list.
1229
1230 """
1231 # N.B. This test may fail if its run time exceeds more than
1232 # about _MARGIN_SECS seconds.
J. Richard Barnette08800322014-05-16 14:49:46 -07001233 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001234 self._add_job(d).set_reportable()
1235 self._offloader._next_report_time += _MARGIN_SECS
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001236 new_jobs = self._offloader._open_jobs.copy()
1237 self._expect_log_message(new_jobs, True)
1238 self._run_update_no_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001239
Jakob Juelich24f22c22014-09-26 11:46:11 -07001240
J. Richard Barnetteea785362014-03-17 16:00:53 -07001241 def test_reportable(self):
1242 """Test `_update_offload_results()` with reportable jobs.
1243
1244 Initial conditions are an `_open_jobs` list consisting of
1245 only reportable jobs and with `_next_report_time` in
1246 the past. Expected result is an e-mail report, and no
1247 change to the `_open_jobs` list.
1248
1249 """
J. Richard Barnette08800322014-05-16 14:49:46 -07001250 for d in self.REGULAR_JOBLIST:
J. Richard Barnetteea785362014-03-17 16:00:53 -07001251 self._add_job(d).set_reportable()
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001252 new_jobs = self._offloader._open_jobs.copy()
1253 self._expect_log_message(new_jobs, True)
1254 self._run_update_with_report(new_jobs)
1255
Jakob Juelich24f22c22014-09-26 11:46:11 -07001256
J. Richard Barnette22dd7482014-06-23 12:25:02 -07001257 def test_reportable_mixed(self):
1258 """Test `_update_offload_results()` with a mixture of jobs.
1259
1260 Initial conditions are an `_open_jobs` list consisting of
1261 one reportable jobs and the remainder of the jobs
1262 incomplete. The value of `_next_report_time` is in the
1263 past. Expected result is an e-mail report that includes
1264 both the reportable and the incomplete jobs, and no change
1265 to the `_open_jobs` list.
1266
1267 """
1268 self._add_job(self.REGULAR_JOBLIST[0]).set_reportable()
1269 for d in self.REGULAR_JOBLIST[1:]:
1270 self._add_job(d).set_incomplete()
1271 new_jobs = self._offloader._open_jobs.copy()
1272 self._expect_log_message(new_jobs, True)
1273 self._run_update_with_report(new_jobs)
J. Richard Barnetteea785362014-03-17 16:00:53 -07001274
1275
1276if __name__ == '__main__':
1277 unittest.main()